Files
ERP/backend/app/schema_sync.py
T
MMD 934b74ebbd Squashed 'pm/' content from commit 91f3271
git-subtree-dir: pm
git-subtree-split: 91f3271d866fa8ee471cfb6793e334b83e6d2b6e
2026-10-07 15:14:20 +03:30

143 lines
5.6 KiB
Python

"""Lightweight schema sync for additive/structural changes.
`Base.metadata.create_all` creates NEW tables but never alters existing ones,
so this module applies the missing pieces at startup:
1. `drop_legacy()` — tables whose shape changed or whose feature was removed
(including all legacy cache & sync tables).
2. `ensure_schema()` — additive columns and new enum values.
"""
import logging
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncConnection
logger = logging.getLogger("pm.schema")
# Tables dropped on startup (idempotent):
# - legacy cache & sync tables completely purged
# - legacy inspection / activity template tables
LEGACY_TABLES: list[str] = [
"material_requirements",
"reservations",
"milestones",
"purchase_requests",
"inspection_results",
"inspections",
"activity_template_materials",
"activity_templates",
"projects_cache",
"task_refs",
"sync_events",
"integration_logs",
"webhook_deliveries",
"sync_state",
]
LEGACY_COLUMNS: list[tuple[str, str]] = [
("projects", "client"),
("projects", "code"),
("projects", "erp_project_id"),
("tasks", "planned_progress"),
("projects", "legacy_op_project_id"),
("tasks", "legacy_op_wp_id"),
("task_qc_definitions", "task_op_wp_id"),
("defects", "task_op_wp_id"),
("project_assignments", "op_project_id"),
("tasks", "equipment"),
]
ADDITIVE_COLUMNS: list[tuple[str, str, str]] = [
# (table, column, type)
# RBAC: users that predate roles keep their previous (manager) access
("pm_users", "role", "VARCHAR(32) NOT NULL DEFAULT 'project_manager'"),
# Native PM Platform linking
("task_qc_definitions", "task_id", "UUID"),
("defects", "task_id", "UUID"),
("project_assignments", "project_id", "UUID"),
("tasks", "human_resources", "JSONB DEFAULT '[]'"),
("tasks", "materials", "JSONB DEFAULT '[]'"),
("task_qc_results", "photo_path", "VARCHAR(512)"),
]
ENUM_VALUES: list[tuple[str, str]] = [
# (enum type, value) — e.g. the pre-execution QC stage
("qc_phase", "pre"),
]
async def drop_legacy(conn: AsyncConnection) -> None:
# Migrate any existing equipment data to materials before dropping the equipment column
try:
check = await conn.execute(
text("SELECT 1 FROM information_schema.columns WHERE table_name = 'tasks' AND column_name = 'equipment'")
)
if check.scalar():
await conn.execute(
text("""
UPDATE tasks
SET materials = COALESCE(materials, '[]'::jsonb) || (
SELECT COALESCE(jsonb_agg(
jsonb_build_object(
'name', elem->>'name',
'unit', COALESCE(elem->>'unit', 'دستگاه'),
'specifications', COALESCE(elem->'specifications', '[]'::jsonb),
'count', COALESCE((elem->>'count')::numeric, 1)
)
), '[]'::jsonb)
FROM jsonb_array_elements(equipment) AS elem
WHERE elem->>'name' IS NOT NULL AND elem->>'name' != ''
)
WHERE equipment IS NOT NULL AND jsonb_array_length(equipment) > 0;
""")
)
logger.info("Migrated equipment data into materials in tasks table.")
except Exception as exc: # noqa: BLE001
logger.warning("equipment migration skipped: %s", exc)
for table in LEGACY_TABLES:
try:
await conn.execute(text(f"DROP TABLE IF EXISTS {table} CASCADE"))
except Exception as exc: # noqa: BLE001 — never block startup on this
logger.warning("legacy drop skipped %s: %s", table, exc)
for table, column in LEGACY_COLUMNS:
try:
await conn.execute(text(f"ALTER TABLE IF EXISTS {table} DROP COLUMN IF EXISTS {column} CASCADE"))
except Exception as exc: # noqa: BLE001
logger.warning("legacy column drop skipped %s.%s: %s", table, column, exc)
async def ensure_schema(conn: AsyncConnection) -> None:
for enum_type, value in ENUM_VALUES:
try:
await conn.execute(text(f"ALTER TYPE {enum_type} ADD VALUE IF NOT EXISTS '{value}'"))
except Exception as exc: # noqa: BLE001
logger.warning("enum sync skipped %s.%s: %s", enum_type, value, exc)
for table, column, coltype in ADDITIVE_COLUMNS:
try:
await conn.execute(text(f"ALTER TABLE IF EXISTS {table} ADD COLUMN IF NOT EXISTS {column} {coltype}"))
except Exception as exc: # noqa: BLE001
logger.warning("schema sync skipped %s.%s: %s", table, column, exc)
try:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS task_progress_logs (
id UUID PRIMARY KEY,
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
progress_before INTEGER DEFAULT 0,
progress_after INTEGER NOT NULL,
human_resources_used JSONB DEFAULT '[]',
materials_used JSONB DEFAULT '[]',
description TEXT DEFAULT '',
logged_by VARCHAR(255) DEFAULT '',
created_at TIMESTAMPTZ DEFAULT NOW()
)
"""))
await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_task_progress_logs_task ON task_progress_logs (task_id)"))
await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_task_progress_logs_created ON task_progress_logs (created_at)"))
except Exception as exc: # noqa: BLE001
logger.warning("task_progress_logs table sync skipped: %s", exc)