Files
fonrey/apps/property/migrations/0002_partitions_and_triggers.py

137 lines
5.3 KiB
Python

from django.db import migrations
CREATE_FOLLOW_LOGS = """
CREATE TABLE follow_logs (
id UUID NOT NULL DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
log_type VARCHAR(30) NOT NULL
CHECK (log_type IN ('written','modified','sensitive_op',
'sensitive_view','other','system')),
purpose VARCHAR(50),
content TEXT,
ai_tag VARCHAR(20)
CHECK (ai_tag IS NULL OR ai_tag IN ('ai_for_sale','ai_not_for_sale')),
change_detail JSONB,
log_tag VARCHAR(50),
is_public BOOLEAN NOT NULL DEFAULT TRUE,
operator_id UUID REFERENCES staff(id) ON DELETE SET NULL,
operator_snapshot JSONB,
is_deletable BOOLEAN NOT NULL DEFAULT TRUE,
deleted_at TIMESTAMPTZ,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
CREATE TABLE follow_logs_2026_04 PARTITION OF follow_logs
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE follow_logs_2026_05 PARTITION OF follow_logs
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE follow_logs_default PARTITION OF follow_logs DEFAULT;
CREATE INDEX idx_follow_logs_property_time ON follow_logs(property_id, created_at DESC)
WHERE deleted_at IS NULL;
CREATE INDEX idx_follow_logs_type ON follow_logs(property_id, log_type, created_at DESC)
WHERE deleted_at IS NULL;
CREATE INDEX idx_follow_logs_operator ON follow_logs(operator_id, created_at DESC)
WHERE deleted_at IS NULL;
CREATE INDEX idx_follow_logs_sensitive ON follow_logs(property_id, created_at DESC)
WHERE log_type IN ('sensitive_view','sensitive_op');
"""
DROP_FOLLOW_LOGS = "DROP TABLE IF EXISTS follow_logs CASCADE;"
CREATE_PROPERTY_PHOTOS = """
CREATE TABLE property_photos (
id UUID NOT NULL DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
category VARCHAR(20) NOT NULL
CHECK (category IN ('cover','entrance','living_room',
'dining_room','bedroom','bathroom',
'kitchen','balcony','study',
'indoor_other','outdoor','panorama')),
file_key TEXT NOT NULL,
thumbnail_key TEXT,
file_name VARCHAR(255),
file_size INTEGER,
width INTEGER,
height INTEGER,
is_cover BOOLEAN NOT NULL DEFAULT FALSE,
sort_order SMALLINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by_id UUID REFERENCES staff(id) ON DELETE SET NULL,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
CREATE TABLE property_photos_2026_04 PARTITION OF property_photos
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
CREATE TABLE property_photos_2026_05 PARTITION OF property_photos
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE property_photos_default PARTITION OF property_photos DEFAULT;
CREATE INDEX idx_property_photos_property ON property_photos(property_id);
CREATE INDEX idx_property_photos_cover ON property_photos(property_id)
WHERE is_cover = TRUE;
CREATE INDEX idx_property_photos_category ON property_photos(property_id, category);
CREATE UNIQUE INDEX idx_property_photos_unique_cover
ON property_photos(property_id)
WHERE is_cover = TRUE;
"""
DROP_PROPERTY_PHOTOS = "DROP TABLE IF EXISTS property_photos CASCADE;"
CREATE_TRIGGERS = """
CREATE OR REPLACE FUNCTION update_property_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('simple', COALESCE(NEW.block_no, '') ||
' ' || COALESCE(NEW.unit_no, '') ||
' ' || COALESCE(NEW.room_no, '')), 'A') ||
setweight(to_tsvector('simple', COALESCE(NEW.remarks, '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_property_search_vector
BEFORE INSERT OR UPDATE OF block_no, unit_no, room_no, remarks
ON properties
FOR EACH ROW EXECUTE FUNCTION update_property_search_vector();
CREATE OR REPLACE FUNCTION update_property_last_followed()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.log_type = 'written' THEN
UPDATE properties
SET last_followed_at = NEW.created_at,
updated_at = NOW()
WHERE id = NEW.property_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_update_last_followed
AFTER INSERT ON follow_logs
FOR EACH ROW EXECUTE FUNCTION update_property_last_followed();
"""
DROP_TRIGGERS = """
DROP TRIGGER IF EXISTS trg_update_last_followed ON follow_logs;
DROP FUNCTION IF EXISTS update_property_last_followed();
DROP TRIGGER IF EXISTS trg_property_search_vector ON properties;
DROP FUNCTION IF EXISTS update_property_search_vector();
"""
class Migration(migrations.Migration):
dependencies = [
("fonrey_property", "0001_initial"),
]
operations = [
migrations.RunSQL(CREATE_FOLLOW_LOGS, reverse_sql=DROP_FOLLOW_LOGS),
migrations.RunSQL(CREATE_PROPERTY_PHOTOS, reverse_sql=DROP_PROPERTY_PHOTOS),
migrations.RunSQL(CREATE_TRIGGERS, reverse_sql=DROP_TRIGGERS),
]