184 lines
6.2 KiB
Python
Executable File
184 lines
6.2 KiB
Python
Executable File
from contextlib import contextmanager
|
|
import time
|
|
from typing import Iterator
|
|
|
|
from psycopg.rows import dict_row
|
|
import psycopg
|
|
|
|
from .config import DATABASE_URL
|
|
|
|
|
|
SCHEMA_SQL = """
|
|
CREATE TABLE IF NOT EXISTS members (
|
|
id SERIAL PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
employee_id TEXT,
|
|
company TEXT,
|
|
rank TEXT,
|
|
role TEXT,
|
|
department TEXT,
|
|
grp TEXT,
|
|
division TEXT,
|
|
team TEXT,
|
|
cell TEXT,
|
|
work_status TEXT,
|
|
work_time TEXT,
|
|
phone TEXT,
|
|
email TEXT,
|
|
seat_label TEXT,
|
|
photo_url TEXT,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS seat_maps (
|
|
id SERIAL PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
image_url TEXT NOT NULL,
|
|
source_type TEXT NOT NULL DEFAULT 'image',
|
|
source_url TEXT,
|
|
preview_svg TEXT,
|
|
view_box_min_x DOUBLE PRECISION,
|
|
view_box_min_y DOUBLE PRECISION,
|
|
view_box_width DOUBLE PRECISION,
|
|
view_box_height DOUBLE PRECISION,
|
|
image_width INTEGER,
|
|
image_height INTEGER,
|
|
grid_rows INTEGER NOT NULL,
|
|
grid_cols INTEGER NOT NULL,
|
|
cell_gap INTEGER NOT NULL DEFAULT 0,
|
|
is_active BOOLEAN NOT NULL DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS seat_positions (
|
|
member_id INTEGER PRIMARY KEY REFERENCES members(id) ON DELETE CASCADE,
|
|
seat_map_id INTEGER REFERENCES seat_maps(id) ON DELETE CASCADE,
|
|
seat_slot_id INTEGER,
|
|
row_index INTEGER NOT NULL DEFAULT 0,
|
|
col_index INTEGER NOT NULL DEFAULT 0,
|
|
seat_label TEXT,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS seat_slots (
|
|
id SERIAL PRIMARY KEY,
|
|
seat_map_id INTEGER NOT NULL REFERENCES seat_maps(id) ON DELETE CASCADE,
|
|
slot_key TEXT NOT NULL,
|
|
label TEXT NOT NULL,
|
|
x DOUBLE PRECISION NOT NULL,
|
|
y DOUBLE PRECISION NOT NULL,
|
|
rotation DOUBLE PRECISION NOT NULL DEFAULT 0,
|
|
layer_name TEXT NOT NULL DEFAULT 'chair',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (seat_map_id, slot_key)
|
|
);
|
|
"""
|
|
|
|
MIGRATION_SQL = """
|
|
ALTER TABLE members ADD COLUMN IF NOT EXISTS employee_id TEXT;
|
|
ALTER TABLE members ADD COLUMN IF NOT EXISTS sort_order INTEGER NOT NULL DEFAULT 0;
|
|
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS seat_map_id INTEGER REFERENCES seat_maps(id) ON DELETE CASCADE;
|
|
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS seat_slot_id INTEGER;
|
|
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS row_index INTEGER NOT NULL DEFAULT 0;
|
|
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS col_index INTEGER NOT NULL DEFAULT 0;
|
|
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS seat_label TEXT;
|
|
ALTER TABLE seat_positions ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
|
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS source_type TEXT NOT NULL DEFAULT 'image';
|
|
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS source_url TEXT;
|
|
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS preview_svg TEXT;
|
|
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS view_box_min_x DOUBLE PRECISION;
|
|
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS view_box_min_y DOUBLE PRECISION;
|
|
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS view_box_width DOUBLE PRECISION;
|
|
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS view_box_height DOUBLE PRECISION;
|
|
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS image_width INTEGER;
|
|
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS image_height INTEGER;
|
|
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS cell_gap INTEGER NOT NULL DEFAULT 0;
|
|
ALTER TABLE seat_maps ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT FALSE;
|
|
ALTER TABLE seat_maps ALTER COLUMN image_url DROP NOT NULL;
|
|
|
|
CREATE TABLE IF NOT EXISTS seat_slots (
|
|
id SERIAL PRIMARY KEY,
|
|
seat_map_id INTEGER NOT NULL REFERENCES seat_maps(id) ON DELETE CASCADE,
|
|
slot_key TEXT NOT NULL,
|
|
label TEXT NOT NULL,
|
|
x DOUBLE PRECISION NOT NULL,
|
|
y DOUBLE PRECISION NOT NULL,
|
|
rotation DOUBLE PRECISION NOT NULL DEFAULT 0,
|
|
layer_name TEXT NOT NULL DEFAULT 'chair',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (seat_map_id, slot_key)
|
|
);
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'seat_positions' AND column_name = 'x'
|
|
) THEN
|
|
EXECUTE 'UPDATE seat_positions SET row_index = COALESCE(y, row_index, 0), col_index = COALESCE(x, col_index, 0) WHERE seat_map_id IS NULL';
|
|
END IF;
|
|
END $$;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'seat_positions' AND column_name = 'floor_label'
|
|
) THEN
|
|
EXECUTE 'UPDATE seat_positions SET seat_label = COALESCE(seat_label, floor_label) WHERE seat_label IS NULL';
|
|
END IF;
|
|
END $$;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS seat_positions_map_cell_idx
|
|
ON seat_positions (seat_map_id, row_index, col_index)
|
|
WHERE seat_map_id IS NOT NULL;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS seat_positions_slot_idx
|
|
ON seat_positions (seat_slot_id)
|
|
WHERE seat_slot_id IS NOT NULL;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.table_constraints
|
|
WHERE constraint_name = 'seat_positions_seat_slot_id_fkey'
|
|
AND table_name = 'seat_positions'
|
|
) THEN
|
|
ALTER TABLE seat_positions
|
|
ADD CONSTRAINT seat_positions_seat_slot_id_fkey
|
|
FOREIGN KEY (seat_slot_id) REFERENCES seat_slots(id) ON DELETE CASCADE;
|
|
END IF;
|
|
END $$;
|
|
"""
|
|
|
|
|
|
@contextmanager
|
|
def get_conn() -> Iterator[psycopg.Connection]:
|
|
with psycopg.connect(DATABASE_URL, row_factory=dict_row) as conn:
|
|
yield conn
|
|
|
|
|
|
def init_db(max_retries: int = 20, retry_delay: float = 2.0) -> None:
|
|
last_error: Exception | None = None
|
|
for _ in range(max_retries):
|
|
try:
|
|
with get_conn() as conn:
|
|
with conn.cursor() as cur:
|
|
cur.execute(SCHEMA_SQL)
|
|
cur.execute(MIGRATION_SQL)
|
|
conn.commit()
|
|
return
|
|
except psycopg.OperationalError as exc:
|
|
last_error = exc
|
|
time.sleep(retry_delay)
|
|
if last_error is not None:
|
|
raise last_error
|