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,
|
err,
|
||||||
)
|
)
|
||||||
from pbl_common.dbutil import (
|
from pbl_common.dbutil import (
|
||||||
|
delete_row,
|
||||||
get_db,
|
get_db,
|
||||||
get_module_dbname,
|
get_module_dbname,
|
||||||
|
insert_row,
|
||||||
now_str,
|
now_str,
|
||||||
safe_ident,
|
safe_ident,
|
||||||
sqlor_available,
|
sqlor_available,
|
||||||
|
update_row,
|
||||||
)
|
)
|
||||||
|
|
||||||
log = logging.getLogger('pbl_appcodes.seed')
|
log = logging.getLogger('pbl_appcodes.seed')
|
||||||
@ -294,20 +297,78 @@ async def ensure_tables(db=None, dbname=None, create_missing=False):
|
|||||||
if not exists:
|
if not exists:
|
||||||
raise DbError(
|
raise DbError(
|
||||||
message='共享编码表 %s 不存在(appbase 未初始化)' % table,
|
message='共享编码表 %s 不存在(appbase 未初始化)' % table,
|
||||||
detail={'table': table, 'dbname': db.dbname,
|
detail={'table': table, 'dbname': _db_dbname(db),
|
||||||
'hint': '先部署 appbase 或传 create_missing=True'})
|
'hint': '先部署 appbase 或传 create_missing=True'})
|
||||||
result[table] = True
|
result[table] = True
|
||||||
result['created'] = created
|
result['created'] = created
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
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):
|
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:
|
try:
|
||||||
rows = await db.execute('SHOW TABLES LIKE %s', [table])
|
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
|
except Exception as exc: # noqa: BLE001
|
||||||
log.warning('SHOW TABLES LIKE %s 失败:%s', table, exc)
|
log.warning('表存在性探测失败 %s:%s', table, exc)
|
||||||
return False
|
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):
|
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)
|
db = db or get_db(module='pbl_appcodes', dbname=dbname)
|
||||||
stat = {'group': parent['id'], 'inserted': 0, 'updated': 0, 'skipped': 0,
|
stat = {'group': parent['id'], 'inserted': 0, 'updated': 0, 'skipped': 0,
|
||||||
'dry_run': False}
|
'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:
|
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
|
stat[action] = stat.get(action, 0) + 1
|
||||||
log.info('seed_group %s -> %s', parent['id'],
|
log.info('seed_group %s -> %s', parent['id'],
|
||||||
{k: v for k, v in stat.items() if k != 'group'})
|
{k: v for k, v in stat.items() if k != 'group'})
|
||||||
return stat
|
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):
|
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']
|
pk_value = row['id']
|
||||||
|
module = _db_module(db)
|
||||||
existing = None
|
existing = None
|
||||||
try:
|
try:
|
||||||
rows = await db.read(table, {'id': pk_value})
|
existing = await _read_by_id(db, table, pk_value)
|
||||||
existing = rows[0] if rows else None
|
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
log.warning('读取 %s id=%s 失败(按不存在处理):%s', table, pk_value, exc)
|
log.warning('读取 %s id=%s 失败(按不存在处理):%s', table, pk_value, exc)
|
||||||
existing = None
|
existing = None
|
||||||
@ -434,7 +545,7 @@ async def _upsert_row(db, table, row, compare_fields):
|
|||||||
payload['created_at'] = stamp
|
payload['created_at'] = stamp
|
||||||
payload['updated_at'] = stamp
|
payload['updated_at'] = stamp
|
||||||
try:
|
try:
|
||||||
await db.create(table, payload)
|
await insert_row(table, payload, module=module)
|
||||||
return 'inserted'
|
return 'inserted'
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
raise DbError(message='注入 %s id=%s 失败:%s' % (table, pk_value, exc),
|
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'
|
return 'skipped'
|
||||||
diff['updated_at'] = stamp
|
diff['updated_at'] = stamp
|
||||||
try:
|
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
|
except Exception as exc: # noqa: BLE001
|
||||||
raise DbError(message='更新 %s id=%s 失败:%s' % (table, pk_value, exc),
|
raise DbError(message='更新 %s id=%s 失败:%s' % (table, pk_value, exc),
|
||||||
detail={'table': table, 'id': pk_value, 'diff': diff})
|
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'],
|
'inserted': totals['inserted'], 'updated': totals['updated'],
|
||||||
'skipped': totals['skipped'], 'per_group': per_group,
|
'skipped': totals['skipped'], 'per_group': per_group,
|
||||||
'dry_run': False, 'sqlor': sqlor_available(),
|
'dry_run': False, 'sqlor': sqlor_available(),
|
||||||
'dbname': db.dbname}
|
'dbname': _db_dbname(db)}
|
||||||
|
|
||||||
|
|
||||||
async def unseed_group(group_id, db=None, dbname=None):
|
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)
|
group = get_group(group_id)
|
||||||
db = db or get_db(module='pbl_appcodes', dbname=dbname)
|
db = db or get_db(module='pbl_appcodes', dbname=dbname)
|
||||||
parent, kvs = _plan_group(group)
|
parent, kvs = _plan_group(group)
|
||||||
|
module = _db_module(db)
|
||||||
removed = 0
|
removed = 0
|
||||||
|
# 写路径走 delete_row 原语(tenant_id 不传豁免租户打头),同 _upsert_row 口径。
|
||||||
|
# 共享编码表无 tenant_id 列,db.delete 门面会 fail-closed 抛 PBL_E_TENANT_MISSING。
|
||||||
for kv in kvs:
|
for kv in kvs:
|
||||||
removed += int(await db.delete(APPCODES_KV_TABLE, {'id': kv['id']}) or 0)
|
removed += int(await delete_row(APPCODES_KV_TABLE, {'id': kv['id']},
|
||||||
removed += int(await db.delete(APPCODES_TABLE, {'id': parent['id']}) or 0)
|
module=module) or 0)
|
||||||
|
removed += int(await delete_row(APPCODES_TABLE, {'id': parent['id']},
|
||||||
|
module=module) or 0)
|
||||||
return {'group': parent['id'], 'removed': removed}
|
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)
|
plan = dry_run_seed(path=path)
|
||||||
missing, mismatch, found = [], [], 0
|
missing, mismatch, found = [], [], 0
|
||||||
for parent in plan['parents']:
|
for parent in plan['parents']:
|
||||||
rows = await db.read(APPCODES_TABLE, {'id': parent['id']})
|
row = await _read_by_id(db, APPCODES_TABLE, parent['id'])
|
||||||
if not rows:
|
if not row:
|
||||||
missing.append('%s:%s' % (APPCODES_TABLE, parent['id']))
|
missing.append('%s:%s' % (APPCODES_TABLE, parent['id']))
|
||||||
continue
|
continue
|
||||||
found += 1
|
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']:
|
if str(row.get('name') or '') != parent['name']:
|
||||||
mismatch.append('%s:%s.name' % (APPCODES_TABLE, parent['id']))
|
mismatch.append('%s:%s.name' % (APPCODES_TABLE, parent['id']))
|
||||||
for kv in plan['kvs']:
|
for kv in plan['kvs']:
|
||||||
rows = await db.read(APPCODES_KV_TABLE, {'id': kv['id']})
|
row = await _read_by_id(db, APPCODES_KV_TABLE, kv['id'])
|
||||||
if not rows:
|
if not row:
|
||||||
missing.append('%s:%s' % (APPCODES_KV_TABLE, kv['id']))
|
missing.append('%s:%s' % (APPCODES_KV_TABLE, kv['id']))
|
||||||
continue
|
continue
|
||||||
found += 1
|
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']:
|
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']))
|
mismatch.append('%s:%s' % (APPCODES_KV_TABLE, kv['id']))
|
||||||
return {'ok': not missing and not mismatch, 'missing': missing,
|
return {'ok': not missing and not mismatch, 'missing': missing,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user