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), ]