173 lines
5.8 KiB
Python
173 lines
5.8 KiB
Python
"""市場情報正式 DB schema 只讀探針。
|
||
|
||
本模組只查詢系統 catalog,不使用 DatabaseManager、不呼叫 create_all、不寫入。
|
||
"""
|
||
|
||
from sqlalchemy import bindparam, create_engine, text
|
||
|
||
|
||
def _build_table_status(expected_tables, existing_tables):
|
||
existing_set = set(existing_tables)
|
||
return [
|
||
{
|
||
"table": table_name,
|
||
"exists": table_name in existing_set,
|
||
}
|
||
for table_name in expected_tables
|
||
]
|
||
|
||
|
||
def _probe_postgresql(conn, expected_tables):
|
||
existing_tables = []
|
||
for table_name in expected_tables:
|
||
exists = conn.execute(
|
||
text(
|
||
"""
|
||
SELECT EXISTS (
|
||
SELECT 1
|
||
FROM information_schema.tables
|
||
WHERE table_schema = ANY (current_schemas(false))
|
||
AND table_name = :table_name
|
||
)
|
||
"""
|
||
),
|
||
{"table_name": table_name},
|
||
).scalar()
|
||
if exists:
|
||
existing_tables.append(table_name)
|
||
return existing_tables
|
||
|
||
|
||
def _probe_sqlite(conn, expected_tables):
|
||
rows = conn.execute(
|
||
text(
|
||
"""
|
||
SELECT name
|
||
FROM sqlite_master
|
||
WHERE type = 'table'
|
||
AND name IN :table_names
|
||
"""
|
||
).bindparams(bindparam("table_names", expanding=True)),
|
||
{"table_names": tuple(expected_tables)},
|
||
).fetchall()
|
||
return [row[0] for row in rows]
|
||
|
||
|
||
def build_schema_db_probe_plan(
|
||
expected_tables,
|
||
*,
|
||
execute_requested=False,
|
||
database_url=None,
|
||
database_type=None,
|
||
engine=None,
|
||
):
|
||
"""建立 DB schema 探針結果;預設只回 planned,不連 DB。"""
|
||
expected_tables = list(expected_tables)
|
||
if not execute_requested:
|
||
return {
|
||
"mode": "schema_db_probe_planned",
|
||
"execute_requested": False,
|
||
"read_only_query_executed": False,
|
||
"database_connection_opened": False,
|
||
"database_session_created": False,
|
||
"explicit_transaction_opened": False,
|
||
"database_write_executed": False,
|
||
"database_commit_executed": False,
|
||
"migration_executed": False,
|
||
"schema_tables_exist": False,
|
||
"expected_tables": expected_tables,
|
||
"existing_tables": [],
|
||
"missing_tables": expected_tables,
|
||
"table_statuses": _build_table_status(expected_tables, []),
|
||
"blocked_reasons": [
|
||
"execute_false_planned_only",
|
||
"migration_not_executed_by_this_probe",
|
||
"seed_write_still_blocked",
|
||
],
|
||
}
|
||
|
||
from config import DATABASE_PATH, DATABASE_TYPE
|
||
|
||
effective_database_type = (database_type or DATABASE_TYPE or "").lower()
|
||
effective_database_url = database_url or DATABASE_PATH
|
||
created_engine = False
|
||
connection_opened = False
|
||
|
||
try:
|
||
if engine is None:
|
||
connect_args = {}
|
||
if effective_database_type == "postgresql":
|
||
connect_args = {
|
||
"connect_timeout": 8,
|
||
"options": "-c statement_timeout=15000",
|
||
}
|
||
engine = create_engine(
|
||
effective_database_url,
|
||
isolation_level="AUTOCOMMIT",
|
||
pool_pre_ping=True,
|
||
connect_args=connect_args,
|
||
)
|
||
created_engine = True
|
||
|
||
with engine.connect() as conn:
|
||
connection_opened = True
|
||
if effective_database_type == "postgresql":
|
||
existing_tables = _probe_postgresql(conn, expected_tables)
|
||
else:
|
||
existing_tables = _probe_sqlite(conn, expected_tables)
|
||
|
||
missing_tables = [
|
||
table_name for table_name in expected_tables
|
||
if table_name not in set(existing_tables)
|
||
]
|
||
schema_tables_exist = not missing_tables
|
||
blocked_reasons = [
|
||
"migration_not_executed_by_this_probe",
|
||
"seed_write_still_blocked",
|
||
]
|
||
if not schema_tables_exist:
|
||
blocked_reasons.insert(0, "market_tables_missing")
|
||
|
||
return {
|
||
"mode": "schema_db_probe_read_only",
|
||
"execute_requested": True,
|
||
"read_only_query_executed": True,
|
||
"database_connection_opened": connection_opened,
|
||
"database_session_created": False,
|
||
"explicit_transaction_opened": False,
|
||
"database_write_executed": False,
|
||
"database_commit_executed": False,
|
||
"migration_executed": False,
|
||
"schema_tables_exist": schema_tables_exist,
|
||
"expected_tables": expected_tables,
|
||
"existing_tables": existing_tables,
|
||
"missing_tables": missing_tables,
|
||
"table_statuses": _build_table_status(expected_tables, existing_tables),
|
||
"blocked_reasons": blocked_reasons,
|
||
}
|
||
except Exception as exc:
|
||
return {
|
||
"mode": "schema_db_probe_error",
|
||
"execute_requested": True,
|
||
"read_only_query_executed": False,
|
||
"database_connection_opened": connection_opened,
|
||
"database_session_created": False,
|
||
"explicit_transaction_opened": False,
|
||
"database_write_executed": False,
|
||
"database_commit_executed": False,
|
||
"migration_executed": False,
|
||
"schema_tables_exist": False,
|
||
"expected_tables": expected_tables,
|
||
"existing_tables": [],
|
||
"missing_tables": expected_tables,
|
||
"table_statuses": _build_table_status(expected_tables, []),
|
||
"blocked_reasons": [
|
||
"schema_db_probe_error",
|
||
"seed_write_still_blocked",
|
||
],
|
||
"error_message": str(exc),
|
||
}
|
||
finally:
|
||
if created_engine:
|
||
engine.dispose()
|