11# Standard library imports
22import sqlite3
3- from typing import List , Tuple , TypedDict
3+ from typing import Any , List , Mapping , Tuple , TypedDict , Union
44
55# App-specific imports
66from app .config .settings import (
@@ -25,15 +25,32 @@ class ImageRecord(TypedDict):
2525 path : ImagePath
2626 folder_id : FolderId
2727 thumbnailPath : str
28- metadata : str
28+ metadata : Union [ Mapping [ str , Any ], str ]
2929 isTagged : bool
3030
3131
32+ class UntaggedImageRecord (TypedDict ):
33+ """Represents an image record returned for tagging."""
34+
35+ id : ImageId
36+ path : ImagePath
37+ folder_id : FolderId
38+ thumbnailPath : str
39+ metadata : Mapping [str , Any ]
40+
41+
3242ImageClassPair = Tuple [ImageId , ClassId ]
3343
3444
35- def db_create_images_table () -> None :
45+ def _connect () -> sqlite3 . Connection :
3646 conn = sqlite3 .connect (DATABASE_PATH )
47+ # Ensure ON DELETE CASCADE and other FKs are enforced
48+ conn .execute ("PRAGMA foreign_keys = ON" )
49+ return conn
50+
51+
52+ def db_create_images_table () -> None :
53+ conn = _connect ()
3754 cursor = conn .cursor ()
3855
3956 # Create new images table with merged fields
@@ -73,15 +90,23 @@ def db_bulk_insert_images(image_records: List[ImageRecord]) -> bool:
7390 if not image_records :
7491 return True
7592
76- conn = sqlite3 . connect ( DATABASE_PATH )
93+ conn = _connect ( )
7794 cursor = conn .cursor ()
7895
7996 try :
8097 cursor .executemany (
8198 """
82- INSERT OR IGNORE INTO images (id, path, folder_id, thumbnailPath, metadata, isTagged)
99+ INSERT INTO images (id, path, folder_id, thumbnailPath, metadata, isTagged)
83100 VALUES (:id, :path, :folder_id, :thumbnailPath, :metadata, :isTagged)
84- """ ,
101+ ON CONFLICT(path) DO UPDATE SET
102+ folder_id=excluded.folder_id,
103+ thumbnailPath=excluded.thumbnailPath,
104+ metadata=excluded.metadata,
105+ isTagged=CASE
106+ WHEN excluded.isTagged THEN 1
107+ ELSE images.isTagged
108+ END
109+ """ ,
85110 image_records ,
86111 )
87112 conn .commit ()
@@ -101,7 +126,7 @@ def db_get_all_images() -> List[dict]:
101126 Returns:
102127 List of dictionaries containing all image data including tags
103128 """
104- conn = sqlite3 . connect ( DATABASE_PATH )
129+ conn = _connect ( )
105130 cursor = conn .cursor ()
106131
107132 try :
@@ -136,18 +161,23 @@ def db_get_all_images() -> List[dict]:
136161 tag_name ,
137162 ) in results :
138163 if image_id not in images_dict :
164+ # Safely parse metadata JSON -> dict
165+ from app .utils .images import image_util_parse_metadata
166+
167+ metadata_dict = image_util_parse_metadata (metadata )
168+
139169 images_dict [image_id ] = {
140170 "id" : image_id ,
141171 "path" : path ,
142- "folder_id" : folder_id ,
172+ "folder_id" : str ( folder_id ) ,
143173 "thumbnailPath" : thumbnail_path ,
144- "metadata" : metadata ,
174+ "metadata" : metadata_dict ,
145175 "isTagged" : bool (is_tagged ),
146176 "tags" : [],
147177 }
148178
149- # Add tag if it exists
150- if tag_name :
179+ # Add tag if it exists (avoid duplicates)
180+ if tag_name and tag_name not in images_dict [ image_id ][ "tags" ] :
151181 images_dict [image_id ]["tags" ].append (tag_name )
152182
153183 # Convert to list and set tags to None if empty
@@ -169,7 +199,7 @@ def db_get_all_images() -> List[dict]:
169199 conn .close ()
170200
171201
172- def db_get_untagged_images () -> List [ImageRecord ]:
202+ def db_get_untagged_images () -> List [UntaggedImageRecord ]:
173203 """
174204 Find all images that need AI tagging.
175205 Returns images where:
@@ -179,7 +209,7 @@ def db_get_untagged_images() -> List[ImageRecord]:
179209 Returns:
180210 List of dictionaries containing image data: id, path, folder_id, thumbnailPath, metadata
181211 """
182- conn = sqlite3 . connect ( DATABASE_PATH )
212+ conn = _connect ( )
183213 cursor = conn .cursor ()
184214
185215 try :
@@ -197,13 +227,16 @@ def db_get_untagged_images() -> List[ImageRecord]:
197227
198228 untagged_images = []
199229 for image_id , path , folder_id , thumbnail_path , metadata in results :
230+ from app .utils .images import image_util_parse_metadata
231+
232+ md = image_util_parse_metadata (metadata )
200233 untagged_images .append (
201234 {
202235 "id" : image_id ,
203236 "path" : path ,
204- "folder_id" : folder_id ,
237+ "folder_id" : str ( folder_id ) if folder_id is not None else None ,
205238 "thumbnailPath" : thumbnail_path ,
206- "metadata" : metadata ,
239+ "metadata" : md ,
207240 }
208241 )
209242
@@ -224,7 +257,7 @@ def db_update_image_tagged_status(image_id: ImageId, is_tagged: bool = True) ->
224257 Returns:
225258 True if update was successful, False otherwise
226259 """
227- conn = sqlite3 . connect ( DATABASE_PATH )
260+ conn = _connect ( )
228261 cursor = conn .cursor ()
229262
230263 try :
@@ -255,7 +288,7 @@ def db_insert_image_classes_batch(image_class_pairs: List[ImageClassPair]) -> bo
255288 if not image_class_pairs :
256289 return True
257290
258- conn = sqlite3 . connect ( DATABASE_PATH )
291+ conn = _connect ( )
259292 cursor = conn .cursor ()
260293
261294 try :
@@ -291,7 +324,7 @@ def db_get_images_by_folder_ids(
291324 if not folder_ids :
292325 return []
293326
294- conn = sqlite3 . connect ( DATABASE_PATH )
327+ conn = _connect ( )
295328 cursor = conn .cursor ()
296329
297330 try :
@@ -327,7 +360,7 @@ def db_delete_images_by_ids(image_ids: List[ImageId]) -> bool:
327360 if not image_ids :
328361 return True
329362
330- conn = sqlite3 . connect ( DATABASE_PATH )
363+ conn = _connect ( )
331364 cursor = conn .cursor ()
332365
333366 try :
0 commit comments