feat: stabilize dev sync and add history tables

This commit is contained in:
hyunho
2026-03-30 09:15:31 +09:00
parent bc60f932c3
commit cbae8769bf
6 changed files with 339 additions and 37 deletions

View File

@@ -281,6 +281,66 @@ CREATE TABLE IF NOT EXISTS integration_vouchers (
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS history_revisions (
id BIGSERIAL PRIMARY KEY,
scope TEXT NOT NULL DEFAULT 'organization',
revision_label TEXT NOT NULL,
created_by_user_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
note TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS member_versions (
id BIGSERIAL PRIMARY KEY,
member_id INTEGER NOT NULL REFERENCES members(id) ON DELETE CASCADE,
name TEXT NOT NULL,
company TEXT NOT NULL DEFAULT '',
rank TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT '',
department TEXT NOT NULL DEFAULT '',
grp TEXT NOT NULL DEFAULT '',
division TEXT NOT NULL DEFAULT '',
team TEXT NOT NULL DEFAULT '',
cell TEXT NOT NULL DEFAULT '',
work_status TEXT NOT NULL DEFAULT '',
work_time TEXT NOT NULL DEFAULT '',
phone TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL DEFAULT '',
photo_url TEXT NOT NULL DEFAULT '',
valid_from TIMESTAMPTZ NOT NULL,
valid_to TIMESTAMPTZ,
revision_no BIGINT NOT NULL,
changed_by_user_id BIGINT,
change_reason TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS seat_assignment_versions (
id BIGSERIAL PRIMARY KEY,
member_id INTEGER NOT NULL REFERENCES members(id) ON DELETE CASCADE,
seat_map_id INTEGER REFERENCES seat_maps(id) ON DELETE CASCADE,
seat_slot_id INTEGER REFERENCES seat_slots(id) ON DELETE CASCADE,
seat_label TEXT NOT NULL DEFAULT '',
valid_from TIMESTAMPTZ NOT NULL,
valid_to TIMESTAMPTZ,
revision_no BIGINT NOT NULL,
changed_by_user_id BIGINT,
change_reason TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS entity_change_events (
id BIGSERIAL PRIMARY KEY,
entity_type TEXT NOT NULL,
entity_id BIGINT NOT NULL,
action_type TEXT NOT NULL,
revision_no BIGINT NOT NULL,
changed_by_user_id BIGINT,
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
change_reason TEXT NOT NULL DEFAULT '',
patch_json JSONB NOT NULL DEFAULT '{}'::jsonb
);
CREATE SCHEMA IF NOT EXISTS auth;
CREATE TABLE IF NOT EXISTS auth.users (
@@ -474,6 +534,18 @@ ON integration_vouchers (project_code, project_name);
CREATE UNIQUE INDEX IF NOT EXISTS integration_project_category_mappings_key_idx
ON integration_project_category_mappings (source_key, normalized_project_key);
CREATE INDEX IF NOT EXISTS member_versions_member_time_idx
ON member_versions (member_id, valid_from, valid_to);
CREATE INDEX IF NOT EXISTS seat_assignment_versions_member_time_idx
ON seat_assignment_versions (member_id, valid_from, valid_to);
CREATE INDEX IF NOT EXISTS history_revisions_scope_created_idx
ON history_revisions (scope, created_at DESC);
CREATE INDEX IF NOT EXISTS entity_change_events_entity_idx
ON entity_change_events (entity_type, entity_id, changed_at DESC);
DO $$
BEGIN
IF NOT EXISTS (
@@ -556,6 +628,7 @@ def init_db(max_retries: int = 20, retry_delay: float = 2.0) -> None:
with conn.cursor() as cur:
cur.execute(SCHEMA_SQL)
cur.execute(MIGRATION_SQL)
ensure_history_backfill(cur)
conn.commit()
return
except psycopg.OperationalError as exc:
@@ -563,3 +636,69 @@ def init_db(max_retries: int = 20, retry_delay: float = 2.0) -> None:
time.sleep(retry_delay)
if last_error is not None:
raise last_error
def ensure_history_backfill(cur) -> None:
cur.execute(
"""
SELECT id
FROM history_revisions
WHERE scope = 'organization'
AND revision_label = 'initial-backfill'
ORDER BY id ASC
LIMIT 1
"""
)
row = cur.fetchone()
if row is None:
cur.execute(
"""
INSERT INTO history_revisions (scope, revision_label, note)
VALUES ('organization', 'initial-backfill', 'Seeded from current members and seat_positions state')
RETURNING id
"""
)
revision_id = int(cur.fetchone()["id"])
else:
revision_id = int(row["id"])
cur.execute(
"""
INSERT INTO member_versions (
member_id, name, company, rank, role, department, grp, division, team, cell,
work_status, work_time, phone, email, photo_url,
valid_from, valid_to, revision_no, changed_by_user_id, change_reason
)
SELECT
m.id, m.name, COALESCE(m.company, ''), COALESCE(m.rank, ''), COALESCE(m.role, ''),
COALESCE(m.department, ''), COALESCE(m.grp, ''), COALESCE(m.division, ''), COALESCE(m.team, ''), COALESCE(m.cell, ''),
COALESCE(m.work_status, ''), COALESCE(m.work_time, ''), COALESCE(m.phone, ''), COALESCE(m.email, ''), COALESCE(m.photo_url, ''),
COALESCE(m.updated_at, m.created_at, NOW()), NULL, %s, NULL, 'initial-backfill'
FROM members AS m
WHERE NOT EXISTS (
SELECT 1
FROM member_versions mv
WHERE mv.member_id = m.id
)
""",
(revision_id,),
)
cur.execute(
"""
INSERT INTO seat_assignment_versions (
member_id, seat_map_id, seat_slot_id, seat_label,
valid_from, valid_to, revision_no, changed_by_user_id, change_reason
)
SELECT
sp.member_id, sp.seat_map_id, sp.seat_slot_id, COALESCE(sp.seat_label, ''),
COALESCE(sp.updated_at, NOW()), NULL, %s, NULL, 'initial-backfill'
FROM seat_positions AS sp
WHERE NOT EXISTS (
SELECT 1
FROM seat_assignment_versions sav
WHERE sav.member_id = sp.member_id
)
""",
(revision_id,),
)