akg-factor-bridge/apply_views.py

125 lines
4.6 KiB
Python
Raw Normal View History

2026-07-27 10:03:45 +08:00
"""把插槽视图 DDL 应用到基座 PG —— 用桥自己的 psycopg 连接,不依赖 psql 客户端。
**为什么需要它**桥是跨机部署的独立单元设计 §4部署它的服务器上通常
**没有** astock-kg `akg-postgres` 容器README 里那条
`docker exec -i akg-postgres psql ...` 只在基座那台机器上成立
而桥容器是 python:3.11-slim也没有 psql 客户端
本模块用桥已有的 `AKG_PG_*` 连接执行 DDL零新增凭据全程 Docker
**边界说明**视图定义文件本来就随桥仓库版本控制sql/astock_kg_slot_views.sql
所以由桥来应用它并不越界桥定义插槽接口基座只是接口的宿主
但注意**权限**日常运行只需只读账号应用 DDL 需要能 CREATE VIEW / ALTER TABLE
的账号通常是基座 owner若报 permission denied owner 账号跑一次即可
docker compose exec -T \
-e AKG_PG_USER=<owner> -e AKG_PG_PASSWORD=<pw> \
akg-factor-bridge python run.py apply-views
注意必须用 `docker compose exec -e`在命令前面写 `AKG_PG_USER=... docker compose exec`
只会设置宿主机上 compose 客户端进程的环境变量传不进容器
逐语句执行 + 逐语句报错一条失败比如 ALTER 缺权限不影响其余语句
这样你能准确看到是哪一条没过而不是整个脚本回滚
"""
from __future__ import annotations
from pathlib import Path
import psycopg
import config
DEFAULT_SQL = "sql/astock_kg_slot_views.sql"
def split_statements(sql: str) -> list[str]:
"""按分号切分 SQL 语句。剥掉 -- 行注释,尊重单引号字符串。
本文件的 DDL 里没有 $$ 引用块与多行注释故不做处理
若将来加了函数体这里要先升级再用
"""
out, buf = [], []
in_str = in_comment = False
i, n = 0, len(sql)
while i < n:
c = sql[i]
nxt = sql[i + 1] if i + 1 < n else ""
if in_comment:
if c == "\n":
in_comment = False
buf.append(c)
i += 1
continue
if in_str:
buf.append(c)
if c == "'":
if nxt == "'": # 转义的单引号 ''
buf.append(nxt)
i += 2
continue
in_str = False
i += 1
continue
if c == "-" and nxt == "-":
in_comment = True
i += 2
continue
if c == "'":
in_str = True
buf.append(c)
i += 1
continue
if c == ";":
stmt = "".join(buf).strip()
if stmt:
out.append(stmt)
buf = []
i += 1
continue
buf.append(c)
i += 1
tail = "".join(buf).strip()
if tail:
out.append(tail)
return out
def _label(stmt: str) -> str:
"""给语句起个人能读的名字,用于逐条报告。"""
head = " ".join(stmt.split())[:78]
return head + ("" if len(" ".join(stmt.split())) > 78 else "")
def apply(path: str = DEFAULT_SQL, dry_run: bool = False) -> int:
p = Path(path)
if not p.exists():
p = Path("/app") / path # 容器内工作目录兜底
if not p.exists():
raise SystemExit(f"找不到 SQL 文件: {path}")
stmts = split_statements(p.read_text(encoding="utf-8"))
c = config.akg_pg()
print(f"应用 {p}{len(stmts)} 条语句)→ {c.user}@{c.host}:{c.port}/{c.db}")
if dry_run:
for i, s in enumerate(stmts, 1):
print(f" [{i:>2}] {_label(s)}")
print("\n(dry-run未执行)")
return 0
ok = fail = 0
# autocommit逐条独立提交一条失败不毒化后续DDL 各自独立、且都幂等)
with psycopg.connect(host=c.host, port=c.port, user=c.user,
password=c.password, dbname=c.db, autocommit=True) as conn:
for i, s in enumerate(stmts, 1):
try:
conn.execute(s)
ok += 1
print(f" ✅ [{i:>2}] {_label(s)}")
except Exception as e: # noqa: BLE001 —— 逐条报错才看得出是哪条缺权限
fail += 1
print(f" ❌ [{i:>2}] {_label(s)}\n {e!r}")
print(f"\n完成:成功 {ok} 条,失败 {fail}")
if fail:
print("提示CREATE VIEW / ALTER TABLE 需要 DDL 权限;只读账号会报 "
"permission denied —— 用基座 owner 账号跑一次即可(见本文件 docstring")
return fail