feat(property): add 23-table property module with partitioned follow_logs and property_photos
This commit is contained in:
136
apps/property/migrations/0002_partitions_and_triggers.py
Normal file
136
apps/property/migrations/0002_partitions_and_triggers.py
Normal file
@@ -0,0 +1,136 @@
|
||||
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),
|
||||
]
|
||||
Reference in New Issue
Block a user