-- Aether – Initial schema -- Phase 0: users, sessions, folders, files (no email) CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- ── Users ───────────────────────────────────────────────────────────────────── CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, -- Argon2id hash is_admin BOOLEAN NOT NULL DEFAULT FALSE, quota_bytes BIGINT NOT NULL DEFAULT 10737418240, -- 10 GB default created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ── Sessions ────────────────────────────────────────────────────────────────── CREATE TABLE sessions ( id TEXT PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, data BYTEA NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL ); CREATE INDEX idx_sessions_user_id ON sessions(user_id); CREATE INDEX idx_sessions_expires ON sessions(expires_at); -- ── Folders ─────────────────────────────────────────────────────────────────── CREATE TABLE folders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, parent_id UUID REFERENCES folders(id) ON DELETE CASCADE, name TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (owner_id, parent_id, name) ); CREATE INDEX idx_folders_owner ON folders(owner_id); CREATE INDEX idx_folders_parent ON folders(parent_id); -- ── Files ───────────────────────────────────────────────────────────────────── CREATE TABLE files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, folder_id UUID REFERENCES folders(id) ON DELETE CASCADE, name TEXT NOT NULL, mime_type TEXT NOT NULL DEFAULT 'application/octet-stream', size BIGINT NOT NULL DEFAULT 0, checksum TEXT NOT NULL, -- SHA-256 hex storage_key TEXT NOT NULL, -- path within blob storage version INTEGER NOT NULL DEFAULT 1, is_deleted BOOLEAN NOT NULL DEFAULT FALSE, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_files_owner ON files(owner_id); CREATE INDEX idx_files_folder ON files(folder_id); CREATE INDEX idx_files_deleted ON files(is_deleted); CREATE INDEX idx_files_storage ON files(storage_key); -- Partial unique index: no two non-deleted files can share name+folder for the same owner. CREATE UNIQUE INDEX idx_files_unique_name ON files(owner_id, folder_id, name) WHERE NOT is_deleted; -- ── File versions ───────────────────────────────────────────────────────────── -- Previous versions of files (kept for Soft-Delete + versioning). CREATE TABLE file_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE, version INTEGER NOT NULL, size BIGINT NOT NULL, checksum TEXT NOT NULL, storage_key TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_file_versions_file ON file_versions(file_id); -- ── Updated_at trigger ──────────────────────────────────────────────────────── CREATE OR REPLACE FUNCTION update_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER trg_folders_updated_at BEFORE UPDATE ON folders FOR EACH ROW EXECUTE FUNCTION update_updated_at(); CREATE TRIGGER trg_files_updated_at BEFORE UPDATE ON files FOR EACH ROW EXECUTE FUNCTION update_updated_at();