v1.4 - Login fixes, etc

This commit is contained in:
2026-04-27 21:53:36 +12:00
parent 8cf9bfb441
commit c9580ac2eb
33 changed files with 2283 additions and 202 deletions
+106 -21
View File
@@ -1,6 +1,8 @@
from __future__ import annotations
from sqlalchemy import inspect, text
from dataclasses import dataclass, field
from sqlalchemy import MetaData, inspect, text
from sqlalchemy.engine import Engine
@@ -20,6 +22,27 @@ TENANT_TABLES = {
}
@dataclass(frozen=True)
class MigrationReport:
created_tables: tuple[str, ...] = ()
added_columns: tuple[str, ...] = ()
synced_tenant_rows: dict[str, int] = field(default_factory=dict)
def has_changes(self) -> bool:
return bool(self.created_tables or self.added_columns or self.synced_tenant_rows)
def summary(self) -> str:
parts: list[str] = []
if self.created_tables:
parts.append(f"created tables: {', '.join(self.created_tables)}")
if self.added_columns:
parts.append(f"patched columns: {', '.join(self.added_columns)}")
if self.synced_tenant_rows:
counts = ", ".join(f"{table}={count}" for table, count in sorted(self.synced_tenant_rows.items()))
parts.append(f"synced tenant rows: {counts}")
return "; ".join(parts) if parts else "schema already up to date"
def _has_column(engine: Engine, table_name: str, column_name: str) -> bool:
inspector = inspect(engine)
try:
@@ -40,25 +63,41 @@ def _table_exists(engine: Engine, table_name: str) -> bool:
return inspect(engine).has_table(table_name)
def ensure_tenant_columns(engine: Engine) -> None:
def ensure_metadata_tables(engine: Engine, metadata: MetaData) -> tuple[str, ...]:
missing_tables = tuple(table.name for table in metadata.sorted_tables if not _table_exists(engine, table.name))
if missing_tables:
metadata.create_all(bind=engine)
return missing_tables
def ensure_tenant_columns(engine: Engine) -> tuple[str, ...]:
added_columns: list[str] = []
for table_name in TENANT_TABLES:
if _table_exists(engine, table_name):
_add_tenant_column(engine, table_name)
if not _has_column(engine, table_name, "tenant_id"):
_add_tenant_column(engine, table_name)
added_columns.append(f"{table_name}.tenant_id")
return tuple(added_columns)
def sync_tenant_ids(engine: Engine) -> None:
if not _table_exists(engine, "client_accounts"):
return
def sync_tenant_ids(engine: Engine) -> dict[str, int]:
existing_tables = set(inspect(engine).get_table_names())
if "client_accounts" not in existing_tables:
return {}
synced_rows: dict[str, int] = {}
with engine.begin() as connection:
default_tenant = connection.execute(
text("SELECT tenant_id FROM client_accounts ORDER BY id LIMIT 1")
).scalar_one_or_none()
if not default_tenant:
return
return {}
statements = [
text(
(
"client_users",
text(
"""
UPDATE client_users
SET tenant_id = (
@@ -68,8 +107,11 @@ def sync_tenant_ids(engine: Engine) -> None:
)
WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
"""
),
),
text(
(
"client_feature_access",
text(
"""
UPDATE client_feature_access
SET tenant_id = (
@@ -79,15 +121,21 @@ def sync_tenant_ids(engine: Engine) -> None:
)
WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
"""
),
),
text(
(
"raw_materials",
text(
"""
UPDATE raw_materials
SET tenant_id = :default_tenant
WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
"""
),
),
text(
(
"raw_material_price_versions",
text(
"""
UPDATE raw_material_price_versions
SET tenant_id = (
@@ -97,8 +145,11 @@ def sync_tenant_ids(engine: Engine) -> None:
)
WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
"""
),
),
text(
(
"mixes",
text(
"""
UPDATE mixes
SET tenant_id = COALESCE(
@@ -111,8 +162,11 @@ def sync_tenant_ids(engine: Engine) -> None:
)
WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
"""
),
),
text(
(
"mix_ingredients",
text(
"""
UPDATE mix_ingredients
SET tenant_id = (
@@ -122,8 +176,11 @@ def sync_tenant_ids(engine: Engine) -> None:
)
WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
"""
),
),
text(
(
"products",
text(
"""
UPDATE products
SET tenant_id = COALESCE(
@@ -141,15 +198,21 @@ def sync_tenant_ids(engine: Engine) -> None:
)
WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
"""
),
),
text(
(
"scenarios",
text(
"""
UPDATE scenarios
SET tenant_id = :default_tenant
WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
"""
),
),
text(
(
"costing_results",
text(
"""
UPDATE costing_results
SET tenant_id = COALESCE(
@@ -167,29 +230,51 @@ def sync_tenant_ids(engine: Engine) -> None:
)
WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
"""
),
),
text(
(
"process_cost_rules",
text(
"""
UPDATE process_cost_rules
SET tenant_id = :default_tenant
WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
"""
),
),
text(
(
"packaging_cost_rules",
text(
"""
UPDATE packaging_cost_rules
SET tenant_id = :default_tenant
WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
"""
),
),
text(
(
"freight_cost_rules",
text(
"""
UPDATE freight_cost_rules
SET tenant_id = :default_tenant
WHERE tenant_id IS NULL OR tenant_id = '' OR tenant_id = 'default'
"""
),
),
]
for statement in statements:
connection.execute(statement, {"default_tenant": default_tenant})
for table_name, statement in statements:
if table_name not in existing_tables:
continue
result = connection.execute(statement, {"default_tenant": default_tenant})
if result.rowcount and result.rowcount > 0:
synced_rows[table_name] = result.rowcount
return synced_rows
def bootstrap_schema(engine: Engine, metadata: MetaData) -> MigrationReport:
created_tables = ensure_metadata_tables(engine, metadata)
added_columns = ensure_tenant_columns(engine)
return MigrationReport(created_tables=created_tables, added_columns=added_columns)