Adds a `libraries` registry table and threads library_id through per-instance metadata tables (image_exif, photo_insights, entity_photo_links, video_preview_clips). File-path columns renamed to rel_path to make the relative-to-root semantics explicit. Adds content_hash + size_bytes on image_exif to support future hash-keyed thumbnail/HLS dedup. Tags and favorites stay library-agnostic so they share across libraries by rel_path. Behavior is unchanged: a single primary library (id=1) is seeded from BASE_PATH on first boot; all handlers and DAOs route through it as a transitional shim until the API gains a library query param. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
217 lines
8.2 KiB
SQL
217 lines
8.2 KiB
SQL
-- Multi-library support.
|
|
-- Adds `libraries` registry table and a `library_id` column on per-instance
|
|
-- metadata tables. Renames `file_path` / `photo_name` to `rel_path` for
|
|
-- semantic clarity (values already stored relative to BASE_PATH).
|
|
-- Adds `content_hash` + `size_bytes` to `image_exif` to support
|
|
-- content-based dedup of thumbnails and HLS output across libraries.
|
|
--
|
|
-- SQLite cannot alter column constraints in place, so per-instance tables
|
|
-- are recreated following the idiom established in
|
|
-- 2026-04-02-000000_photo_insights_history/up.sql. Existing row `id`s are
|
|
-- preserved so foreign keys (entity_facts.source_insight_id, etc.) remain
|
|
-- valid after migration.
|
|
|
|
PRAGMA foreign_keys=OFF;
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 1. Libraries registry.
|
|
-- Seeded with a placeholder for the primary library; AppState patches
|
|
-- `root_path` from the BASE_PATH env var on first boot. Subsequent
|
|
-- prod-to-dev DB syncs update this row via a single SQL UPDATE.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE libraries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
name TEXT NOT NULL UNIQUE,
|
|
root_path TEXT NOT NULL,
|
|
created_at BIGINT NOT NULL
|
|
);
|
|
|
|
INSERT INTO libraries (id, name, root_path, created_at)
|
|
VALUES (1, 'main', 'BASE_PATH_PLACEHOLDER', strftime('%s','now'));
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 2. image_exif: + library_id, file_path → rel_path, + content_hash/size_bytes.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE image_exif_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
library_id INTEGER NOT NULL REFERENCES libraries(id),
|
|
rel_path TEXT NOT NULL,
|
|
|
|
-- Camera information
|
|
camera_make TEXT,
|
|
camera_model TEXT,
|
|
lens_model TEXT,
|
|
|
|
-- Image properties
|
|
width INTEGER,
|
|
height INTEGER,
|
|
orientation INTEGER,
|
|
|
|
-- GPS
|
|
gps_latitude REAL,
|
|
gps_longitude REAL,
|
|
gps_altitude REAL,
|
|
|
|
-- Capture settings
|
|
focal_length REAL,
|
|
aperture REAL,
|
|
shutter_speed TEXT,
|
|
iso INTEGER,
|
|
date_taken BIGINT,
|
|
|
|
-- Housekeeping
|
|
created_time BIGINT NOT NULL,
|
|
last_modified BIGINT NOT NULL,
|
|
|
|
-- Content identity (backfilled by the `backfill_hashes` binary and by the watcher for new files)
|
|
content_hash TEXT,
|
|
size_bytes BIGINT,
|
|
|
|
UNIQUE(library_id, rel_path)
|
|
);
|
|
|
|
INSERT INTO image_exif_new (
|
|
id, library_id, rel_path,
|
|
camera_make, camera_model, lens_model,
|
|
width, height, orientation,
|
|
gps_latitude, gps_longitude, gps_altitude,
|
|
focal_length, aperture, shutter_speed, iso, date_taken,
|
|
created_time, last_modified
|
|
)
|
|
SELECT
|
|
id, 1, file_path,
|
|
camera_make, camera_model, lens_model,
|
|
width, height, orientation,
|
|
gps_latitude, gps_longitude, gps_altitude,
|
|
focal_length, aperture, shutter_speed, iso, date_taken,
|
|
created_time, last_modified
|
|
FROM image_exif;
|
|
|
|
DROP TABLE image_exif;
|
|
ALTER TABLE image_exif_new RENAME TO image_exif;
|
|
|
|
CREATE INDEX idx_image_exif_rel_path ON image_exif(rel_path);
|
|
CREATE INDEX idx_image_exif_camera ON image_exif(camera_make, camera_model);
|
|
CREATE INDEX idx_image_exif_gps ON image_exif(gps_latitude, gps_longitude);
|
|
CREATE INDEX idx_image_exif_date_taken ON image_exif(date_taken);
|
|
CREATE INDEX idx_image_exif_date_path ON image_exif(date_taken DESC, rel_path);
|
|
CREATE INDEX idx_image_exif_lib_date ON image_exif(library_id, date_taken);
|
|
CREATE INDEX idx_image_exif_content_hash ON image_exif(content_hash);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 3. photo_insights: + library_id, file_path → rel_path.
|
|
-- Preserve `id` so entity_facts.source_insight_id FKs remain valid.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE photo_insights_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
library_id INTEGER NOT NULL REFERENCES libraries(id),
|
|
rel_path TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
summary TEXT NOT NULL,
|
|
generated_at BIGINT NOT NULL,
|
|
model_version TEXT NOT NULL,
|
|
is_current BOOLEAN NOT NULL DEFAULT 0,
|
|
training_messages TEXT,
|
|
approved BOOLEAN
|
|
);
|
|
|
|
INSERT INTO photo_insights_new (
|
|
id, library_id, rel_path, title, summary, generated_at, model_version,
|
|
is_current, training_messages, approved
|
|
)
|
|
SELECT
|
|
id, 1, file_path, title, summary, generated_at, model_version,
|
|
is_current, training_messages, approved
|
|
FROM photo_insights;
|
|
|
|
DROP TABLE photo_insights;
|
|
ALTER TABLE photo_insights_new RENAME TO photo_insights;
|
|
|
|
CREATE INDEX idx_photo_insights_rel_path ON photo_insights(rel_path);
|
|
CREATE INDEX idx_photo_insights_current ON photo_insights(library_id, rel_path, is_current);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 4. entity_photo_links: + library_id, file_path → rel_path.
|
|
-- Preserves entity FK; UNIQUE now includes library_id to allow the same
|
|
-- rel_path to link entities in multiple libraries independently.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE entity_photo_links_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
entity_id INTEGER NOT NULL,
|
|
library_id INTEGER NOT NULL REFERENCES libraries(id),
|
|
rel_path TEXT NOT NULL,
|
|
role TEXT NOT NULL,
|
|
CONSTRAINT fk_epl_entity FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE,
|
|
UNIQUE(entity_id, library_id, rel_path, role)
|
|
);
|
|
|
|
INSERT INTO entity_photo_links_new (id, entity_id, library_id, rel_path, role)
|
|
SELECT id, entity_id, 1, file_path, role FROM entity_photo_links;
|
|
|
|
DROP TABLE entity_photo_links;
|
|
ALTER TABLE entity_photo_links_new RENAME TO entity_photo_links;
|
|
|
|
CREATE INDEX idx_entity_photo_links_entity ON entity_photo_links(entity_id);
|
|
CREATE INDEX idx_entity_photo_links_photo ON entity_photo_links(library_id, rel_path);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 5. video_preview_clips: + library_id, file_path → rel_path.
|
|
-- ---------------------------------------------------------------------------
|
|
CREATE TABLE video_preview_clips_new (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
library_id INTEGER NOT NULL REFERENCES libraries(id),
|
|
rel_path TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
duration_seconds REAL,
|
|
file_size_bytes INTEGER,
|
|
error_message TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
UNIQUE(library_id, rel_path)
|
|
);
|
|
|
|
INSERT INTO video_preview_clips_new (
|
|
id, library_id, rel_path, status, duration_seconds, file_size_bytes,
|
|
error_message, created_at, updated_at
|
|
)
|
|
SELECT
|
|
id, 1, file_path, status, duration_seconds, file_size_bytes,
|
|
error_message, created_at, updated_at
|
|
FROM video_preview_clips;
|
|
|
|
DROP TABLE video_preview_clips;
|
|
ALTER TABLE video_preview_clips_new RENAME TO video_preview_clips;
|
|
|
|
CREATE INDEX idx_preview_clips_rel_path ON video_preview_clips(rel_path);
|
|
CREATE INDEX idx_preview_clips_status ON video_preview_clips(status);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 6. favorites: path → rel_path. Library-agnostic (cross-library sharing).
|
|
-- ---------------------------------------------------------------------------
|
|
ALTER TABLE favorites RENAME COLUMN path TO rel_path;
|
|
|
|
DROP INDEX IF EXISTS idx_favorites_path;
|
|
DROP INDEX IF EXISTS idx_favorites_unique;
|
|
CREATE INDEX idx_favorites_rel_path ON favorites(rel_path);
|
|
CREATE UNIQUE INDEX idx_favorites_unique ON favorites(userid, rel_path);
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- 7. tagged_photo: photo_name → rel_path. Library-agnostic.
|
|
-- Dedup first so the (rel_path, tag_id) unique index can be created safely.
|
|
-- ---------------------------------------------------------------------------
|
|
ALTER TABLE tagged_photo RENAME COLUMN photo_name TO rel_path;
|
|
|
|
DELETE FROM tagged_photo
|
|
WHERE id NOT IN (
|
|
SELECT MIN(id) FROM tagged_photo GROUP BY rel_path, tag_id
|
|
);
|
|
|
|
DROP INDEX IF EXISTS idx_tagged_photo_photo_name;
|
|
DROP INDEX IF EXISTS idx_tagged_photo_count;
|
|
CREATE INDEX idx_tagged_photo_rel_path ON tagged_photo(rel_path);
|
|
CREATE UNIQUE INDEX idx_tagged_photo_relpath_tag ON tagged_photo(rel_path, tag_id);
|
|
|
|
PRAGMA foreign_keys=ON;
|
|
|
|
ANALYZE;
|