akg-factor-bridge/apply_views.py

125 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""把插槽视图 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