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.
This commit is contained in:
99
apps/client/migrations/0002_partitions_and_triggers.py
Normal file
99
apps/client/migrations/0002_partitions_and_triggers.py
Normal file
@@ -0,0 +1,99 @@
|
||||
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),
|
||||
]
|
||||
Reference in New Issue
Block a user