Adopt pressly/goose for schema migrations
Replaces the previous hand-rolled MigrateWithLock + GORM AutoMigrate path,
which had two compounding problems:
- AutoMigrate ran on every pod startup (~5 min over the transatlantic
link) even when no schema changes had landed
- pg_advisory_lock is session-scoped, which silently fails through
Neon's pgbouncer transaction-mode pooler — turns out this is a
known and documented limitation that bites golang-migrate too
Goose was chosen over golang-migrate (the other heavyweight) because:
- Goose wraps each migration file in a transaction by default, so a
failure rolls back cleanly instead of leaving a "dirty" version
state requiring manual force-reset (golang-migrate's known
weakness, per its own issue tracker — see #1001 + Atlas's writeup)
- Goose's locking is opt-in. We don't opt in: migrations run as a
single Kubernetes Job, which IS the singleton process. No advisory
lock needed at all.
Layout:
- migrations/000001_init.sql — schema-only pg_dump of the live Neon
DB at adoption, stripped of psql-only directives that block goose's
bookkeeping insert. Pre-goose hand-numbered migrations 002-022 had
their effects folded into this baseline; deleted from the live tree
but preserved in git history at 58e6997.
- Dockerfile installs `goose v3.22.1` at build time and copies the
binary into the api image. The migrate Job reuses the api image with
command=goose, so no separate image to build/push/version.
- deploy-k3s/manifests/migrate/job.yaml: a one-shot Job that strips
the -pooler segment from DB_HOST (advisory lock won't survive
pgbouncer transaction-mode), runs `goose up`, exits.
- deploy-k3s/scripts/03-deploy.sh: deletes any prior Job, applies the
fresh one, `kubectl wait --for=condition=complete --timeout=10m`,
then proceeds with api/worker rollout. Job failure aborts the deploy
before any new app pod sees a stale schema.
- internal/database/database.go::RequireSchemaApplied checks
goose_db_version on startup. api/worker refuse to boot if the
table is missing or its latest row has is_applied=false — the
fail-fast for "operator forgot to run migrate."
- Makefile: migrate-up / migrate-down / migrate-status / migrate-new
for local workflow.
Production DB was bootstrapped manually:
$ goose -dir migrations postgres "$DSN" version # creates table
$ psql ... -c "INSERT INTO goose_db_version (version_id, is_applied, tstamp) VALUES (1, true, NOW());"
Smoke test against fresh Postgres locally: 50 user tables created in
284ms via `goose up`, version_id=1 + is_applied=t recorded.
Verified the local goose CLI talks to prod successfully:
$ goose ... status
Applied At Migration
=======================================
Mon Apr 27 03:43:55 2026 -- 000001_init.sql
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
-- Rollback GoAdmin tables
|
||||
|
||||
DROP TABLE IF EXISTS goadmin_role_menu;
|
||||
DROP TABLE IF EXISTS goadmin_role_permissions;
|
||||
DROP TABLE IF EXISTS goadmin_user_permissions;
|
||||
DROP TABLE IF EXISTS goadmin_role_users;
|
||||
DROP TABLE IF EXISTS goadmin_operation_log;
|
||||
DROP TABLE IF EXISTS goadmin_site;
|
||||
DROP TABLE IF EXISTS goadmin_menu;
|
||||
DROP TABLE IF EXISTS goadmin_permissions;
|
||||
DROP TABLE IF EXISTS goadmin_roles;
|
||||
DROP TABLE IF EXISTS goadmin_users;
|
||||
DROP TABLE IF EXISTS goadmin_session;
|
||||
@@ -1,185 +0,0 @@
|
||||
-- GoAdmin required tables for PostgreSQL
|
||||
-- This migration creates all tables needed by GoAdmin
|
||||
|
||||
-- Session storage table
|
||||
CREATE TABLE IF NOT EXISTS goadmin_session (
|
||||
id SERIAL PRIMARY KEY,
|
||||
sid VARCHAR(50) NOT NULL DEFAULT '',
|
||||
"values" TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_goadmin_session_sid ON goadmin_session(sid);
|
||||
|
||||
-- Users table for admin authentication
|
||||
CREATE TABLE IF NOT EXISTS goadmin_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL DEFAULT '',
|
||||
password VARCHAR(100) NOT NULL DEFAULT '',
|
||||
name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
avatar VARCHAR(255) DEFAULT '',
|
||||
remember_token VARCHAR(100) DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_goadmin_users_username ON goadmin_users(username);
|
||||
|
||||
-- Roles table
|
||||
CREATE TABLE IF NOT EXISTS goadmin_roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL DEFAULT '',
|
||||
slug VARCHAR(50) NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_goadmin_roles_slug ON goadmin_roles(slug);
|
||||
|
||||
-- Permissions table
|
||||
CREATE TABLE IF NOT EXISTS goadmin_permissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL DEFAULT '',
|
||||
slug VARCHAR(50) NOT NULL DEFAULT '',
|
||||
http_method VARCHAR(255) DEFAULT '',
|
||||
http_path TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_goadmin_permissions_slug ON goadmin_permissions(slug);
|
||||
|
||||
-- Role-User relationship table
|
||||
CREATE TABLE IF NOT EXISTS goadmin_role_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
role_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_goadmin_role_users_role_id ON goadmin_role_users(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_goadmin_role_users_user_id ON goadmin_role_users(user_id);
|
||||
|
||||
-- User-Permission relationship table
|
||||
CREATE TABLE IF NOT EXISTS goadmin_user_permissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
permission_id INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_goadmin_user_permissions_user_id ON goadmin_user_permissions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_goadmin_user_permissions_permission_id ON goadmin_user_permissions(permission_id);
|
||||
|
||||
-- Role-Permission relationship table
|
||||
CREATE TABLE IF NOT EXISTS goadmin_role_permissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
role_id INT NOT NULL,
|
||||
permission_id INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_goadmin_role_permissions_role_id ON goadmin_role_permissions(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_goadmin_role_permissions_permission_id ON goadmin_role_permissions(permission_id);
|
||||
|
||||
-- Menu table for admin sidebar
|
||||
CREATE TABLE IF NOT EXISTS goadmin_menu (
|
||||
id SERIAL PRIMARY KEY,
|
||||
parent_id INT NOT NULL DEFAULT 0,
|
||||
type INT NOT NULL DEFAULT 0,
|
||||
"order" INT NOT NULL DEFAULT 0,
|
||||
title VARCHAR(50) NOT NULL DEFAULT '',
|
||||
icon VARCHAR(50) NOT NULL DEFAULT '',
|
||||
uri VARCHAR(3000) NOT NULL DEFAULT '',
|
||||
header VARCHAR(150) DEFAULT '',
|
||||
plugin_name VARCHAR(150) NOT NULL DEFAULT '',
|
||||
uuid VARCHAR(150) DEFAULT '',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_goadmin_menu_parent_id ON goadmin_menu(parent_id);
|
||||
|
||||
-- Role-Menu relationship table
|
||||
CREATE TABLE IF NOT EXISTS goadmin_role_menu (
|
||||
id SERIAL PRIMARY KEY,
|
||||
role_id INT NOT NULL,
|
||||
menu_id INT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_goadmin_role_menu_role_id ON goadmin_role_menu(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_goadmin_role_menu_menu_id ON goadmin_role_menu(menu_id);
|
||||
|
||||
-- Operation log table for audit trail
|
||||
CREATE TABLE IF NOT EXISTS goadmin_operation_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
path VARCHAR(255) NOT NULL DEFAULT '',
|
||||
method VARCHAR(10) NOT NULL DEFAULT '',
|
||||
ip VARCHAR(15) NOT NULL DEFAULT '',
|
||||
input TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_goadmin_operation_log_user_id ON goadmin_operation_log(user_id);
|
||||
|
||||
-- Site configuration table
|
||||
CREATE TABLE IF NOT EXISTS goadmin_site (
|
||||
id SERIAL PRIMARY KEY,
|
||||
key VARCHAR(100) NOT NULL DEFAULT '',
|
||||
value TEXT NOT NULL,
|
||||
description VARCHAR(3000) DEFAULT '',
|
||||
state INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_goadmin_site_key ON goadmin_site(key);
|
||||
|
||||
-- Insert default admin user (password: admin)
|
||||
-- Password is bcrypt hash of 'admin'
|
||||
INSERT INTO goadmin_users (username, password, name, avatar)
|
||||
VALUES ('admin', '$2a$10$sRv1E1XmGXS5HgU7VK3bNOQRZLGDON0.2xvMlz.bKcIzI3pAF1T3y', 'Administrator', '')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Insert default roles
|
||||
INSERT INTO goadmin_roles (name, slug) VALUES ('Administrator', 'administrator') ON CONFLICT DO NOTHING;
|
||||
INSERT INTO goadmin_roles (name, slug) VALUES ('Operator', 'operator') ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Insert default permissions
|
||||
INSERT INTO goadmin_permissions (name, slug, http_method, http_path)
|
||||
VALUES ('All permissions', '*', '', '*') ON CONFLICT DO NOTHING;
|
||||
INSERT INTO goadmin_permissions (name, slug, http_method, http_path)
|
||||
VALUES ('Dashboard', 'dashboard', 'GET', '/') ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Assign admin user to administrator role
|
||||
INSERT INTO goadmin_role_users (role_id, user_id)
|
||||
SELECT r.id, u.id FROM goadmin_roles r, goadmin_users u
|
||||
WHERE r.slug = 'administrator' AND u.username = 'admin'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Assign all permissions to administrator role
|
||||
INSERT INTO goadmin_role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id FROM goadmin_roles r, goadmin_permissions p
|
||||
WHERE r.slug = 'administrator' AND p.slug = '*'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Insert default menu items
|
||||
INSERT INTO goadmin_menu (parent_id, type, "order", title, icon, uri, plugin_name) VALUES
|
||||
(0, 1, 1, 'Dashboard', 'fa-bar-chart', '/', ''),
|
||||
(0, 1, 2, 'Admin', 'fa-tasks', '', ''),
|
||||
(2, 1, 1, 'Users', 'fa-users', '/info/goadmin_users', ''),
|
||||
(2, 1, 2, 'Roles', 'fa-user', '/info/goadmin_roles', ''),
|
||||
(2, 1, 3, 'Permissions', 'fa-ban', '/info/goadmin_permissions', ''),
|
||||
(2, 1, 4, 'Menu', 'fa-bars', '/menu', ''),
|
||||
(2, 1, 5, 'Operation Log', 'fa-history', '/info/goadmin_operation_log', ''),
|
||||
(0, 1, 3, 'HoneyDue', 'fa-home', '', ''),
|
||||
(8, 1, 1, 'Users', 'fa-user', '/info/users', ''),
|
||||
(8, 1, 2, 'Residences', 'fa-building', '/info/residences', ''),
|
||||
(8, 1, 3, 'Tasks', 'fa-tasks', '/info/tasks', ''),
|
||||
(8, 1, 4, 'Contractors', 'fa-wrench', '/info/contractors', ''),
|
||||
(8, 1, 5, 'Documents', 'fa-file', '/info/documents', ''),
|
||||
(8, 1, 6, 'Notifications', 'fa-bell', '/info/notifications', '')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Assign all menus to administrator role
|
||||
INSERT INTO goadmin_role_menu (role_id, menu_id)
|
||||
SELECT r.id, m.id FROM goadmin_roles r, goadmin_menu m
|
||||
WHERE r.slug = 'administrator'
|
||||
ON CONFLICT DO NOTHING;
|
||||
@@ -1,4 +0,0 @@
|
||||
-- Revert: Make residence_id required again
|
||||
-- WARNING: This will fail if there are contractors with NULL residence_id
|
||||
|
||||
ALTER TABLE task_contractor ALTER COLUMN residence_id SET NOT NULL;
|
||||
@@ -1,4 +0,0 @@
|
||||
-- Make residence_id optional for contractors
|
||||
-- Allows contractors to be personal (no residence) or shared (with residence)
|
||||
|
||||
ALTER TABLE task_contractor ALTER COLUMN residence_id DROP NOT NULL;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Remove is_free column from subscription_usersubscription table
|
||||
ALTER TABLE subscription_usersubscription DROP COLUMN IF EXISTS is_free;
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Add is_free column to subscription_usersubscription table
|
||||
-- When true, user bypasses all limitations regardless of global settings
|
||||
ALTER TABLE subscription_usersubscription ADD COLUMN IF NOT EXISTS is_free BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -1,45 +0,0 @@
|
||||
-- Rollback: Restore status_id foreign key from in_progress boolean
|
||||
|
||||
-- Step 1: Recreate the task_taskstatus table
|
||||
CREATE TABLE IF NOT EXISTS task_taskstatus (
|
||||
id SERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMPTZ,
|
||||
name VARCHAR(20) NOT NULL,
|
||||
description TEXT,
|
||||
color VARCHAR(7),
|
||||
display_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- Step 2: Seed the status lookup data
|
||||
INSERT INTO task_taskstatus (name, description, color, display_order) VALUES
|
||||
('Pending', 'Task is waiting to be started', '#808080', 1),
|
||||
('In Progress', 'Task is currently being worked on', '#3498db', 2),
|
||||
('Completed', 'Task has been finished', '#27ae60', 3),
|
||||
('On Hold', 'Task is temporarily paused', '#f39c12', 4),
|
||||
('Cancelled', 'Task has been cancelled', '#e74c3c', 5)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Step 3: Add status_id column back
|
||||
ALTER TABLE task_task ADD COLUMN IF NOT EXISTS status_id INTEGER;
|
||||
|
||||
-- Step 4: Migrate data - set status_id based on in_progress flag
|
||||
-- Set to "In Progress" status if in_progress is true, otherwise "Pending"
|
||||
UPDATE task_task
|
||||
SET status_id = (
|
||||
CASE
|
||||
WHEN in_progress = true THEN (SELECT id FROM task_taskstatus WHERE name = 'In Progress' LIMIT 1)
|
||||
ELSE (SELECT id FROM task_taskstatus WHERE name = 'Pending' LIMIT 1)
|
||||
END
|
||||
);
|
||||
|
||||
-- Step 5: Add foreign key constraint
|
||||
ALTER TABLE task_task ADD CONSTRAINT fk_task_task_status
|
||||
FOREIGN KEY (status_id) REFERENCES task_taskstatus(id);
|
||||
|
||||
-- Step 6: Drop the in_progress column
|
||||
ALTER TABLE task_task DROP COLUMN IF EXISTS in_progress;
|
||||
|
||||
-- Step 7: Drop the index
|
||||
DROP INDEX IF EXISTS idx_task_task_in_progress;
|
||||
@@ -1,44 +0,0 @@
|
||||
-- Migration: Replace status_id foreign key with in_progress boolean
|
||||
-- This simplifies the task model since status was only used to determine if a task is "In Progress"
|
||||
|
||||
-- Step 1: Add in_progress boolean column with default false
|
||||
ALTER TABLE task_task ADD COLUMN IF NOT EXISTS in_progress BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- Step 2: Create index on in_progress for query performance
|
||||
CREATE INDEX IF NOT EXISTS idx_task_task_in_progress ON task_task(in_progress);
|
||||
|
||||
-- Step 3: Migrate existing data - set in_progress = true for tasks with "In Progress" status
|
||||
UPDATE task_task
|
||||
SET in_progress = true
|
||||
WHERE status_id IN (
|
||||
SELECT id FROM task_taskstatus WHERE LOWER(name) = 'in progress'
|
||||
);
|
||||
|
||||
-- Step 4: Drop the foreign key constraint on status_id (if it exists)
|
||||
-- PostgreSQL syntax - the constraint name might vary
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Try to drop the constraint if it exists
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE constraint_name = 'fk_task_task_status'
|
||||
AND table_name = 'task_task'
|
||||
) THEN
|
||||
ALTER TABLE task_task DROP CONSTRAINT fk_task_task_status;
|
||||
END IF;
|
||||
|
||||
-- Also try the gorm auto-generated constraint name
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE constraint_name = 'task_task_status_id_fkey'
|
||||
AND table_name = 'task_task'
|
||||
) THEN
|
||||
ALTER TABLE task_task DROP CONSTRAINT task_task_status_id_fkey;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Step 5: Drop the status_id column
|
||||
ALTER TABLE task_task DROP COLUMN IF EXISTS status_id;
|
||||
|
||||
-- Step 6: Drop the task_taskstatus table
|
||||
DROP TABLE IF EXISTS task_taskstatus;
|
||||
@@ -1,23 +0,0 @@
|
||||
-- Rollback performance optimization indexes
|
||||
-- Migration: 006_performance_indexes
|
||||
|
||||
DROP INDEX IF EXISTS idx_user_email_lower;
|
||||
DROP INDEX IF EXISTS idx_user_username_lower;
|
||||
DROP INDEX IF EXISTS idx_admin_email_lower;
|
||||
|
||||
DROP INDEX IF EXISTS idx_task_residence_status;
|
||||
DROP INDEX IF EXISTS idx_task_residence_active;
|
||||
DROP INDEX IF EXISTS idx_task_next_due_date;
|
||||
|
||||
DROP INDEX IF EXISTS idx_notification_user_read;
|
||||
DROP INDEX IF EXISTS idx_notification_user_sent;
|
||||
DROP INDEX IF EXISTS idx_notification_task;
|
||||
|
||||
DROP INDEX IF EXISTS idx_document_residence_active_type;
|
||||
DROP INDEX IF EXISTS idx_document_expiry_active;
|
||||
|
||||
DROP INDEX IF EXISTS idx_contractor_created_by;
|
||||
DROP INDEX IF EXISTS idx_completion_task;
|
||||
DROP INDEX IF EXISTS idx_completion_completed_by;
|
||||
DROP INDEX IF EXISTS idx_residence_member_user;
|
||||
DROP INDEX IF EXISTS idx_share_code_active;
|
||||
@@ -1,75 +0,0 @@
|
||||
-- Performance optimization indexes
|
||||
-- Migration: 006_performance_indexes
|
||||
|
||||
-- =====================================================
|
||||
-- CRITICAL: Case-insensitive indexes for auth lookups
|
||||
-- =====================================================
|
||||
-- These eliminate full table scans on login/registration
|
||||
CREATE INDEX IF NOT EXISTS idx_user_email_lower ON auth_user (LOWER(email));
|
||||
CREATE INDEX IF NOT EXISTS idx_user_username_lower ON auth_user (LOWER(username));
|
||||
CREATE INDEX IF NOT EXISTS idx_admin_email_lower ON admin_users (LOWER(email));
|
||||
|
||||
-- =====================================================
|
||||
-- HIGH PRIORITY: Composite indexes for common queries
|
||||
-- =====================================================
|
||||
|
||||
-- Tasks: Most common query pattern is by residence with status filters
|
||||
CREATE INDEX IF NOT EXISTS idx_task_residence_status
|
||||
ON task_task (residence_id, is_cancelled, is_archived);
|
||||
|
||||
-- Tasks: For kanban board queries (active tasks by residence)
|
||||
CREATE INDEX IF NOT EXISTS idx_task_residence_active
|
||||
ON task_task (residence_id, is_archived, in_progress)
|
||||
WHERE is_cancelled = false;
|
||||
|
||||
-- Tasks: For overdue queries (next_due_date lookups)
|
||||
CREATE INDEX IF NOT EXISTS idx_task_next_due_date
|
||||
ON task_task (next_due_date)
|
||||
WHERE is_cancelled = false AND is_archived = false AND next_due_date IS NOT NULL;
|
||||
|
||||
-- Notifications: Queried constantly for unread count
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_user_read
|
||||
ON notifications_notification (user_id, read);
|
||||
|
||||
-- Notifications: For pending notification worker
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_user_sent
|
||||
ON notifications_notification (user_id, sent);
|
||||
|
||||
-- Notifications: Task-based lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_task
|
||||
ON notifications_notification (task_id)
|
||||
WHERE task_id IS NOT NULL;
|
||||
|
||||
-- Documents: Warranty expiry queries
|
||||
CREATE INDEX IF NOT EXISTS idx_document_residence_active_type
|
||||
ON task_document (residence_id, is_active, document_type);
|
||||
|
||||
-- Documents: Expiring warranties lookup
|
||||
CREATE INDEX IF NOT EXISTS idx_document_expiry_active
|
||||
ON task_document (expiry_date, is_active)
|
||||
WHERE document_type = 'warranty' AND is_active = true;
|
||||
|
||||
-- =====================================================
|
||||
-- MEDIUM PRIORITY: Foreign key indexes
|
||||
-- =====================================================
|
||||
|
||||
-- Contractor: Query by creator (user's personal contractors)
|
||||
CREATE INDEX IF NOT EXISTS idx_contractor_created_by
|
||||
ON task_contractor (created_by_id);
|
||||
|
||||
-- Task completions: Query by task
|
||||
CREATE INDEX IF NOT EXISTS idx_completion_task
|
||||
ON task_taskcompletion (task_id);
|
||||
|
||||
-- Task completions: Query by user who completed
|
||||
CREATE INDEX IF NOT EXISTS idx_completion_completed_by
|
||||
ON task_taskcompletion (completed_by_id);
|
||||
|
||||
-- Residence members: Query by user
|
||||
CREATE INDEX IF NOT EXISTS idx_residence_member_user
|
||||
ON residence_residencemember (user_id);
|
||||
|
||||
-- Share codes: Active code lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_share_code_active
|
||||
ON residence_residencesharecode (code, is_active)
|
||||
WHERE is_active = true;
|
||||
@@ -1,5 +0,0 @@
|
||||
-- Rollback custom_interval_days column
|
||||
-- Migration: 007_custom_interval_days
|
||||
|
||||
ALTER TABLE task_task
|
||||
DROP COLUMN IF EXISTS custom_interval_days;
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Add custom_interval_days for custom frequency tasks
|
||||
-- Migration: 007_custom_interval_days
|
||||
|
||||
ALTER TABLE task_task
|
||||
ADD COLUMN IF NOT EXISTS custom_interval_days INTEGER;
|
||||
|
||||
-- Add comment for documentation
|
||||
COMMENT ON COLUMN task_task.custom_interval_days IS 'For Custom frequency tasks, the user-specified number of days between occurrences';
|
||||
@@ -1,8 +0,0 @@
|
||||
-- Rollback additional performance optimization indexes
|
||||
-- Migration: 008_additional_performance_indexes
|
||||
|
||||
DROP INDEX IF EXISTS idx_task_kanban_composite;
|
||||
DROP INDEX IF EXISTS idx_completion_task_date;
|
||||
DROP INDEX IF EXISTS idx_sharecode_code_active;
|
||||
DROP INDEX IF EXISTS idx_residence_users_user_residence;
|
||||
DROP INDEX IF EXISTS idx_task_in_progress;
|
||||
@@ -1,47 +0,0 @@
|
||||
-- Additional performance optimization indexes
|
||||
-- Migration: 008_additional_performance_indexes
|
||||
|
||||
-- =====================================================
|
||||
-- KANBAN QUERY OPTIMIZATION
|
||||
-- =====================================================
|
||||
|
||||
-- Composite index for kanban board queries
|
||||
-- Covers: WHERE residence_id IN ? AND is_archived = false
|
||||
-- with ordering by due_date, next_due_date
|
||||
CREATE INDEX IF NOT EXISTS idx_task_kanban_composite
|
||||
ON task_task (residence_id, is_archived, is_cancelled, next_due_date, due_date)
|
||||
WHERE is_archived = false;
|
||||
|
||||
-- =====================================================
|
||||
-- COMPLETION QUERY OPTIMIZATION
|
||||
-- =====================================================
|
||||
|
||||
-- Ordering index for completion queries (most recent first)
|
||||
CREATE INDEX IF NOT EXISTS idx_completion_task_date
|
||||
ON task_taskcompletion (task_id, completed_at DESC);
|
||||
|
||||
-- =====================================================
|
||||
-- SHARE CODE OPTIMIZATION
|
||||
-- =====================================================
|
||||
|
||||
-- Unique index for active share code lookups
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_sharecode_code_active
|
||||
ON residence_residencesharecode (code)
|
||||
WHERE is_active = true;
|
||||
|
||||
-- =====================================================
|
||||
-- RESIDENCE USER ACCESS OPTIMIZATION
|
||||
-- =====================================================
|
||||
|
||||
-- Index for residence user membership queries (used by FindResidenceIDsByUser)
|
||||
CREATE INDEX IF NOT EXISTS idx_residence_users_user_residence
|
||||
ON residence_residence_users (user_id, residence_id);
|
||||
|
||||
-- =====================================================
|
||||
-- TASK IN_PROGRESS QUERIES
|
||||
-- =====================================================
|
||||
|
||||
-- Index for in_progress task queries (kanban "In Progress" column)
|
||||
CREATE INDEX IF NOT EXISTS idx_task_in_progress
|
||||
ON task_task (residence_id, in_progress)
|
||||
WHERE in_progress = true AND is_cancelled = false AND is_archived = false;
|
||||
@@ -1,10 +0,0 @@
|
||||
-- Rollback: Recreate removed indexes
|
||||
|
||||
-- Recreate share code index (from migration 006)
|
||||
CREATE INDEX IF NOT EXISTS idx_share_code_active
|
||||
ON residence_residencesharecode (residence_id)
|
||||
WHERE is_active = true;
|
||||
|
||||
-- Recreate notification user+sent index (from migration 006)
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_user_sent
|
||||
ON notifications_notification (user_id, sent);
|
||||
@@ -1,15 +0,0 @@
|
||||
-- Migration: 009_remove_redundant_indexes
|
||||
-- Description: Remove indexes that are redundant or unused
|
||||
|
||||
-- Remove redundant share code index
|
||||
-- idx_share_code_active is superseded by unique idx_sharecode_code_active (migration 008)
|
||||
-- Both filter WHERE is_active = true, but 008's unique index on (code) is more restrictive
|
||||
DROP INDEX IF EXISTS idx_share_code_active;
|
||||
|
||||
-- Remove unused composite notification index
|
||||
-- idx_notification_user_sent on (user_id, sent) is never used:
|
||||
-- - GetPendingNotifications() only filters on sent, not user_id
|
||||
-- - FindByUser() only filters on user_id, not sent (uses idx_notification_user_created_at)
|
||||
-- - No queries combine user_id AND sent together
|
||||
-- The leading column (user_id) queries already use idx_notification_user_created_at
|
||||
DROP INDEX IF EXISTS idx_notification_user_sent;
|
||||
@@ -1,5 +0,0 @@
|
||||
-- Rollback: Smart Notification Reminder System
|
||||
|
||||
DROP INDEX IF EXISTS idx_reminderlog_sent_at;
|
||||
DROP INDEX IF EXISTS idx_reminderlog_task_user_date;
|
||||
DROP TABLE IF EXISTS task_reminderlog;
|
||||
@@ -1,24 +0,0 @@
|
||||
-- Smart Notification Reminder System
|
||||
-- Tracks which reminders have been sent to prevent duplicates
|
||||
|
||||
CREATE TABLE task_reminderlog (
|
||||
id SERIAL PRIMARY KEY,
|
||||
task_id INTEGER NOT NULL REFERENCES task_task(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE,
|
||||
due_date DATE NOT NULL, -- Which occurrence this is for
|
||||
reminder_stage VARCHAR(20) NOT NULL, -- e.g., "reminder_30d", "reminder_7d", "day_of", "overdue_1", "overdue_4"
|
||||
sent_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
notification_id INTEGER REFERENCES notifications_notification(id) ON DELETE SET NULL,
|
||||
|
||||
-- Prevent duplicate reminders for same task/user/date/stage
|
||||
UNIQUE(task_id, user_id, due_date, reminder_stage)
|
||||
);
|
||||
|
||||
-- Index for quick lookup when checking if reminder was already sent
|
||||
CREATE INDEX idx_reminderlog_task_user_date ON task_reminderlog(task_id, user_id, due_date);
|
||||
|
||||
-- Index for cleanup job (delete old logs)
|
||||
CREATE INDEX idx_reminderlog_sent_at ON task_reminderlog(sent_at);
|
||||
|
||||
COMMENT ON TABLE task_reminderlog IS 'Tracks sent task reminders to prevent duplicate notifications';
|
||||
COMMENT ON COLUMN task_reminderlog.reminder_stage IS 'Stage of reminder: reminder_30d, reminder_14d, reminder_7d, reminder_3d, reminder_1d, day_of, overdue_N';
|
||||
@@ -1,3 +0,0 @@
|
||||
-- Remove timezone column from notification preferences
|
||||
ALTER TABLE notifications_notificationpreference
|
||||
DROP COLUMN IF EXISTS timezone;
|
||||
@@ -1,7 +0,0 @@
|
||||
-- Add timezone column to notification preferences
|
||||
-- Stores IANA timezone name (e.g., "America/Los_Angeles") for background job calculations
|
||||
ALTER TABLE notifications_notificationpreference
|
||||
ADD COLUMN timezone VARCHAR(50);
|
||||
|
||||
-- Add comment for documentation
|
||||
COMMENT ON COLUMN notifications_notificationpreference.timezone IS 'IANA timezone name for daily digest calculations (e.g., America/Los_Angeles)';
|
||||
@@ -1 +0,0 @@
|
||||
DROP TABLE IF EXISTS webhook_event_log;
|
||||
@@ -1,9 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS webhook_event_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
event_id VARCHAR(255) NOT NULL,
|
||||
provider VARCHAR(20) NOT NULL,
|
||||
event_type VARCHAR(100) NOT NULL,
|
||||
processed_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
payload_hash VARCHAR(64),
|
||||
UNIQUE(provider, event_id)
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
ALTER TABLE task_task DROP CONSTRAINT IF EXISTS chk_task_not_cancelled_and_archived;
|
||||
ALTER TABLE subscriptions_usersubscription DROP CONSTRAINT IF EXISTS chk_subscription_tier;
|
||||
ALTER TABLE notifications_notification DROP CONSTRAINT IF EXISTS chk_notification_sent_consistency;
|
||||
ALTER TABLE subscriptions_usersubscription DROP CONSTRAINT IF EXISTS uq_subscription_user;
|
||||
ALTER TABLE notifications_notificationpreference DROP CONSTRAINT IF EXISTS uq_notif_pref_user;
|
||||
@@ -1,31 +0,0 @@
|
||||
-- Prevent task from being both cancelled and archived simultaneously
|
||||
ALTER TABLE task_task ADD CONSTRAINT chk_task_not_cancelled_and_archived
|
||||
CHECK (NOT (is_cancelled = true AND is_archived = true));
|
||||
|
||||
-- Subscription tier must be valid
|
||||
ALTER TABLE subscriptions_usersubscription ADD CONSTRAINT chk_subscription_tier
|
||||
CHECK (tier IN ('free', 'pro'));
|
||||
|
||||
-- Notification: sent_at must be set if sent is true
|
||||
ALTER TABLE notifications_notification ADD CONSTRAINT chk_notification_sent_consistency
|
||||
CHECK ((sent = false) OR (sent = true AND sent_at IS NOT NULL));
|
||||
|
||||
-- One subscription per user
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'uq_subscription_user'
|
||||
) THEN
|
||||
ALTER TABLE subscriptions_usersubscription ADD CONSTRAINT uq_subscription_user UNIQUE (user_id);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- One notification preference per user
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'uq_notif_pref_user'
|
||||
) THEN
|
||||
ALTER TABLE notifications_notificationpreference ADD CONSTRAINT uq_notif_pref_user UNIQUE (user_id);
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE task_task DROP COLUMN IF EXISTS version;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE task_task ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1;
|
||||
@@ -1,3 +0,0 @@
|
||||
DROP INDEX IF EXISTS idx_task_kanban_query;
|
||||
DROP INDEX IF EXISTS idx_notification_user_unread;
|
||||
DROP INDEX IF EXISTS idx_document_residence_active;
|
||||
@@ -1,14 +0,0 @@
|
||||
-- Kanban: composite index for active task queries by residence with due date ordering
|
||||
CREATE INDEX IF NOT EXISTS idx_task_kanban_query
|
||||
ON task_task (residence_id, is_cancelled, is_archived, next_due_date, due_date)
|
||||
WHERE is_cancelled = false AND is_archived = false;
|
||||
|
||||
-- Notifications: index for unread count (hot query)
|
||||
CREATE INDEX IF NOT EXISTS idx_notification_user_unread
|
||||
ON notifications_notification (user_id, read)
|
||||
WHERE read = false;
|
||||
|
||||
-- Documents: residence + active filter
|
||||
CREATE INDEX IF NOT EXISTS idx_document_residence_active
|
||||
ON documents_document (residence_id, is_active)
|
||||
WHERE is_active = true;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- No-op: the created column is part of Django's original schema and should not
|
||||
-- be removed.
|
||||
@@ -1,6 +0,0 @@
|
||||
-- Ensure created column exists on user_authtoken (Django already creates it,
|
||||
-- but this migration guarantees it for fresh Go-only deployments).
|
||||
ALTER TABLE user_authtoken ADD COLUMN IF NOT EXISTS created TIMESTAMP WITH TIME ZONE DEFAULT NOW();
|
||||
|
||||
-- Backfill any rows that may have a NULL created timestamp.
|
||||
UPDATE user_authtoken SET created = NOW() WHERE created IS NULL;
|
||||
@@ -1,40 +0,0 @@
|
||||
-- Rollback: 017_fk_indexes
|
||||
-- Drop all FK indexes added in the up migration.
|
||||
|
||||
-- auth / user tables
|
||||
DROP INDEX IF EXISTS idx_authtoken_user_id;
|
||||
DROP INDEX IF EXISTS idx_userprofile_user_id;
|
||||
DROP INDEX IF EXISTS idx_confirmationcode_user_id;
|
||||
DROP INDEX IF EXISTS idx_passwordresetcode_user_id;
|
||||
DROP INDEX IF EXISTS idx_applesocialauth_user_id;
|
||||
DROP INDEX IF EXISTS idx_googlesocialauth_user_id;
|
||||
|
||||
-- push notification device tables
|
||||
DROP INDEX IF EXISTS idx_apnsdevice_user_id;
|
||||
DROP INDEX IF EXISTS idx_gcmdevice_user_id;
|
||||
|
||||
-- notification tables
|
||||
DROP INDEX IF EXISTS idx_notificationpreference_user_id;
|
||||
|
||||
-- subscription tables
|
||||
DROP INDEX IF EXISTS idx_subscription_user_id;
|
||||
|
||||
-- residence tables
|
||||
DROP INDEX IF EXISTS idx_residence_owner_id;
|
||||
DROP INDEX IF EXISTS idx_sharecode_residence_id;
|
||||
DROP INDEX IF EXISTS idx_sharecode_created_by_id;
|
||||
|
||||
-- task tables
|
||||
DROP INDEX IF EXISTS idx_task_created_by_id;
|
||||
DROP INDEX IF EXISTS idx_task_assigned_to_id;
|
||||
DROP INDEX IF EXISTS idx_task_category_id;
|
||||
DROP INDEX IF EXISTS idx_task_priority_id;
|
||||
DROP INDEX IF EXISTS idx_task_frequency_id;
|
||||
DROP INDEX IF EXISTS idx_task_contractor_id;
|
||||
DROP INDEX IF EXISTS idx_task_parent_task_id;
|
||||
DROP INDEX IF EXISTS idx_completionimage_completion_id;
|
||||
DROP INDEX IF EXISTS idx_document_created_by_id;
|
||||
DROP INDEX IF EXISTS idx_document_task_id;
|
||||
DROP INDEX IF EXISTS idx_documentimage_document_id;
|
||||
DROP INDEX IF EXISTS idx_contractor_residence_id;
|
||||
DROP INDEX IF EXISTS idx_reminderlog_notification_id;
|
||||
@@ -1,131 +0,0 @@
|
||||
-- Migration: 017_fk_indexes
|
||||
-- Add indexes on all foreign key columns that are not already covered by existing indexes.
|
||||
-- Uses CREATE INDEX IF NOT EXISTS to be idempotent (safe to re-run).
|
||||
|
||||
-- =====================================================
|
||||
-- auth / user tables
|
||||
-- =====================================================
|
||||
|
||||
-- user_authtoken: user_id (unique FK, but ensure index exists)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_authtoken_user_id
|
||||
ON user_authtoken (user_id);
|
||||
|
||||
-- user_userprofile: user_id (unique FK)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_userprofile_user_id
|
||||
ON user_userprofile (user_id);
|
||||
|
||||
-- user_confirmationcode: user_id
|
||||
CREATE INDEX IF NOT EXISTS idx_confirmationcode_user_id
|
||||
ON user_confirmationcode (user_id);
|
||||
|
||||
-- user_passwordresetcode: user_id
|
||||
CREATE INDEX IF NOT EXISTS idx_passwordresetcode_user_id
|
||||
ON user_passwordresetcode (user_id);
|
||||
|
||||
-- user_applesocialauth: user_id (unique FK)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_applesocialauth_user_id
|
||||
ON user_applesocialauth (user_id);
|
||||
|
||||
-- user_googlesocialauth: user_id (unique FK)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_googlesocialauth_user_id
|
||||
ON user_googlesocialauth (user_id);
|
||||
|
||||
-- =====================================================
|
||||
-- push notification device tables
|
||||
-- =====================================================
|
||||
|
||||
-- push_notifications_apnsdevice: user_id
|
||||
CREATE INDEX IF NOT EXISTS idx_apnsdevice_user_id
|
||||
ON push_notifications_apnsdevice (user_id);
|
||||
|
||||
-- push_notifications_gcmdevice: user_id
|
||||
CREATE INDEX IF NOT EXISTS idx_gcmdevice_user_id
|
||||
ON push_notifications_gcmdevice (user_id);
|
||||
|
||||
-- =====================================================
|
||||
-- notification tables
|
||||
-- =====================================================
|
||||
|
||||
-- notifications_notificationpreference: user_id (unique FK)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_notificationpreference_user_id
|
||||
ON notifications_notificationpreference (user_id);
|
||||
|
||||
-- =====================================================
|
||||
-- subscription tables
|
||||
-- =====================================================
|
||||
|
||||
-- subscription_usersubscription: user_id (unique FK)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_subscription_user_id
|
||||
ON subscription_usersubscription (user_id);
|
||||
|
||||
-- =====================================================
|
||||
-- residence tables
|
||||
-- =====================================================
|
||||
|
||||
-- residence_residence: owner_id
|
||||
CREATE INDEX IF NOT EXISTS idx_residence_owner_id
|
||||
ON residence_residence (owner_id);
|
||||
|
||||
-- residence_residencesharecode: residence_id (may already exist from model index tag via GORM)
|
||||
CREATE INDEX IF NOT EXISTS idx_sharecode_residence_id
|
||||
ON residence_residencesharecode (residence_id);
|
||||
|
||||
-- residence_residencesharecode: created_by_id
|
||||
CREATE INDEX IF NOT EXISTS idx_sharecode_created_by_id
|
||||
ON residence_residencesharecode (created_by_id);
|
||||
|
||||
-- =====================================================
|
||||
-- task tables
|
||||
-- =====================================================
|
||||
|
||||
-- task_task: created_by_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_created_by_id
|
||||
ON task_task (created_by_id);
|
||||
|
||||
-- task_task: assigned_to_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_assigned_to_id
|
||||
ON task_task (assigned_to_id);
|
||||
|
||||
-- task_task: category_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_category_id
|
||||
ON task_task (category_id);
|
||||
|
||||
-- task_task: priority_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_priority_id
|
||||
ON task_task (priority_id);
|
||||
|
||||
-- task_task: frequency_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_frequency_id
|
||||
ON task_task (frequency_id);
|
||||
|
||||
-- task_task: contractor_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_contractor_id
|
||||
ON task_task (contractor_id);
|
||||
|
||||
-- task_task: parent_task_id
|
||||
CREATE INDEX IF NOT EXISTS idx_task_parent_task_id
|
||||
ON task_task (parent_task_id);
|
||||
|
||||
-- task_taskcompletionimage: completion_id
|
||||
CREATE INDEX IF NOT EXISTS idx_completionimage_completion_id
|
||||
ON task_taskcompletionimage (completion_id);
|
||||
|
||||
-- task_document: created_by_id
|
||||
CREATE INDEX IF NOT EXISTS idx_document_created_by_id
|
||||
ON task_document (created_by_id);
|
||||
|
||||
-- task_document: task_id
|
||||
CREATE INDEX IF NOT EXISTS idx_document_task_id
|
||||
ON task_document (task_id);
|
||||
|
||||
-- task_documentimage: document_id
|
||||
CREATE INDEX IF NOT EXISTS idx_documentimage_document_id
|
||||
ON task_documentimage (document_id);
|
||||
|
||||
-- task_contractor: residence_id
|
||||
CREATE INDEX IF NOT EXISTS idx_contractor_residence_id
|
||||
ON task_contractor (residence_id);
|
||||
|
||||
-- task_reminderlog: notification_id
|
||||
CREATE INDEX IF NOT EXISTS idx_reminderlog_notification_id
|
||||
ON task_reminderlog (notification_id);
|
||||
@@ -1,2 +0,0 @@
|
||||
-- Rollback: 018_audit_log
|
||||
DROP TABLE IF EXISTS audit_log;
|
||||
@@ -1,16 +0,0 @@
|
||||
-- Migration: 018_audit_log
|
||||
-- Create audit_log table for tracking security-relevant events (login, register, etc.)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER,
|
||||
event_type VARCHAR(50) NOT NULL,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
details JSONB,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_log_user_id ON audit_log(user_id);
|
||||
CREATE INDEX idx_audit_log_event_type ON audit_log(event_type);
|
||||
CREATE INDEX idx_audit_log_created_at ON audit_log(created_at);
|
||||
@@ -1,18 +0,0 @@
|
||||
-- Migration: 019_residence_home_profile (rollback)
|
||||
-- Remove home profile fields from residence
|
||||
|
||||
ALTER TABLE residence_residence
|
||||
DROP COLUMN IF EXISTS heating_type,
|
||||
DROP COLUMN IF EXISTS cooling_type,
|
||||
DROP COLUMN IF EXISTS water_heater_type,
|
||||
DROP COLUMN IF EXISTS roof_type,
|
||||
DROP COLUMN IF EXISTS has_pool,
|
||||
DROP COLUMN IF EXISTS has_sprinkler_system,
|
||||
DROP COLUMN IF EXISTS has_septic,
|
||||
DROP COLUMN IF EXISTS has_fireplace,
|
||||
DROP COLUMN IF EXISTS has_garage,
|
||||
DROP COLUMN IF EXISTS has_basement,
|
||||
DROP COLUMN IF EXISTS has_attic,
|
||||
DROP COLUMN IF EXISTS exterior_type,
|
||||
DROP COLUMN IF EXISTS flooring_primary,
|
||||
DROP COLUMN IF EXISTS landscaping_type;
|
||||
@@ -1,18 +0,0 @@
|
||||
-- Migration: 019_residence_home_profile
|
||||
-- Add home profile fields to residence for smart onboarding task suggestions
|
||||
|
||||
ALTER TABLE residence_residence
|
||||
ADD COLUMN IF NOT EXISTS heating_type VARCHAR(50),
|
||||
ADD COLUMN IF NOT EXISTS cooling_type VARCHAR(50),
|
||||
ADD COLUMN IF NOT EXISTS water_heater_type VARCHAR(50),
|
||||
ADD COLUMN IF NOT EXISTS roof_type VARCHAR(50),
|
||||
ADD COLUMN IF NOT EXISTS has_pool BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS has_sprinkler_system BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS has_septic BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS has_fireplace BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS has_garage BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS has_basement BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS has_attic BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS exterior_type VARCHAR(50),
|
||||
ADD COLUMN IF NOT EXISTS flooring_primary VARCHAR(50),
|
||||
ADD COLUMN IF NOT EXISTS landscaping_type VARCHAR(50);
|
||||
@@ -1,4 +0,0 @@
|
||||
-- Migration: 020_template_conditions (rollback)
|
||||
-- Remove conditions column from task templates
|
||||
|
||||
ALTER TABLE task_tasktemplate DROP COLUMN IF EXISTS conditions;
|
||||
@@ -1,4 +0,0 @@
|
||||
-- Migration: 020_template_conditions
|
||||
-- Add conditions column to task templates for residence-aware suggestions
|
||||
|
||||
ALTER TABLE task_tasktemplate ADD COLUMN IF NOT EXISTS conditions JSONB DEFAULT '{}';
|
||||
@@ -1,2 +0,0 @@
|
||||
DROP INDEX IF EXISTS idx_task_task_task_template_id;
|
||||
ALTER TABLE task_task DROP COLUMN IF EXISTS task_template_id;
|
||||
@@ -1,13 +0,0 @@
|
||||
-- Add a backlink from task_task to task_tasktemplate so that tasks created from
|
||||
-- a template (e.g. onboarding suggestions or the template catalog) can be
|
||||
-- reported on and filtered. Nullable — user-created custom tasks remain unset.
|
||||
|
||||
ALTER TABLE task_task
|
||||
ADD COLUMN IF NOT EXISTS task_template_id BIGINT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_task_task_task_template_id
|
||||
ON task_task (task_template_id);
|
||||
|
||||
-- Deferred FK — not enforced at the DB level because task_tasktemplate rows
|
||||
-- may be renamed/retired; application code is the source of truth for the
|
||||
-- relationship and already tolerates nil.
|
||||
@@ -1,12 +0,0 @@
|
||||
-- Recreates the legacy task_tasktemplate_regions join table. Data is not
|
||||
-- restored — if a rollback needs the prior associations they have to be
|
||||
-- reseeded from the task template conditions JSON.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS task_tasktemplate_regions (
|
||||
task_template_id BIGINT NOT NULL,
|
||||
climate_region_id BIGINT NOT NULL,
|
||||
PRIMARY KEY (task_template_id, climate_region_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_task_tasktemplate_regions_region
|
||||
ON task_tasktemplate_regions (climate_region_id);
|
||||
@@ -1,5 +0,0 @@
|
||||
-- Drop the legacy many-to-many join table task_tasktemplate_regions.
|
||||
-- Climate-region affinity now lives in task_tasktemplate.conditions->'climate_region_id'
|
||||
-- and is scored by SuggestionService alongside the other home-profile conditions.
|
||||
|
||||
DROP TABLE IF EXISTS task_tasktemplate_regions;
|
||||
Reference in New Issue
Block a user