diff --git a/internal/db/migrations/00020_drop_non_core_tables.sql b/internal/db/migrations/00020_drop_non_core_tables.sql new file mode 100644 index 0000000..7d3c175 --- /dev/null +++ b/internal/db/migrations/00020_drop_non_core_tables.sql @@ -0,0 +1,392 @@ +-- +goose Up +-- +goose StatementBegin + +-- Leaf tables (no dependents) +DROP TABLE IF EXISTS receipt_funding_sources CASCADE; +DROP TABLE IF EXISTS recurring_receipt_sources CASCADE; +DROP TABLE IF EXISTS expense_items CASCADE; +DROP TABLE IF EXISTS expense_tags CASCADE; +DROP TABLE IF EXISTS recurring_expense_tags CASCADE; +DROP TABLE IF EXISTS budget_tags CASCADE; + +-- Mid-level tables +DROP TABLE IF EXISTS receipts CASCADE; +DROP TABLE IF EXISTS recurring_receipts CASCADE; +DROP TABLE IF EXISTS loans CASCADE; +DROP TABLE IF EXISTS recurring_deposits CASCADE; +DROP TABLE IF EXISTS account_transfers CASCADE; +DROP TABLE IF EXISTS budgets CASCADE; +DROP TABLE IF EXISTS recurring_expenses CASCADE; +DROP TABLE IF EXISTS expenses CASCADE; +DROP TABLE IF EXISTS payment_methods CASCADE; +DROP TABLE IF EXISTS list_items CASCADE; +DROP TABLE IF EXISTS shopping_lists CASCADE; +DROP TABLE IF EXISTS space_invitations CASCADE; +DROP TABLE IF EXISTS money_accounts CASCADE; +DROP TABLE IF EXISTS tags CASCADE; +DROP TABLE IF EXISTS space_members CASCADE; +DROP TABLE IF EXISTS files CASCADE; + +-- Root table (many others referenced this) +DROP TABLE IF EXISTS spaces CASCADE; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +-- Recreate all dropped tables in dependency order (reverse of drop) + +CREATE TABLE IF NOT EXISTS files ( + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL, + owner_type TEXT NOT NULL, + owner_id TEXT NOT NULL, + type TEXT NOT NULL, + filename TEXT NOT NULL, + original_name TEXT, + mime_type TEXT, + size INTEGER, + storage_path TEXT NOT NULL, + public BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_files_user_id ON files(user_id); +CREATE INDEX IF NOT EXISTS idx_files_owner ON files(owner_type, owner_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_files_owner_type ON files(owner_type, owner_id, type) WHERE type IN ('avatar'); + +CREATE TABLE IF NOT EXISTS spaces ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + owner_id TEXT NOT NULL, + timezone TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_spaces_owner ON spaces(owner_id); + +CREATE TABLE IF NOT EXISTS space_members ( + space_id TEXT NOT NULL, + user_id TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN ('owner', 'member')), + joined_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (space_id, user_id), + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_space_members_user ON space_members(user_id); + +CREATE TABLE IF NOT EXISTS tags ( + id TEXT PRIMARY KEY NOT NULL, + space_id TEXT NOT NULL, + name TEXT NOT NULL, + color TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (space_id, name), + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_tags_space_id ON tags(space_id); + +CREATE TABLE IF NOT EXISTS shopping_lists ( + id TEXT PRIMARY KEY NOT NULL, + space_id TEXT NOT NULL, + name TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_shopping_lists_space_id ON shopping_lists(space_id); + +CREATE TABLE IF NOT EXISTS list_items ( + id TEXT PRIMARY KEY NOT NULL, + list_id TEXT NOT NULL, + name TEXT NOT NULL, + is_checked BOOLEAN NOT NULL DEFAULT FALSE, + created_by TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (list_id) REFERENCES shopping_lists(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_list_items_list_id ON list_items(list_id); + +CREATE TABLE IF NOT EXISTS money_accounts ( + id TEXT PRIMARY KEY NOT NULL, + space_id TEXT NOT NULL, + name TEXT NOT NULL, + created_by TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(space_id, name), + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_money_accounts_space_id ON money_accounts(space_id); + +CREATE TABLE payment_methods ( + id TEXT PRIMARY KEY NOT NULL, + space_id TEXT NOT NULL, + name TEXT NOT NULL, + type TEXT NOT NULL CHECK (type IN ('credit', 'debit')), + last_four TEXT NOT NULL, + created_by TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(space_id, name), + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_payment_method_space_id ON payment_methods(space_id); + +CREATE TABLE IF NOT EXISTS expenses ( + id TEXT PRIMARY KEY NOT NULL, + space_id TEXT NOT NULL, + created_by TEXT NOT NULL, + description TEXT NOT NULL, + amount_cents INTEGER NOT NULL, + amount TEXT NOT NULL DEFAULT '0', + type TEXT NOT NULL CHECK (type IN ('expense', 'topup')), + date TIMESTAMP NOT NULL, + payment_method_id TEXT REFERENCES payment_methods(id) ON DELETE SET NULL, + recurring_expense_id TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_expenses_space_id ON expenses(space_id); +CREATE INDEX IF NOT EXISTS idx_expenses_payment_method_id ON expenses(payment_method_id); +CREATE INDEX IF NOT EXISTS idx_expenses_recurring_expense_id ON expenses(recurring_expense_id); + +CREATE TABLE recurring_expenses ( + id TEXT PRIMARY KEY NOT NULL, + space_id TEXT NOT NULL, + created_by TEXT NOT NULL, + description TEXT NOT NULL, + amount_cents INTEGER NOT NULL, + amount TEXT NOT NULL DEFAULT '0', + type TEXT NOT NULL CHECK (type IN ('expense', 'topup')), + payment_method_id TEXT, + frequency TEXT NOT NULL CHECK (frequency IN ('daily', 'weekly', 'biweekly', 'monthly', 'yearly')), + start_date DATE NOT NULL, + end_date DATE, + next_occurrence DATE NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (payment_method_id) REFERENCES payment_methods(id) ON DELETE SET NULL +); +CREATE INDEX IF NOT EXISTS idx_recurring_expenses_space_id ON recurring_expenses(space_id); +CREATE INDEX IF NOT EXISTS idx_recurring_expenses_next_occurrence ON recurring_expenses(next_occurrence); +CREATE INDEX IF NOT EXISTS idx_recurring_expenses_active ON recurring_expenses(is_active); + +-- Add FK for recurring_expense_id on expenses now that recurring_expenses exists +ALTER TABLE expenses ADD CONSTRAINT fk_expenses_recurring_expense + FOREIGN KEY (recurring_expense_id) REFERENCES recurring_expenses(id) ON DELETE SET NULL; + +CREATE TABLE recurring_expense_tags ( + recurring_expense_id TEXT NOT NULL, + tag_id TEXT NOT NULL, + PRIMARY KEY (recurring_expense_id, tag_id), + FOREIGN KEY (recurring_expense_id) REFERENCES recurring_expenses(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS expense_tags ( + expense_id TEXT NOT NULL, + tag_id TEXT NOT NULL, + PRIMARY KEY (expense_id, tag_id), + FOREIGN KEY (expense_id) REFERENCES expenses(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS expense_items ( + expense_id TEXT NOT NULL, + item_id TEXT NOT NULL, + PRIMARY KEY (expense_id, item_id), + FOREIGN KEY (expense_id) REFERENCES expenses(id) ON DELETE CASCADE, + FOREIGN KEY (item_id) REFERENCES list_items(id) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS space_invitations ( + token TEXT PRIMARY KEY NOT NULL, + space_id TEXT NOT NULL, + inviter_id TEXT NOT NULL, + email TEXT NOT NULL, + status TEXT NOT NULL CHECK (status IN ('pending', 'accepted', 'expired')), + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, + FOREIGN KEY (inviter_id) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_space_invitations_email ON space_invitations(email); +CREATE INDEX IF NOT EXISTS idx_space_invitations_space_id ON space_invitations(space_id); + +CREATE TABLE IF NOT EXISTS account_transfers ( + id TEXT PRIMARY KEY NOT NULL, + account_id TEXT NOT NULL, + amount_cents INTEGER NOT NULL, + amount TEXT NOT NULL DEFAULT '0', + direction TEXT NOT NULL CHECK (direction IN ('deposit', 'withdrawal')), + note TEXT NOT NULL DEFAULT '', + recurring_deposit_id TEXT, + created_by TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (account_id) REFERENCES money_accounts(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_account_transfers_account_id ON account_transfers(account_id); + +CREATE TABLE budgets ( + id TEXT PRIMARY KEY NOT NULL, + space_id TEXT NOT NULL, + amount_cents INTEGER NOT NULL, + amount TEXT NOT NULL DEFAULT '0', + period TEXT NOT NULL CHECK (period IN ('weekly', 'monthly', 'yearly')), + start_date DATE NOT NULL, + end_date DATE, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_by TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_budgets_space_id ON budgets(space_id); + +CREATE TABLE budget_tags ( + budget_id TEXT NOT NULL, + tag_id TEXT NOT NULL, + PRIMARY KEY (budget_id, tag_id), + FOREIGN KEY (budget_id) REFERENCES budgets(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_budget_tags_tag_id ON budget_tags(tag_id); + +CREATE TABLE recurring_deposits ( + id TEXT PRIMARY KEY NOT NULL, + space_id TEXT NOT NULL, + account_id TEXT NOT NULL, + amount_cents INTEGER NOT NULL, + amount TEXT NOT NULL DEFAULT '0', + frequency TEXT NOT NULL CHECK (frequency IN ('daily', 'weekly', 'biweekly', 'monthly', 'yearly')), + start_date DATE NOT NULL, + end_date DATE, + next_occurrence DATE NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + title TEXT NOT NULL DEFAULT '', + created_by TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, + FOREIGN KEY (account_id) REFERENCES money_accounts(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_recurring_deposits_space_id ON recurring_deposits(space_id); +CREATE INDEX IF NOT EXISTS idx_recurring_deposits_account_id ON recurring_deposits(account_id); +CREATE INDEX IF NOT EXISTS idx_recurring_deposits_next_occurrence ON recurring_deposits(next_occurrence); +CREATE INDEX IF NOT EXISTS idx_recurring_deposits_active ON recurring_deposits(is_active); + +-- Add FK for recurring_deposit_id on account_transfers now that recurring_deposits exists +ALTER TABLE account_transfers ADD CONSTRAINT fk_account_transfers_recurring_deposit + FOREIGN KEY (recurring_deposit_id) REFERENCES recurring_deposits(id) ON DELETE SET NULL; + +CREATE TABLE loans ( + id TEXT PRIMARY KEY NOT NULL, + space_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + original_amount_cents INTEGER NOT NULL, + original_amount TEXT NOT NULL DEFAULT '0', + interest_rate_bps INTEGER NOT NULL DEFAULT 0, + start_date DATE NOT NULL, + end_date DATE, + is_paid_off BOOLEAN NOT NULL DEFAULT FALSE, + created_by TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_loans_space_id ON loans(space_id); + +CREATE TABLE recurring_receipts ( + id TEXT PRIMARY KEY NOT NULL, + loan_id TEXT NOT NULL, + space_id TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + total_amount_cents INTEGER NOT NULL, + total_amount TEXT NOT NULL DEFAULT '0', + frequency TEXT NOT NULL CHECK (frequency IN ('daily', 'weekly', 'biweekly', 'monthly', 'yearly')), + start_date DATE NOT NULL, + end_date DATE, + next_occurrence DATE NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_by TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (loan_id) REFERENCES loans(id) ON DELETE CASCADE, + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_recurring_receipts_space_id ON recurring_receipts(space_id); +CREATE INDEX IF NOT EXISTS idx_recurring_receipts_loan_id ON recurring_receipts(loan_id); +CREATE INDEX IF NOT EXISTS idx_recurring_receipts_next_occurrence ON recurring_receipts(next_occurrence); +CREATE INDEX IF NOT EXISTS idx_recurring_receipts_active ON recurring_receipts(is_active); + +CREATE TABLE recurring_receipt_sources ( + id TEXT PRIMARY KEY NOT NULL, + recurring_receipt_id TEXT NOT NULL, + source_type TEXT NOT NULL CHECK (source_type IN ('balance', 'account')), + account_id TEXT, + amount_cents INTEGER NOT NULL, + amount TEXT NOT NULL DEFAULT '0', + FOREIGN KEY (recurring_receipt_id) REFERENCES recurring_receipts(id) ON DELETE CASCADE, + FOREIGN KEY (account_id) REFERENCES money_accounts(id) ON DELETE SET NULL +); +CREATE INDEX IF NOT EXISTS idx_recurring_receipt_sources_recurring_receipt_id ON recurring_receipt_sources(recurring_receipt_id); + +CREATE TABLE receipts ( + id TEXT PRIMARY KEY NOT NULL, + loan_id TEXT NOT NULL, + space_id TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + total_amount_cents INTEGER NOT NULL, + total_amount TEXT NOT NULL DEFAULT '0', + date DATE NOT NULL, + recurring_receipt_id TEXT, + created_by TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (loan_id) REFERENCES loans(id) ON DELETE CASCADE, + FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, + FOREIGN KEY (recurring_receipt_id) REFERENCES recurring_receipts(id) ON DELETE SET NULL, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_receipts_loan_id ON receipts(loan_id); +CREATE INDEX IF NOT EXISTS idx_receipts_space_id ON receipts(space_id); +CREATE INDEX IF NOT EXISTS idx_receipts_recurring_receipt_id ON receipts(recurring_receipt_id); + +CREATE TABLE receipt_funding_sources ( + id TEXT PRIMARY KEY NOT NULL, + receipt_id TEXT NOT NULL, + source_type TEXT NOT NULL CHECK (source_type IN ('balance', 'account')), + account_id TEXT, + amount_cents INTEGER NOT NULL, + amount TEXT NOT NULL DEFAULT '0', + linked_expense_id TEXT, + linked_transfer_id TEXT, + FOREIGN KEY (receipt_id) REFERENCES receipts(id) ON DELETE CASCADE, + FOREIGN KEY (account_id) REFERENCES money_accounts(id) ON DELETE SET NULL, + FOREIGN KEY (linked_expense_id) REFERENCES expenses(id) ON DELETE SET NULL, + FOREIGN KEY (linked_transfer_id) REFERENCES account_transfers(id) ON DELETE SET NULL +); +CREATE INDEX IF NOT EXISTS idx_receipt_funding_sources_receipt_id ON receipt_funding_sources(receipt_id); + +-- +goose StatementEnd