feat(photos): separate object storage from metadata layer
Move physical file facts out of the photos table into an objects ledger:
- objects / photo_objects / variant_jobs tables (migration 000008)
- drop storage_key/thumb_key/preview_key/size/width/height/mime_type from
photos (migration 000009); they are derived via joins to the original object
- album-free storage keys: objects/{id}/{role}.{ext}
- explicit GC via pending/ready/deleting states; objects deleted only when no
photo references them
- background worker drives variant generation (durable variant_jobs) and GC
- copy/move are metadata-only; storage objects are shared between photos
- remove unused storage.DeletePrefix
Non-destructive to the external JSON API: size/dimensions/mime and URLs are
still returned, just sourced from the object layer.
OpenSpec change: media-object-layer
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
DROP TABLE IF EXISTS variant_jobs;
|
||||
DROP TABLE IF EXISTS photo_objects;
|
||||
DROP TABLE IF EXISTS objects;
|
||||
@@ -0,0 +1,39 @@
|
||||
-- Physical objects stored in S3. Keys are opaque and never encode album, photo
|
||||
-- or any other metadata entity. The row is the ledger entry that governs the
|
||||
-- lifecycle of the corresponding blob.
|
||||
CREATE TABLE IF NOT EXISTS objects (
|
||||
id text PRIMARY KEY,
|
||||
storage_key text NOT NULL UNIQUE,
|
||||
mime_type text NOT NULL DEFAULT '',
|
||||
size bigint NOT NULL DEFAULT 0,
|
||||
width integer NOT NULL DEFAULT 0,
|
||||
height integer NOT NULL DEFAULT 0,
|
||||
checksum text NOT NULL DEFAULT '',
|
||||
state text NOT NULL DEFAULT 'pending'
|
||||
CHECK (state IN ('pending', 'ready', 'deleting')),
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS objects_state_created_idx ON objects (state, created_at);
|
||||
|
||||
-- Binding of a photo to its objects by role. The role is a property of the
|
||||
-- binding, not of the object itself.
|
||||
CREATE TABLE IF NOT EXISTS photo_objects (
|
||||
photo_id text NOT NULL REFERENCES photos(id) ON DELETE CASCADE,
|
||||
object_id text NOT NULL REFERENCES objects(id) ON DELETE RESTRICT,
|
||||
role text NOT NULL CHECK (role IN ('original', 'thumb', 'preview')),
|
||||
PRIMARY KEY (photo_id, role)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS photo_objects_object_idx ON photo_objects (object_id);
|
||||
|
||||
-- Durable queue of pending variant generation work, one job per photo.
|
||||
CREATE TABLE IF NOT EXISTS variant_jobs (
|
||||
photo_id text PRIMARY KEY REFERENCES photos(id) ON DELETE CASCADE,
|
||||
attempts integer NOT NULL DEFAULT 0,
|
||||
next_attempt_at timestamptz NOT NULL DEFAULT now(),
|
||||
last_error text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS variant_jobs_due_idx ON variant_jobs (next_attempt_at);
|
||||
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE photos
|
||||
ADD COLUMN IF NOT EXISTS storage_key text NOT NULL DEFAULT '',
|
||||
ADD COLUMN IF NOT EXISTS thumb_key text,
|
||||
ADD COLUMN IF NOT EXISTS preview_key text,
|
||||
ADD COLUMN IF NOT EXISTS size bigint NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS width integer NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS height integer NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS mime_type text NOT NULL DEFAULT '';
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Physical facts move to the objects table; photos keeps only membership and
|
||||
-- presentation data. size/width/height/mime_type are derived from the original
|
||||
-- object via photo_objects + objects.
|
||||
ALTER TABLE photos DROP COLUMN IF EXISTS storage_key;
|
||||
ALTER TABLE photos DROP COLUMN IF EXISTS thumb_key;
|
||||
ALTER TABLE photos DROP COLUMN IF EXISTS preview_key;
|
||||
ALTER TABLE photos DROP COLUMN IF EXISTS size;
|
||||
ALTER TABLE photos DROP COLUMN IF EXISTS width;
|
||||
ALTER TABLE photos DROP COLUMN IF EXISTS height;
|
||||
ALTER TABLE photos DROP COLUMN IF EXISTS mime_type;
|
||||
Reference in New Issue
Block a user