1919
2020logger = logging .getLogger (__name__ )
2121
22- SCHEMA_VERSION = 2
22+ SCHEMA_VERSION = 3
2323
2424_FRESH_SCHEMA = """\
2525 CREATE TABLE IF NOT EXISTS manifest (
2626 asset_id TEXT NOT NULL,
2727 zone_id TEXT NOT NULL DEFAULT '',
28+ asset_resource TEXT NOT NULL DEFAULT 'resOriginal',
2829 local_path TEXT NOT NULL,
2930 version_size INTEGER NOT NULL,
3031 version_checksum TEXT,
5859 burst_id TEXT,
5960 original_orientation INTEGER,
6061 raw_fields TEXT,
61- PRIMARY KEY (asset_id, zone_id, local_path )
62+ PRIMARY KEY (asset_id, zone_id, asset_resource )
6263);
6364CREATE INDEX IF NOT EXISTS idx_manifest_path ON manifest(local_path);
6465"""
7677 (2 , "ALTER TABLE manifest ADD COLUMN burst_id TEXT" ),
7778 (2 , "ALTER TABLE manifest ADD COLUMN original_orientation INTEGER" ),
7879 (2 , "ALTER TABLE manifest ADD COLUMN raw_fields TEXT" ),
80+ (3 , "ALTER TABLE manifest ADD COLUMN asset_resource TEXT NOT NULL DEFAULT 'resOriginal'" ),
7981]
8082
8183
@@ -85,6 +87,7 @@ class ManifestRow:
8587
8688 asset_id : str
8789 zone_id : str
90+ asset_resource : str
8891 local_path : str
8992 version_size : int
9093 version_checksum : str | None
@@ -121,7 +124,7 @@ class ManifestRow:
121124
122125
123126_ALL_COLUMNS = (
124- "asset_id, zone_id, local_path, version_size, version_checksum, "
127+ "asset_id, zone_id, asset_resource, local_path, version_size, version_checksum, "
125128 "change_tag, downloaded_at, last_updated_at, item_type, filename, "
126129 "asset_date, added_date, is_favorite, is_hidden, is_deleted, "
127130 "original_width, original_height, duration, orientation, "
@@ -211,12 +214,14 @@ def _migrate_from_v0(self) -> None:
211214 ("burst_id" , "TEXT" ),
212215 ("original_orientation" , "INTEGER" ),
213216 ("raw_fields" , "TEXT" ),
217+ ("asset_resource" , "TEXT NOT NULL DEFAULT 'resOriginal'" ),
214218 ]
215219 for col_name , col_def in new_columns :
216220 if col_name not in existing :
217221 self ._conn .execute (f"ALTER TABLE manifest ADD COLUMN { col_name } { col_def } " ) # type: ignore[union-attr]
218222 logger .info ("Migrated manifest DB from v0 to v%d (%d columns added)" ,
219223 SCHEMA_VERSION , sum (1 for c , _ in new_columns if c not in existing ))
224+ self ._rebuild_pk ()
220225 self ._conn .execute ( # type: ignore[union-attr]
221226 "CREATE INDEX IF NOT EXISTS idx_manifest_path ON manifest(local_path)"
222227 )
@@ -225,10 +230,30 @@ def _run_migrations(self, from_version: int) -> None:
225230 """Run incremental migrations from from_version to SCHEMA_VERSION."""
226231 for version , sql in _MIGRATIONS :
227232 if version > from_version :
228- self ._conn .execute (sql ) # type: ignore[union-attr]
233+ try :
234+ self ._conn .execute (sql ) # type: ignore[union-attr]
235+ except sqlite3 .OperationalError :
236+ pass # column already exists
237+ if from_version < 3 :
238+ self ._rebuild_pk ()
229239 self ._conn .execute (f"PRAGMA user_version={ SCHEMA_VERSION } " ) # type: ignore[union-attr]
230240 self ._conn .commit () # type: ignore[union-attr]
231241
242+ def _rebuild_pk (self ) -> None :
243+ """Rebuild the manifest table with the correct PK (asset_id, zone_id, asset_resource)."""
244+ conn = self ._conn
245+ assert conn is not None
246+ conn .execute ("ALTER TABLE manifest RENAME TO manifest_old" )
247+ conn .executescript (_FRESH_SCHEMA )
248+ # Copy data, keeping only one row per (asset_id, zone_id, asset_resource)
249+ cols = _ALL_COLUMNS
250+ conn .execute (
251+ f"INSERT OR IGNORE INTO manifest ({ cols } ) "
252+ f"SELECT { cols } FROM manifest_old"
253+ )
254+ conn .execute ("DROP TABLE manifest_old" )
255+ conn .commit ()
256+
232257 def close (self ) -> None :
233258 """Close the manifest DB, committing any pending writes."""
234259 if self ._conn :
@@ -265,12 +290,12 @@ def __enter__(self) -> "ManifestDB":
265290 def __exit__ (self , * _ : object ) -> None :
266291 self .close ()
267292
268- def lookup (self , asset_id : str , zone_id : str , local_path : str ) -> ManifestRow | None :
293+ def lookup (self , asset_id : str , zone_id : str , asset_resource : str ) -> ManifestRow | None :
269294 """Look up a manifest entry by identity."""
270295 row = self ._db .execute (
271296 f"SELECT { _ALL_COLUMNS } FROM manifest "
272- "WHERE asset_id = ? AND zone_id = ? AND local_path = ?" ,
273- (asset_id , zone_id , local_path ),
297+ "WHERE asset_id = ? AND zone_id = ? AND asset_resource = ?" ,
298+ (asset_id , zone_id , asset_resource ),
274299 ).fetchone ()
275300 if row is None :
276301 return None
@@ -293,6 +318,7 @@ def upsert(
293318 zone_id : str ,
294319 local_path : str ,
295320 version_size : int ,
321+ asset_resource : str = "resOriginal" ,
296322 version_checksum : str | None = None ,
297323 change_tag : str | None = None ,
298324 item_type : str | None = None ,
@@ -326,7 +352,7 @@ def upsert(
326352 """Insert or update a manifest entry. Auto-flushes every 500 writes."""
327353 now = datetime .now (tz = timezone .utc ).isoformat ()
328354 params = (
329- asset_id , zone_id , local_path , version_size , version_checksum ,
355+ asset_id , zone_id , asset_resource , local_path , version_size , version_checksum ,
330356 change_tag , now , now , item_type , filename ,
331357 asset_date , added_date , is_favorite , is_hidden , is_deleted ,
332358 original_width , original_height , duration , orientation ,
@@ -336,8 +362,9 @@ def upsert(
336362 )
337363 sql = (
338364 f"INSERT INTO manifest ({ _ALL_COLUMNS } ) "
339- "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) "
340- "ON CONFLICT(asset_id, zone_id, local_path) DO UPDATE SET "
365+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) "
366+ "ON CONFLICT(asset_id, zone_id, asset_resource) DO UPDATE SET "
367+ "local_path=excluded.local_path, "
341368 "version_size=excluded.version_size, "
342369 "version_checksum=excluded.version_checksum, "
343370 "change_tag=excluded.change_tag, "
@@ -392,14 +419,14 @@ def upsert(
392419 logger .warning ("Manifest write failed for %s: %s" , local_path , e )
393420 return
394421
395- def update_path (self , asset_id : str , zone_id : str , old_path : str , new_path : str ) -> None :
422+ def update_path (self , asset_id : str , zone_id : str , asset_resource : str , new_path : str ) -> None :
396423 """Update local_path for an existing manifest entry."""
397424 try :
398425 self ._db .execute (
399426 "UPDATE manifest SET local_path = ?, last_updated_at = ? "
400- "WHERE asset_id = ? AND zone_id = ? AND local_path = ?" ,
427+ "WHERE asset_id = ? AND zone_id = ? AND asset_resource = ?" ,
401428 (new_path , datetime .now (tz = timezone .utc ).isoformat (),
402- asset_id , zone_id , old_path ),
429+ asset_id , zone_id , asset_resource ),
403430 )
404431 self ._dirty = True
405432 self ._pending_count += 1
@@ -417,11 +444,11 @@ def count_by_path(self, local_path: str) -> int:
417444 ).fetchone ()
418445 return row [0 ] if row else 0
419446
420- def remove (self , asset_id : str , zone_id : str , local_path : str ) -> None :
447+ def remove (self , asset_id : str , zone_id : str , asset_resource : str ) -> None :
421448 """Remove a manifest entry."""
422449 self ._db .execute (
423- "DELETE FROM manifest WHERE asset_id = ? AND zone_id = ? AND local_path = ?" ,
424- (asset_id , zone_id , local_path ),
450+ "DELETE FROM manifest WHERE asset_id = ? AND zone_id = ? AND asset_resource = ?" ,
451+ (asset_id , zone_id , asset_resource ),
425452 )
426453 self ._dirty = True
427454
0 commit comments