WIP: DICOM import improvements and database query optimizations
This commit is contained in:
parent
75e9ec7722
commit
d5133fd56f
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 strings.HasSuffix(fieldName, "ID") {
|
||||
field.SetString(string(*b))
|
||||
} else {
|
||||
if unpacked := Unpack(*b); unpacked != nil {
|
||||
field.SetString(string(unpacked))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
127
lib/stubs.go
127
lib/stubs.go
|
|
@ -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")
|
||||
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 ---
|
||||
|
|
|
|||
Loading…
Reference in New Issue