tanabata/backend/migrations/003_data_tables.sql
Masahiko AMANO 071829a79e fix(backend): fix file upload and integration test suite
- Make data.files.exif column nullable (was NOT NULL but service passes nil
  for files without EXIF data, causing a constraint violation on upload)
- FileRepo.Create: include id in INSERT so disk storage path and DB record
  share the same UUID (previously DB generated its own UUID, causing a mismatch)
- Integration test: use correct filter DSL format {t=<uuid>} instead of tag:<uuid>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 02:56:04 +03:00

119 lines
5.2 KiB
SQL

-- +goose Up
CREATE TABLE data.categories (
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
name varchar(256) NOT NULL,
notes text,
color char(6),
metadata jsonb,
creator_id smallint NOT NULL REFERENCES core.users(id)
ON UPDATE CASCADE ON DELETE RESTRICT,
is_public boolean NOT NULL DEFAULT false,
CONSTRAINT uni__categories__name UNIQUE (name),
CONSTRAINT chk__categories__color_hex
CHECK (color IS NULL OR color ~* '^[A-Fa-f0-9]{6}$')
);
CREATE TABLE data.tags (
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
name varchar(256) NOT NULL,
notes text,
color char(6),
category_id uuid REFERENCES data.categories(id)
ON UPDATE CASCADE ON DELETE SET NULL,
metadata jsonb,
creator_id smallint NOT NULL REFERENCES core.users(id)
ON UPDATE CASCADE ON DELETE RESTRICT,
is_public boolean NOT NULL DEFAULT false,
CONSTRAINT uni__tags__name UNIQUE (name),
CONSTRAINT chk__tags__color_hex
CHECK (color IS NULL OR color ~* '^[A-Fa-f0-9]{6}$')
);
CREATE TABLE data.tag_rules (
when_tag_id uuid NOT NULL REFERENCES data.tags(id)
ON UPDATE CASCADE ON DELETE CASCADE,
then_tag_id uuid NOT NULL REFERENCES data.tags(id)
ON UPDATE CASCADE ON DELETE CASCADE,
is_active boolean NOT NULL DEFAULT true,
PRIMARY KEY (when_tag_id, then_tag_id)
);
CREATE TABLE data.files (
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
original_name varchar(256), -- original filename at upload time
mime_id smallint NOT NULL REFERENCES core.mime_types(id)
ON UPDATE CASCADE ON DELETE RESTRICT,
content_datetime timestamptz NOT NULL DEFAULT clock_timestamp(), -- content datetime (e.g. photo taken)
notes text,
metadata jsonb, -- user-editable key-value data
exif jsonb, -- EXIF data extracted at upload (immutable)
phash bigint, -- perceptual hash for duplicate detection (future)
creator_id smallint NOT NULL REFERENCES core.users(id)
ON UPDATE CASCADE ON DELETE RESTRICT,
is_public boolean NOT NULL DEFAULT false,
is_deleted boolean NOT NULL DEFAULT false -- soft delete (trash)
);
CREATE TABLE data.file_tag (
file_id uuid NOT NULL REFERENCES data.files(id)
ON UPDATE CASCADE ON DELETE CASCADE,
tag_id uuid NOT NULL REFERENCES data.tags(id)
ON UPDATE CASCADE ON DELETE CASCADE,
PRIMARY KEY (file_id, tag_id)
);
CREATE TABLE data.pools (
id uuid NOT NULL DEFAULT public.uuid_v7() PRIMARY KEY,
name varchar(256) NOT NULL,
notes text,
metadata jsonb,
creator_id smallint NOT NULL REFERENCES core.users(id)
ON UPDATE CASCADE ON DELETE RESTRICT,
is_public boolean NOT NULL DEFAULT false,
CONSTRAINT uni__pools__name UNIQUE (name)
);
-- `position` uses integer with gaps (e.g. 1000, 2000, 3000) to allow
-- insertions without renumbering. Compact when gaps get too small.
CREATE TABLE data.file_pool (
file_id uuid NOT NULL REFERENCES data.files(id)
ON UPDATE CASCADE ON DELETE CASCADE,
pool_id uuid NOT NULL REFERENCES data.pools(id)
ON UPDATE CASCADE ON DELETE CASCADE,
position integer NOT NULL DEFAULT 0,
PRIMARY KEY (file_id, pool_id)
);
COMMENT ON TABLE data.categories IS 'Logical grouping of tags';
COMMENT ON TABLE data.tags IS 'File labels/tags';
COMMENT ON TABLE data.tag_rules IS 'Auto-tagging rules: when when_tag is assigned, then_tag follows';
COMMENT ON TABLE data.files IS 'Managed files; actual content stored on disk as {id}.{ext}';
COMMENT ON TABLE data.file_tag IS 'Many-to-many: files <-> tags';
COMMENT ON TABLE data.pools IS 'Ordered collections of files';
COMMENT ON TABLE data.file_pool IS 'Many-to-many: files <-> pools, with ordering';
COMMENT ON COLUMN data.files.original_name IS 'Original filename at upload time';
COMMENT ON COLUMN data.files.content_datetime IS 'Content datetime (e.g. when photo was taken); falls back to EXIF DateTimeOriginal';
COMMENT ON COLUMN data.files.metadata IS 'User-editable key-value metadata';
COMMENT ON COLUMN data.files.exif IS 'EXIF data extracted at upload time (immutable, system-managed)';
COMMENT ON COLUMN data.files.phash IS 'Perceptual hash for image/video duplicate detection';
COMMENT ON COLUMN data.files.is_deleted IS 'Soft-deleted files (trash); true = in recycle bin';
COMMENT ON COLUMN data.file_pool.position IS 'Manual ordering within pool; uses gapped integers';
-- +goose Down
DROP TABLE IF EXISTS data.file_pool;
DROP TABLE IF EXISTS data.pools;
DROP TABLE IF EXISTS data.file_tag;
DROP TABLE IF EXISTS data.files;
DROP TABLE IF EXISTS data.tag_rules;
DROP TABLE IF EXISTS data.tags;
DROP TABLE IF EXISTS data.categories;