Files
fonrey/apps/client/migrations/0002_partitions_and_triggers.py
Sisyphus ed40de4050 feat(client,setting): complete Phase 2 with partitioned client_follow_logs
- apps/client (11 models): Client, ClientContact, ClientRequirement,
  ClientSchoolPreference, ClientFollowLog (partitioned),
  ClientFollowLogAttachment, ClientViewing, ClientPropertyMatch,
  ClientStatusLog, ClientFavoriteFolder, ClientFolderItem
- apps/client/0002 RunSQL: PARTITION BY RANGE(created_at) for
  client_follow_logs + monthly partitions + default; triggers
  update_client_last_follow + update_client_viewing_progress;
  partial unique index on client_no WHERE deleted_at IS NULL
- apps/setting (4 models): LookupGroup, LookupItem, TenantSetting,
  FieldRequirementRule (tenant schema only per spec)

manage.py check green; all 9 Phase 2 apps complete.
2026-04-29 17:33:58 +08:00

100 lines
3.4 KiB
Python

from django.db import migrations
CREATE_CLIENT_FOLLOW_LOGS = """
CREATE TABLE client_follow_logs (
id UUID NOT NULL DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
log_type VARCHAR(30) NOT NULL
CHECK (log_type IN ('written','modified','sensitive_view',
'other','system')),
purpose VARCHAR(50),
content TEXT,
log_tag VARCHAR(50),
change_detail JSONB,
is_public BOOLEAN NOT NULL DEFAULT TRUE,
is_deletable BOOLEAN NOT NULL DEFAULT TRUE,
operator_id UUID REFERENCES staff(id) ON DELETE SET NULL,
operator_snapshot JSONB,
deleted_at TIMESTAMPTZ,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
CREATE TABLE client_follow_logs_2026_04 PARTITION OF client_follow_logs
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE client_follow_logs_2026_05 PARTITION OF client_follow_logs
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE client_follow_logs_default PARTITION OF client_follow_logs DEFAULT;
CREATE INDEX idx_cfl_client_time ON client_follow_logs(client_id, created_at DESC)
WHERE deleted_at IS NULL;
CREATE INDEX idx_cfl_type ON client_follow_logs(client_id, log_type, created_at DESC)
WHERE deleted_at IS NULL;
CREATE INDEX idx_cfl_operator ON client_follow_logs(operator_id, created_at DESC)
WHERE deleted_at IS NULL;
CREATE INDEX idx_cfl_sensitive ON client_follow_logs(client_id, created_at DESC)
WHERE log_type = 'sensitive_view';
"""
DROP_CLIENT_FOLLOW_LOGS = "DROP TABLE IF EXISTS client_follow_logs CASCADE;"
CREATE_TRIGGERS = """
CREATE OR REPLACE FUNCTION update_client_last_follow()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.log_type = 'written' THEN
UPDATE clients
SET last_follow_at = NEW.created_at,
last_active_at = NEW.created_at,
updated_at = NOW()
WHERE id = NEW.client_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_client_last_follow
AFTER INSERT ON client_follow_logs
FOR EACH ROW EXECUTE FUNCTION update_client_last_follow();
CREATE OR REPLACE FUNCTION update_client_viewing_progress()
RETURNS TRIGGER AS $$
BEGIN
UPDATE clients
SET updated_at = NOW()
WHERE id = NEW.client_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_client_viewing_progress
AFTER INSERT ON client_viewings
FOR EACH ROW EXECUTE FUNCTION update_client_viewing_progress();
"""
DROP_TRIGGERS = """
DROP TRIGGER IF EXISTS trg_client_viewing_progress ON client_viewings;
DROP FUNCTION IF EXISTS update_client_viewing_progress();
DROP TRIGGER IF EXISTS trg_client_last_follow ON client_follow_logs;
DROP FUNCTION IF EXISTS update_client_last_follow();
"""
CREATE_UNIQUE_CLIENT_NO = """
CREATE UNIQUE INDEX idx_clients_client_no_active ON clients(client_no)
WHERE deleted_at IS NULL;
"""
DROP_UNIQUE_CLIENT_NO = "DROP INDEX IF EXISTS idx_clients_client_no_active;"
class Migration(migrations.Migration):
dependencies = [
("fonrey_client", "0001_initial"),
]
operations = [
migrations.RunSQL(CREATE_CLIENT_FOLLOW_LOGS, reverse_sql=DROP_CLIENT_FOLLOW_LOGS),
migrations.RunSQL(CREATE_TRIGGERS, reverse_sql=DROP_TRIGGERS),
migrations.RunSQL(CREATE_UNIQUE_CLIENT_NO, reverse_sql=DROP_UNIQUE_CLIENT_NO),
]