Squashed 'pm/' content from commit 91f3271
git-subtree-dir: pm git-subtree-split: 91f3271d866fa8ee471cfb6793e334b83e6d2b6e
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""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)
|
||||
|
||||
Reference in New Issue
Block a user