approve: 修复当前迭代 2 个功能 Bug(pbls-初始迭代)
This commit is contained in:
parent
27fd7addee
commit
66fcd7af90
11
pbl_appcodes.egg-info/PKG-INFO
Normal file
11
pbl_appcodes.egg-info/PKG-INFO
Normal file
@ -0,0 +1,11 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: pbl_appcodes
|
||||
Version: 1.0.0
|
||||
Summary: PBL 枚举编码注入(8 组 74 项,幂等,零表)——appcodes/apptype 等枚举种子 + self_check
|
||||
Requires-Python: >=3.7
|
||||
Requires-Dist: pbl_common
|
||||
Requires-Dist: apppublic
|
||||
Requires-Dist: sqlor
|
||||
Requires-Dist: ahserver
|
||||
Requires-Dist: appbase
|
||||
Requires-Dist: rbac
|
||||
15
pbl_appcodes.egg-info/SOURCES.txt
Normal file
15
pbl_appcodes.egg-info/SOURCES.txt
Normal file
@ -0,0 +1,15 @@
|
||||
README.md
|
||||
pyproject.toml
|
||||
pbl_appcodes/__init__.py
|
||||
pbl_appcodes/api.py
|
||||
pbl_appcodes/codes.json
|
||||
pbl_appcodes/codes_data.py
|
||||
pbl_appcodes/init.py
|
||||
pbl_appcodes/seed.py
|
||||
pbl_appcodes/self_check.py
|
||||
pbl_appcodes.egg-info/PKG-INFO
|
||||
pbl_appcodes.egg-info/SOURCES.txt
|
||||
pbl_appcodes.egg-info/dependency_links.txt
|
||||
pbl_appcodes.egg-info/requires.txt
|
||||
pbl_appcodes.egg-info/top_level.txt
|
||||
pbl_appcodes/data/codes.json
|
||||
1
pbl_appcodes.egg-info/dependency_links.txt
Normal file
1
pbl_appcodes.egg-info/dependency_links.txt
Normal file
@ -0,0 +1 @@
|
||||
|
||||
6
pbl_appcodes.egg-info/requires.txt
Normal file
6
pbl_appcodes.egg-info/requires.txt
Normal file
@ -0,0 +1,6 @@
|
||||
pbl_common
|
||||
apppublic
|
||||
sqlor
|
||||
ahserver
|
||||
appbase
|
||||
rbac
|
||||
1
pbl_appcodes.egg-info/top_level.txt
Normal file
1
pbl_appcodes.egg-info/top_level.txt
Normal file
@ -0,0 +1 @@
|
||||
pbl_appcodes
|
||||
@ -37,11 +37,14 @@ from pbl_common.errors import (
|
||||
err,
|
||||
)
|
||||
from pbl_common.dbutil import (
|
||||
delete_row,
|
||||
get_db,
|
||||
get_module_dbname,
|
||||
insert_row,
|
||||
now_str,
|
||||
safe_ident,
|
||||
sqlor_available,
|
||||
update_row,
|
||||
)
|
||||
|
||||
log = logging.getLogger('pbl_appcodes.seed')
|
||||
@ -294,20 +297,78 @@ async def ensure_tables(db=None, dbname=None, create_missing=False):
|
||||
if not exists:
|
||||
raise DbError(
|
||||
message='共享编码表 %s 不存在(appbase 未初始化)' % table,
|
||||
detail={'table': table, 'dbname': db.dbname,
|
||||
detail={'table': table, 'dbname': _db_dbname(db),
|
||||
'hint': '先部署 appbase 或传 create_missing=True'})
|
||||
result[table] = True
|
||||
result['created'] = created
|
||||
return result
|
||||
|
||||
|
||||
async def _table_exists(db, table):
|
||||
def _db_module(db):
|
||||
"""取 db 门面的模块名(原语按 module 解析库名;替身缺则回落 pbl_appcodes)。"""
|
||||
return str(getattr(db, 'module', None) or 'pbl_appcodes')
|
||||
|
||||
|
||||
def _db_dbname(db):
|
||||
"""解析 db 门面对应的库名(三种形态兼容,取不到返回 None 不抛)。
|
||||
|
||||
* 真实 ``pbl_common.dbutil.Db``:只有公开 slot ``module``(``__slots__`` 里是
|
||||
私有 ``_dbname``),故经 ``get_module_dbname(db.module)`` 走宿主映射;
|
||||
* 测试替身(self_check 的 ``_NoTableDb``):只声明 ``dbname``;
|
||||
* 显式注入库名的 Db:``_dbname`` 有值。
|
||||
"""
|
||||
mod = getattr(db, 'module', None)
|
||||
if mod:
|
||||
try:
|
||||
rows = await db.execute('SHOW TABLES LIKE %s', [table])
|
||||
name = get_module_dbname(mod)
|
||||
if name:
|
||||
return str(name)
|
||||
except Exception: # noqa: BLE001 宿主映射未挂时降级
|
||||
pass
|
||||
for attr in ('dbname', '_dbname'):
|
||||
value = getattr(db, attr, None)
|
||||
if value:
|
||||
return str(value)
|
||||
return None
|
||||
|
||||
|
||||
async def _table_exists(db, table):
|
||||
"""表是否存在(information_schema COUNT,sqlor ${name}$ 具名参数)。
|
||||
|
||||
三条踩坑教训(2026-09-29 真机 probe_show_tables.py 实测):
|
||||
* 不能用 ``SHOW TABLES LIKE``:sqlor 通道下它对**存在**的表返回 int(而非
|
||||
行集),``rows_to_dicts`` 迭代 int 抛 ``TypeError: 'int' object is not
|
||||
iterable``;只有不存在的表才返回 []。故把旧 ``%s`` 简单换成 ``${t}$``
|
||||
仍会崩——必须改 information_schema COUNT,实测稳定返回 [{'c': N}]。
|
||||
* 必须**继续走 ``db.execute``**,不能直接调 ``q_all``:self_check 的
|
||||
``_NoTableDb`` 替身靠拦截 ``execute`` 返回 [] 来模拟「缺表」,绕过 db
|
||||
通道会让离线自检真连库、替身失效(首版修复即踩此坑,17/18 FAIL)。
|
||||
* 不直接复用 ``pbl_common.db.table_exists``:它带 ``_TABLE_CACHE``,而本
|
||||
函数语义是「实时探测」——``ensure_tables`` 建表后要立即复查,缓存会返回
|
||||
stale False 把刚建好的表误判成缺失。
|
||||
"""
|
||||
try:
|
||||
dbname = _db_dbname(db)
|
||||
if dbname:
|
||||
rows = await db.execute(
|
||||
'SELECT COUNT(*) AS c FROM information_schema.TABLES '
|
||||
'WHERE TABLE_SCHEMA=${s}$ AND TABLE_NAME=${t}$',
|
||||
{'s': dbname, 't': str(table)})
|
||||
else:
|
||||
# 库名不可解析时退化为仅按表名匹配(一个连接只连一个库,语义等价
|
||||
# 于 SHOW TABLES LIKE,但走 information_schema 避免 int 返回陷阱)
|
||||
rows = await db.execute(
|
||||
'SELECT COUNT(*) AS c FROM information_schema.TABLES '
|
||||
'WHERE TABLE_NAME=${t}$', {'t': str(table)})
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.warning('SHOW TABLES LIKE %s 失败:%s', table, exc)
|
||||
log.warning('表存在性探测失败 %s:%s', table, exc)
|
||||
return False
|
||||
return bool(rows)
|
||||
if not rows: # 替身返回 [] → 表不存在
|
||||
return False
|
||||
first = rows[0]
|
||||
count = list(first.values())[0] if isinstance(first, dict) \
|
||||
else getattr(first, 'c', None)
|
||||
return int(count or 0) > 0
|
||||
|
||||
|
||||
def _ddl_of(table):
|
||||
@ -409,22 +470,72 @@ async def seed_group(group_id, db=None, dbname=None, path=None, dry_run=False):
|
||||
db = db or get_db(module='pbl_appcodes', dbname=dbname)
|
||||
stat = {'group': parent['id'], 'inserted': 0, 'updated': 0, 'skipped': 0,
|
||||
'dry_run': False}
|
||||
stat[_merge(await _upsert_row(db, APPCODES_TABLE, parent, ('name', 'hierarchy_flg', 'sort')))] = 1
|
||||
# parent 记 1 行(每组恰一条父记录);kv 逐行累加。两侧都经 _merge 归一,
|
||||
# 保证写入的键恒在 totals 闭集内(原版 kv 侧用裸 action,键名漂移即静默丢计数)。
|
||||
_p_action = _merge(await _upsert_row(db, APPCODES_TABLE, parent,
|
||||
('name', 'hierarchy_flg', 'sort')))
|
||||
stat[_p_action] = 1
|
||||
for kv in kvs:
|
||||
action = await _upsert_row(db, APPCODES_KV_TABLE, kv, ('v', 'sort', 'parentid'))
|
||||
action = _merge(await _upsert_row(db, APPCODES_KV_TABLE, kv,
|
||||
('v', 'sort', 'parentid')))
|
||||
stat[action] = stat.get(action, 0) + 1
|
||||
log.info('seed_group %s -> %s', parent['id'],
|
||||
{k: v for k, v in stat.items() if k != 'group'})
|
||||
return stat
|
||||
|
||||
|
||||
#: 统计键闭集(seed_all 汇总 totals 只认这三个键;域外键会被静默丢计数)
|
||||
STAT_ACTIONS = ('inserted', 'updated', 'skipped')
|
||||
|
||||
|
||||
def _merge(action):
|
||||
"""把 ``_upsert_row`` 返回的动作归一到统计键闭集(防御层,非业务分支)。
|
||||
|
||||
``_upsert_row`` 契约返回 'inserted'/'updated'/'skipped';本函数保证任何域外
|
||||
值折叠为 'skipped'——既不虚报写入,也避免 ``seed_all`` 的 totals 汇总因键名
|
||||
漂移而**静默丢计数**(域外键不在 totals 循环里,加了多少都看不见)。
|
||||
|
||||
历史缺陷:原版(HEAD 第 412 行)调用了**从未定义**的 ``_merge``,
|
||||
``seed_group`` 一进真实写库分支即 NameError —— 即 pbl_appcodes 的
|
||||
``seed_all`` 从未成功跑通过(dry_run 路径不经过该行,故 self_check 全绿
|
||||
也发现不了)。2026-09-29 补齐定义。
|
||||
"""
|
||||
action = str(action or '').strip()
|
||||
return action if action in STAT_ACTIONS else 'skipped'
|
||||
|
||||
|
||||
async def _read_by_id(db, table, pk_value):
|
||||
"""按主键读一行(走 ``db.execute`` → q_all,**不强制租户上下文**)。
|
||||
|
||||
为什么不能用 ``db.read(table, {...})``:``Db.read`` 传表名时会先
|
||||
``self._tenant()`` 再拼 ``tenant_id`` 过滤,而 appcodes / appcodes_kv 是
|
||||
appbase 的**共享编码表,没有 tenant_id 列**(实测列面:
|
||||
id/name/hierarchy_flg/sort/created_at/updated_at 与
|
||||
id/parentid/k/v/sort/created_at/updated_at)→ 必然
|
||||
``PBL_E_TENANT_MISSING``,或把 tenant_id 当未知列拒掉。
|
||||
``db.execute`` 只过禁写词校验后委托 ``q_all(sql, dict, module=self.module)``,
|
||||
是共享表的合规读通道(同 _table_exists 的口径)。
|
||||
"""
|
||||
tbl = safe_ident(table, '表名')
|
||||
rows = await db.execute('SELECT * FROM `%s` WHERE `id`=${i}$ LIMIT 1' % tbl,
|
||||
{'i': str(pk_value)})
|
||||
return rows[0] if rows else None
|
||||
|
||||
|
||||
async def _upsert_row(db, table, row, compare_fields):
|
||||
"""按主键幂等 upsert,返回 'inserted' / 'updated' / 'skipped'。"""
|
||||
"""按主键幂等 upsert,返回 'inserted' / 'updated' / 'skipped'。
|
||||
|
||||
写路径走 ``insert_row`` / ``update_row`` **原语**(tenant_id 不传即豁免租户
|
||||
打头),而不是 ``Db.create`` / ``Db.update`` 门面:门面无条件
|
||||
``self._tenant()`` fail-closed,对无 tenant_id 列的共享编码表必抛
|
||||
``PBL_E_TENANT_MISSING``(2026-09-29 build_run5 实测)。原语同样做列发现 +
|
||||
``pick_columns`` + ``build_insert/update``,纪律不降。
|
||||
"""
|
||||
pk_value = row['id']
|
||||
module = _db_module(db)
|
||||
existing = None
|
||||
try:
|
||||
rows = await db.read(table, {'id': pk_value})
|
||||
existing = rows[0] if rows else None
|
||||
existing = await _read_by_id(db, table, pk_value)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.warning('读取 %s id=%s 失败(按不存在处理):%s', table, pk_value, exc)
|
||||
existing = None
|
||||
@ -434,7 +545,7 @@ async def _upsert_row(db, table, row, compare_fields):
|
||||
payload['created_at'] = stamp
|
||||
payload['updated_at'] = stamp
|
||||
try:
|
||||
await db.create(table, payload)
|
||||
await insert_row(table, payload, module=module)
|
||||
return 'inserted'
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise DbError(message='注入 %s id=%s 失败:%s' % (table, pk_value, exc),
|
||||
@ -450,7 +561,7 @@ async def _upsert_row(db, table, row, compare_fields):
|
||||
return 'skipped'
|
||||
diff['updated_at'] = stamp
|
||||
try:
|
||||
await db.update(table, diff, {'id': pk_value})
|
||||
await update_row(table, diff, where={'id': pk_value}, module=module)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
raise DbError(message='更新 %s id=%s 失败:%s' % (table, pk_value, exc),
|
||||
detail={'table': table, 'id': pk_value, 'diff': diff})
|
||||
@ -496,7 +607,7 @@ async def seed_all(db=None, dbname=None, path=None, dry_run=False, ensure=True,
|
||||
'inserted': totals['inserted'], 'updated': totals['updated'],
|
||||
'skipped': totals['skipped'], 'per_group': per_group,
|
||||
'dry_run': False, 'sqlor': sqlor_available(),
|
||||
'dbname': db.dbname}
|
||||
'dbname': _db_dbname(db)}
|
||||
|
||||
|
||||
async def unseed_group(group_id, db=None, dbname=None):
|
||||
@ -508,10 +619,15 @@ async def unseed_group(group_id, db=None, dbname=None):
|
||||
group = get_group(group_id)
|
||||
db = db or get_db(module='pbl_appcodes', dbname=dbname)
|
||||
parent, kvs = _plan_group(group)
|
||||
module = _db_module(db)
|
||||
removed = 0
|
||||
# 写路径走 delete_row 原语(tenant_id 不传豁免租户打头),同 _upsert_row 口径。
|
||||
# 共享编码表无 tenant_id 列,db.delete 门面会 fail-closed 抛 PBL_E_TENANT_MISSING。
|
||||
for kv in kvs:
|
||||
removed += int(await db.delete(APPCODES_KV_TABLE, {'id': kv['id']}) or 0)
|
||||
removed += int(await db.delete(APPCODES_TABLE, {'id': parent['id']}) or 0)
|
||||
removed += int(await delete_row(APPCODES_KV_TABLE, {'id': kv['id']},
|
||||
module=module) or 0)
|
||||
removed += int(await delete_row(APPCODES_TABLE, {'id': parent['id']},
|
||||
module=module) or 0)
|
||||
return {'group': parent['id'], 'removed': removed}
|
||||
|
||||
|
||||
@ -521,21 +637,21 @@ async def verify_seed(db=None, dbname=None, path=None):
|
||||
plan = dry_run_seed(path=path)
|
||||
missing, mismatch, found = [], [], 0
|
||||
for parent in plan['parents']:
|
||||
rows = await db.read(APPCODES_TABLE, {'id': parent['id']})
|
||||
if not rows:
|
||||
row = await _read_by_id(db, APPCODES_TABLE, parent['id'])
|
||||
if not row:
|
||||
missing.append('%s:%s' % (APPCODES_TABLE, parent['id']))
|
||||
continue
|
||||
found += 1
|
||||
row = rows[0] if isinstance(rows[0], dict) else dict(rows[0] or {})
|
||||
row = row if isinstance(row, dict) else dict(row or {})
|
||||
if str(row.get('name') or '') != parent['name']:
|
||||
mismatch.append('%s:%s.name' % (APPCODES_TABLE, parent['id']))
|
||||
for kv in plan['kvs']:
|
||||
rows = await db.read(APPCODES_KV_TABLE, {'id': kv['id']})
|
||||
if not rows:
|
||||
row = await _read_by_id(db, APPCODES_KV_TABLE, kv['id'])
|
||||
if not row:
|
||||
missing.append('%s:%s' % (APPCODES_KV_TABLE, kv['id']))
|
||||
continue
|
||||
found += 1
|
||||
row = rows[0] if isinstance(rows[0], dict) else dict(rows[0] or {})
|
||||
row = row if isinstance(row, dict) else dict(row or {})
|
||||
if str(row.get('v') or '') != kv['v'] or int(row.get('sort') or 0) != kv['sort']:
|
||||
mismatch.append('%s:%s' % (APPCODES_KV_TABLE, kv['id']))
|
||||
return {'ok': not missing and not mismatch, 'missing': missing,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user