feat(metadata): SQLite backend, storage reconciliation and local defaults
- pluggable per-domain Repository interfaces; PostgreSQL and SQLite impls - SQLite on modernc.org/sqlite (WAL, busy_timeout, foreign_keys, immediate tx) - separate migrations/sqlite and db.MigrateSQLite; store.NewPostgres/NewSQLite - cmd/reconcile compares object storage with the journal (report, then --delete) - storage.Backend.List and photos.Repository.AllStorageKeys - DB_BACKEND/SQLITE_PATH and STORAGE_BACKEND/FS_ROOT selectable; local defaults (sqlite ./galo.db, fs ./data) - Makefile reconcile/reconcile-delete; docs and .env.example updated
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
-- Consolidated SQLite schema equivalent to the PostgreSQL migration history.
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
avatar_url TEXT,
|
||||
preferences TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE TABLE albums (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
cover_photo_id TEXT REFERENCES photos(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE TABLE photos (
|
||||
id TEXT PRIMARY KEY,
|
||||
album_id TEXT REFERENCES albums(id) ON DELETE SET NULL,
|
||||
uploader_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
original_filename TEXT NOT NULL DEFAULT '',
|
||||
taken_at TEXT,
|
||||
latitude REAL,
|
||||
longitude REAL,
|
||||
caption TEXT NOT NULL DEFAULT '',
|
||||
exif TEXT NOT NULL DEFAULT '{}',
|
||||
deleted_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE INDEX photos_album_created_idx ON photos (album_id, created_at DESC, id DESC);
|
||||
CREATE INDEX photos_deleted_idx ON photos (deleted_at) WHERE deleted_at IS NOT NULL;
|
||||
|
||||
CREATE TABLE album_members (
|
||||
album_id TEXT NOT NULL REFERENCES albums(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL,
|
||||
invited_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
joined_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
PRIMARY KEY (album_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE invites (
|
||||
id TEXT PRIMARY KEY,
|
||||
album_id TEXT NOT NULL REFERENCES albums(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
role TEXT NOT NULL,
|
||||
created_by TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at TEXT NOT NULL,
|
||||
max_uses INTEGER NOT NULL DEFAULT 1,
|
||||
uses INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE TABLE comments (
|
||||
id TEXT PRIMARY KEY,
|
||||
photo_id TEXT NOT NULL REFERENCES photos(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE INDEX comments_photo_idx ON comments (photo_id, created_at);
|
||||
|
||||
CREATE TABLE likes (
|
||||
photo_id TEXT NOT NULL REFERENCES photos(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
PRIMARY KEY (photo_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE notifications (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
type TEXT NOT NULL,
|
||||
payload_json TEXT NOT NULL DEFAULT '{}',
|
||||
read_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE INDEX notifications_user_idx ON notifications (user_id, created_at DESC);
|
||||
|
||||
CREATE TABLE share_links (
|
||||
id TEXT PRIMARY KEY,
|
||||
album_id TEXT NOT NULL REFERENCES albums(id) ON DELETE CASCADE,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
created_by TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
expires_at TEXT NOT NULL,
|
||||
revoked_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE refresh_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
expires_at TEXT NOT NULL,
|
||||
revoked_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE INDEX refresh_tokens_user_idx ON refresh_tokens (user_id);
|
||||
|
||||
CREATE TABLE tags (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX tags_user_name_idx ON tags (user_id, lower(name));
|
||||
|
||||
CREATE TABLE photo_tags (
|
||||
photo_id TEXT NOT NULL REFERENCES photos(id) ON DELETE CASCADE,
|
||||
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (photo_id, tag_id)
|
||||
);
|
||||
|
||||
CREATE INDEX photo_tags_tag_idx ON photo_tags (tag_id);
|
||||
|
||||
CREATE TABLE objects (
|
||||
id TEXT PRIMARY KEY,
|
||||
storage_key TEXT NOT NULL UNIQUE,
|
||||
mime_type TEXT NOT NULL DEFAULT '',
|
||||
size INTEGER 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 TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE INDEX objects_state_created_idx ON objects (state, created_at);
|
||||
|
||||
CREATE TABLE 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 photo_objects_object_idx ON photo_objects (object_id);
|
||||
|
||||
CREATE TABLE variant_jobs (
|
||||
photo_id TEXT PRIMARY KEY REFERENCES photos(id) ON DELETE CASCADE,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
next_attempt_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
last_error TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||
);
|
||||
|
||||
CREATE INDEX variant_jobs_due_idx ON variant_jobs (next_attempt_at);
|
||||
Reference in New Issue
Block a user