137 lines
5.3 KiB
Python
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),
|
|
]
|