WIP: DICOM import improvements and database query optimizations

This commit is contained in:
James 2026-02-14 18:00:30 -05:00
parent 75e9ec7722
commit d5133fd56f
4 changed files with 139 additions and 68 deletions

View File

@ -450,8 +450,8 @@ func getOrCreateStudy(data []byte, dossierID string) (string, error) {
return id, nil
}
// Query for existing study by category+type (parent-agnostic)
studies, err := lib.EntryQuery(nil, dossierID, lib.CategoryImaging, "study", "*")
// Query for existing study
studies, err := lib.EntryRead("", dossierID, &lib.Filter{Category: lib.CategoryImaging, Type: "study"})
if err == nil {
for _, s := range studies {
if s.Value == studyUID {
@ -461,12 +461,6 @@ func getOrCreateStudy(data []byte, dossierID string) (string, error) {
}
}
// Get imaging category root (create if needed)
catRootID, err := lib.EnsureCategoryRoot(dossierID, lib.CategoryImaging)
if err != nil {
return "", fmt.Errorf("ensure imaging category root: %w", err)
}
// Extract study metadata
patientName := formatPatientName(readStringTag(data, 0x0010, 0x0010))
studyDesc := readStringTag(data, 0x0008, 0x1030)
@ -503,7 +497,7 @@ func getOrCreateStudy(data []byte, dossierID string) (string, error) {
e := &lib.Entry{
EntryID: lib.NewID(),
DossierID: dossierID,
ParentID: catRootID, // child of imaging category root
ParentID: dossierID, // child of dossier root
Category: lib.CategoryImaging,
Type: "study",
Value: studyUID,
@ -527,11 +521,8 @@ func getOrCreateSeries(data []byte, dossierID, studyID string) (string, error) {
return id, nil
}
// Query for existing series using V2 API
children, err := lib.EntryList(lib.SystemAccessorID, studyID, lib.CategoryImaging, &lib.EntryFilter{ // nil ctx - import tool
DossierID: dossierID,
Type: "series",
})
// Query for existing series
children, err := lib.EntryRead("", dossierID, &lib.Filter{Category: lib.CategoryImaging, ParentID: studyID, Type: "series"})
if err == nil {
for _, c := range children {
if c.Value == seriesUID && c.Tags == seriesDesc {
@ -578,7 +569,7 @@ func getOrCreateSeries(data []byte, dossierID, studyID string) (string, error) {
Data: string(dataJSON),
SearchKey: modality,
}
if err := lib.EntryWrite("", e); err != nil { // nil ctx - import tool
if err := lib.EntryWrite("", e); err != nil {
return "", err
}
seriesCache[cacheKey] = e.EntryID
@ -676,7 +667,7 @@ func insertSlice(data []byte, sliceID, dossierID, seriesID string, pixelMin, pix
Timestamp: time.Now().Unix(),
Data: string(dataJSON),
}
return lib.EntryWrite("", e) // nil ctx - import tool
return lib.EntryWrite("", e)
}
// ============================================================================
@ -948,7 +939,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err
fmt.Printf(" Error encoding RGB: %v\n", err)
continue
}
if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil { // nil ctx - import tool
if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil {
fmt.Printf(" Error saving RGB: %v\n", err)
continue
}
@ -992,7 +983,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err
fmt.Printf(" Error encoding 16-bit: %v\n", err)
continue
}
if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil { // nil ctx - import tool
if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil {
fmt.Printf(" Error saving 16-bit: %v\n", err)
continue
}
@ -1032,7 +1023,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err
fmt.Printf(" Error encoding RGB: %v\n", err)
continue
}
if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil { // nil ctx - import tool
if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil {
fmt.Printf(" Error saving RGB: %v\n", err)
continue
}
@ -1076,7 +1067,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err
fmt.Printf(" Error encoding 16-bit: %v\n", err)
continue
}
if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil { // nil ctx - import tool
if err := lib.ObjectWrite(nil, dossierID, sliceID, pngData); err != nil {
fmt.Printf(" Error saving 16-bit: %v\n", err)
continue
}
@ -1098,7 +1089,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err
if savedPct > 5.0 {
// Read existing series entry to update its Data field
series, err := lib.EntryGet(nil, seriesID) // nil ctx - import tool
series, err := lib.EntryGet(nil, seriesID)
if err == nil {
var seriesDataMap map[string]interface{}
json.Unmarshal([]byte(series.Data), &seriesDataMap)
@ -1113,7 +1104,7 @@ func importFromPath(inputPath string, dossierID string, seriesFilter string) err
seriesDataMap["crop_height"] = cropH
updatedData, _ := json.Marshal(seriesDataMap)
series.Data = string(updatedData)
lib.EntryWrite("", series) // nil ctx - import tool
lib.EntryWrite("", series)
log(" Crop: %d,%d → %d,%d (%dx%d, saves %.0f%% pixels)\n",
seriesBBox[0], seriesBBox[1], seriesBBox[2], seriesBBox[3], cropW, cropH, savedPct)
}
@ -1184,27 +1175,23 @@ func main() {
fmt.Println("Initialized")
// Look up dossier
dossier, err := lib.DossierGet(nil, dossierID) // nil ctx - import tool
if err != nil {
dossierEntries, err := lib.EntryRead("", dossierID, &lib.Filter{Category: 0})
if err != nil || len(dossierEntries) == 0 {
fmt.Printf("Error: dossier %s not found\n", dossierID)
os.Exit(1)
}
dob := "unknown"
if !dossier.DOB.IsZero() {
dob = dossier.DOB.Format("2006-01-02")
}
fmt.Printf("Dossier: %s (DOB: %s)\n", dossier.Name, dob)
fmt.Printf("Dossier: %s\n", dossierEntries[0].Summary)
// Check for existing imaging data
existing, _ := lib.EntryList(lib.SystemAccessorID, "", lib.CategoryImaging, &lib.EntryFilter{DossierID: dossierID, Limit: 1}) // nil ctx - import tool
existing, _ := lib.EntryRead("", dossierID, &lib.Filter{Category: lib.CategoryImaging, Limit: 1})
if len(existing) > 0 {
fmt.Printf("Clean existing imaging data? (yes/no): ")
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
if strings.TrimSpace(answer) == "yes" {
fmt.Print("Cleaning...")
lib.EntryRemoveByDossier(nil, dossierID) // nil ctx - import tool
lib.ObjectRemoveByDossier(nil, dossierID) // nil ctx - import tool
lib.EntryDeleteByCategory(nil, dossierID, lib.CategoryImaging)
lib.ObjectRemoveByDossier(nil, dossierID)
fmt.Println(" done")
}
}

View File

@ -626,13 +626,14 @@ func encryptField(v reflect.Value, fieldName string) any {
}
}
// createScanDest creates an appropriate scan destination for a Go type
// createScanDest creates an appropriate scan destination for a Go type.
// String fields scan as []byte because they may be packed BLOBs.
func createScanDest(t reflect.Type) any {
switch t.Kind() {
case reflect.Int, reflect.Int64:
return new(sql.NullInt64)
case reflect.String:
return new(sql.NullString)
return new([]byte)
case reflect.Slice:
if t.Elem().Kind() == reflect.Uint8 {
return new([]byte)
@ -656,12 +657,10 @@ func decryptAndSet(field reflect.Value, scanned any, t reflect.Type, fieldName s
}
case reflect.String:
if strings.HasSuffix(fieldName, "ID") {
if ns, ok := scanned.(*sql.NullString); ok && ns.Valid {
field.SetString(ns.String)
}
} else {
if b, ok := scanned.(*[]byte); ok && b != nil && len(*b) > 0 {
if b, ok := scanned.(*[]byte); ok && b != nil && len(*b) > 0 {
if strings.HasSuffix(fieldName, "ID") {
field.SetString(string(*b))
} else {
if unpacked := Unpack(*b); unpacked != nil {
field.SetString(string(unpacked))
}

View File

@ -252,6 +252,18 @@ func EntryWrite(accessorID string, entries ...*Entry) error {
return nil
}
// entryGetByID loads a single entry by ID with RBAC. Internal choke point.
func entryGetByID(accessorID, entryID string) (*Entry, error) {
var e Entry
if err := dbLoad("entries", entryID, &e); err != nil {
return nil, err
}
if !CheckAccess(accessorID, e.DossierID, entryID, PermRead) {
return nil, nil
}
return &e, nil
}
// entrySave inserts or replaces one entry. Internal only.
func entrySave(e *Entry) error {
_, err := db.Exec(`INSERT OR REPLACE INTO entries

View File

@ -85,12 +85,18 @@ func EntryAddBatchValues(entries []Entry) error {
}
func EntryGet(ctx *AccessContext, id string) (*Entry, error) {
log.Printf("[STUB] EntryGet(id=%s) — needs migration to EntryRead", id)
return nil, fmt.Errorf("EntryGet stub: not implemented")
accessorID := ""
if ctx != nil {
accessorID = ctx.AccessorID
}
return entryGetByID(accessorID, id)
}
func EntryQuery(ctx *AccessContext, dossierID string, category int, typ, parent string) ([]*Entry, error) {
log.Printf("[STUB] EntryQuery(dossier=%s, cat=%d, typ=%s, parent=%s)", dossierID, category, typ, parent)
accessorID := ""
if ctx != nil {
accessorID = ctx.AccessorID
}
f := &Filter{Category: category}
if typ != "" {
f.Type = typ
@ -98,31 +104,28 @@ func EntryQuery(ctx *AccessContext, dossierID string, category int, typ, parent
if parent != "" && parent != "*" {
f.ParentID = parent
}
return entryQuery(dossierID, f)
return EntryRead(accessorID, dossierID, f)
}
func EntryList(accessorID string, parent string, category int, f *EntryFilter) ([]*Entry, error) {
log.Printf("[STUB] EntryList(accessor=%s, parent=%s, cat=%d)", accessorID, parent, category)
dossierID := ""
typ := ""
filter := &Filter{Category: category, ParentID: parent}
if f != nil {
dossierID = f.DossierID
typ = f.Type
filter.Type = f.Type
filter.SearchKey = f.SearchKey
filter.FromDate = f.FromDate
filter.ToDate = f.ToDate
filter.Limit = f.Limit
}
return EntryQuery(&AccessContext{AccessorID: accessorID}, dossierID, category, typ, parent)
return EntryRead(accessorID, dossierID, filter)
}
func EntryQueryOld(dossierID string, category int, typ string) ([]*Entry, error) {
log.Printf("[STUB] EntryQueryOld(dossier=%s, cat=%d, typ=%s)", dossierID, category, typ)
f := &Filter{Category: category}
if typ != "" {
f.Type = typ
}
return entryQuery(dossierID, f)
return EntryRead("", dossierID, &Filter{Category: category, Type: typ})
}
func EntryCount(ctx *AccessContext, dossierID string, category int, typ string) (int, error) {
log.Printf("[STUB] EntryCount(dossier=%s, cat=%d, typ=%s)", dossierID, category, typ)
entries, err := EntryQuery(ctx, dossierID, category, typ, "*")
if err != nil {
return 0, err
@ -165,18 +168,15 @@ func EntryRemoveByDossier(ctx *AccessContext, dossierID string) error {
}
func EntryChildren(dossierID, parentID string) ([]*Entry, error) {
log.Printf("[STUB] EntryChildren(dossier=%s, parent=%s)", dossierID, parentID)
return entryQuery(dossierID, &Filter{Category: -1, ParentID: parentID})
return EntryRead("", dossierID, &Filter{Category: -1, ParentID: parentID})
}
func EntryChildrenByType(dossierID, parentID string, typ string) ([]*Entry, error) {
log.Printf("[STUB] EntryChildrenByType(dossier=%s, parent=%s, typ=%s)", dossierID, parentID, typ)
return entryQuery(dossierID, &Filter{Category: -1, ParentID: parentID, Type: typ})
return EntryRead("", dossierID, &Filter{Category: -1, ParentID: parentID, Type: typ})
}
func EntryTypes(dossierID string, category int) ([]string, error) {
log.Printf("[STUB] EntryTypes(dossier=%s, cat=%d)", dossierID, category)
entries, err := entryQuery(dossierID, &Filter{Category: category})
entries, err := EntryRead("", dossierID, &Filter{Category: category})
if err != nil {
return nil, err
}
@ -194,7 +194,11 @@ func EntryTypes(dossierID string, category int) ([]string, error) {
// --- Dossier stubs ---
func DossierGet(ctx *AccessContext, id string) (*Dossier, error) {
entries, err := entryQuery(id, &Filter{Category: 0})
accessorID := ""
if ctx != nil {
accessorID = ctx.AccessorID
}
entries, err := EntryRead(accessorID, id, &Filter{Category: 0})
if err != nil || len(entries) == 0 {
return nil, fmt.Errorf("dossier not found: %s", id)
}
@ -243,8 +247,52 @@ func DossierList(ctx *AccessContext, f *DossierFilter) ([]*Dossier, error) {
}
func DossierQuery(accessorID string) ([]*DossierQueryRow, error) {
log.Printf("[STUB] DossierQuery(accessor=%s)", accessorID)
return nil, nil
// Get all accessible dossier profiles via RBAC
dossierEntries, err := EntryRead(accessorID, "", &Filter{Category: 0})
if err != nil {
return nil, err
}
var rows []*DossierQueryRow
for _, de := range dossierEntries {
d := Dossier{DossierID: de.DossierID, Name: de.Summary, Email: de.SearchKey}
if de.Data != "" {
var data struct {
DOB string `json:"dob"`
Sex int `json:"sex"`
Lang string `json:"lang"`
}
if json.Unmarshal([]byte(de.Data), &data) == nil {
d.DateOfBirth = data.DOB
d.Sex = data.Sex
if data.Lang != "" {
d.Preferences.Language = data.Lang
}
}
}
// Count entries by category for this dossier
allEntries, err := EntryRead(accessorID, de.DossierID, &Filter{Category: -1})
if err != nil {
continue
}
catCounts := map[int]int{}
for _, e := range allEntries {
if e.Category > 0 {
catCounts[e.Category]++
}
}
if len(catCounts) == 0 {
// Still include dossier even with no entries
rows = append(rows, &DossierQueryRow{Dossier: d})
} else {
for cat, count := range catCounts {
rows = append(rows, &DossierQueryRow{Dossier: d, Category: cat, EntryCount: count})
}
}
}
return rows, nil
}
func DossierSetAuthCode(dossierID string, code int, expiresAt int64) error {
@ -462,8 +510,7 @@ func EntryCategoryCounts(ctx *AccessContext, dossierID string) (map[string]int,
}
func EntryQueryByDate(dossierID string, from, to int64) ([]*Entry, error) {
log.Printf("[STUB] EntryQueryByDate(dossier=%s, from=%d, to=%d)", dossierID, from, to)
return entryQuery(dossierID, &Filter{Category: -1, FromDate: from, ToDate: to})
return EntryRead("", dossierID, &Filter{Category: -1, FromDate: from, ToDate: to})
}
// --- Audit types and stubs ---
@ -478,8 +525,34 @@ type AuditFilter struct {
}
func AuditList(f *AuditFilter) ([]*AuditEntry, error) {
log.Printf("[STUB] AuditList")
return nil, nil
if f == nil {
return nil, nil
}
q := "SELECT AuditID, Actor1ID, Actor2ID, TargetID, Action, Details, RelationID, Timestamp FROM audit WHERE 1=1"
var args []any
if f.TargetID != "" {
q += " AND TargetID = ?"
args = append(args, f.TargetID)
}
if f.ActorID != "" {
q += " AND Actor1ID = ?"
args = append(args, f.ActorID)
}
if f.FromDate > 0 {
q += " AND Timestamp >= ?"
args = append(args, f.FromDate)
}
if f.ToDate > 0 {
q += " AND Timestamp < ?"
args = append(args, f.ToDate)
}
q += " ORDER BY Timestamp DESC"
if f.Limit > 0 {
q += fmt.Sprintf(" LIMIT %d", f.Limit)
}
var entries []*AuditEntry
err := dbQuery(q, args, &entries)
return entries, err
}
// --- Tracker types and stubs ---