Files
TallyNote/migrations/0000_initial.sql
T
Qiufeng 9719429f4a
TallyNote release / linux-x64 (push) Failing after 2m41s
feat: add TallyNote local reimbursement ledger
2026-08-29 01:02:29 +08:00

129 lines
5.1 KiB
SQL

CREATE TABLE IF NOT EXISTS admins (
id TEXT PRIMARY KEY,
username TEXT NOT NULL,
username_norm TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
password_hash TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','disabled')),
must_change_password INTEGER NOT NULL DEFAULT 1 CHECK(must_change_password IN (0,1)),
auth_version INTEGER NOT NULL DEFAULT 1 CHECK(auth_version >= 1),
version INTEGER NOT NULL DEFAULT 1 CHECK(version >= 1),
created_at INTEGER NOT NULL,
created_by TEXT REFERENCES admins(id) ON DELETE RESTRICT,
password_changed_at INTEGER,
last_login_at INTEGER,
disabled_at INTEGER,
disabled_by TEXT REFERENCES admins(id) ON DELETE RESTRICT
) STRICT;
CREATE UNIQUE INDEX IF NOT EXISTS admins_username_norm_uq ON admins(username_norm);
CREATE TABLE IF NOT EXISTS sessions (
token_hash TEXT PRIMARY KEY,
admin_id TEXT NOT NULL REFERENCES admins(id) ON DELETE CASCADE,
csrf_hash TEXT NOT NULL,
auth_version INTEGER NOT NULL,
created_at INTEGER NOT NULL,
last_seen_at INTEGER NOT NULL,
idle_expires_at INTEGER NOT NULL,
absolute_expires_at INTEGER NOT NULL
) STRICT;
CREATE INDEX IF NOT EXISTS sessions_admin_idx ON sessions(admin_id);
CREATE INDEX IF NOT EXISTS sessions_expiry_idx ON sessions(idle_expires_at);
CREATE TABLE IF NOT EXISTS expenses (
id TEXT PRIMARY KEY,
paid_at INTEGER NOT NULL,
amount_cents INTEGER NOT NULL CHECK(amount_cents > 0 AND amount_cents <= 999999999999),
note TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'unreimbursed' CHECK(status IN ('unreimbursed','reimbursed')),
version INTEGER NOT NULL DEFAULT 1 CHECK(version >= 1),
created_at INTEGER NOT NULL,
created_by TEXT NOT NULL REFERENCES admins(id) ON DELETE RESTRICT,
updated_at INTEGER NOT NULL,
updated_by TEXT NOT NULL REFERENCES admins(id) ON DELETE RESTRICT,
reimbursed_at INTEGER,
reimbursed_by TEXT REFERENCES admins(id) ON DELETE RESTRICT,
deleted_at INTEGER,
deleted_by TEXT REFERENCES admins(id) ON DELETE RESTRICT
) STRICT;
CREATE INDEX IF NOT EXISTS expenses_list_idx ON expenses(deleted_at, status, paid_at DESC);
CREATE TABLE IF NOT EXISTS attachments (
id TEXT PRIMARY KEY,
expense_id TEXT NOT NULL REFERENCES expenses(id) ON DELETE CASCADE,
kind TEXT NOT NULL CHECK(kind IN ('payment_proof','invoice')),
storage_path TEXT NOT NULL UNIQUE,
original_name TEXT NOT NULL,
mime_type TEXT NOT NULL,
size_bytes INTEGER NOT NULL CHECK(size_bytes > 0),
sha256 TEXT NOT NULL,
created_at INTEGER NOT NULL,
created_by TEXT NOT NULL REFERENCES admins(id) ON DELETE RESTRICT
) STRICT;
CREATE INDEX IF NOT EXISTS attachments_expense_idx ON attachments(expense_id);
CREATE TABLE IF NOT EXISTS audit_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
occurred_at INTEGER NOT NULL,
request_id TEXT NOT NULL,
actor_admin_id TEXT,
actor_username TEXT,
action TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id TEXT,
outcome TEXT NOT NULL CHECK(outcome IN ('success','denied','failure')),
before_json TEXT CHECK(before_json IS NULL OR json_valid(before_json)),
after_json TEXT CHECK(after_json IS NULL OR json_valid(after_json)),
metadata_json TEXT CHECK(metadata_json IS NULL OR json_valid(metadata_json))
) STRICT;
CREATE INDEX IF NOT EXISTS audit_time_idx ON audit_events(occurred_at DESC);
CREATE INDEX IF NOT EXISTS audit_target_idx ON audit_events(target_type, target_id, occurred_at DESC);
CREATE TRIGGER IF NOT EXISTS audit_events_no_update BEFORE UPDATE ON audit_events
BEGIN SELECT RAISE(ABORT, 'audit_events are append-only'); END;
CREATE TRIGGER IF NOT EXISTS audit_events_no_delete BEFORE DELETE ON audit_events
BEGIN SELECT RAISE(ABORT, 'audit_events are append-only'); END;
CREATE TABLE IF NOT EXISTS system_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
) STRICT;
CREATE TABLE IF NOT EXISTS export_jobs (
id TEXT PRIMARY KEY,
admin_id TEXT NOT NULL REFERENCES admins(id) ON DELETE CASCADE,
session_hash TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('queued','building','ready','failed','expired')),
selection_json TEXT NOT NULL CHECK(json_valid(selection_json)),
snapshot_json TEXT NOT NULL CHECK(json_valid(snapshot_json)),
file_path TEXT,
file_name TEXT NOT NULL,
size_bytes INTEGER,
sha256 TEXT,
error_message TEXT,
created_at INTEGER NOT NULL,
ready_at INTEGER,
expires_at INTEGER NOT NULL
) STRICT;
CREATE INDEX IF NOT EXISTS exports_expiry_idx ON export_jobs(expires_at);
CREATE INDEX IF NOT EXISTS exports_session_idx ON export_jobs(session_hash);
CREATE TABLE IF NOT EXISTS login_attempts (
key_hash TEXT PRIMARY KEY,
window_start INTEGER NOT NULL,
failures INTEGER NOT NULL,
blocked_until INTEGER
) STRICT;
CREATE TABLE IF NOT EXISTS file_deletions (
id TEXT PRIMARY KEY,
storage_path TEXT NOT NULL,
reason TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','complete','failed')),
attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
created_at INTEGER NOT NULL,
completed_at INTEGER
) STRICT;
CREATE INDEX IF NOT EXISTS file_deletions_status_idx ON file_deletions(status);