mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 18:41:59 +00:00
8059cd5147
连接层 - 所有 SQLite 数据源关闭双引号字符串兼容(DQS),写错的双引号列名直接报错;设置失败就拒绝连接,启动时自检 - 连接缓存按数据源加版本快照分键、按源整体失效、最多 32 个;版本快照以 immutable 只读打开,建连接前核对文件哈希,并在借出连接时核对文件指纹,被改过就拒绝查询 - 结构探查遇到取不了列的视图时逐个对象重取,只跳过坏的那个并注明原因 解析(新 xlsx_scan + tabular) - 先流式扫描真实边界和版式(不信 dimension),再按边界读值,不整表进内存;按 Excel 自身上限设硬上限,并发解析最多两个 - 隐藏和深度隐藏的工作表不导入;稀疏区域、无缓存值的公式、同名工作表、DTD 拒收 - 去掉空行和两侧空列;列名表名统一规范化(新 names 模块)并保留原表头,撞名不再互相覆盖 - 前导零、超长数字按文本;千分位按数字;日期不带 T;带单位的数字文本写进回执 - 数字列混入非数字、交叉表和多块结构必须由用户决定(422),可选存空值或按原样导入(未规整);STRICT 建表,整份在一个事务里写完 版本底座(新 table_versions、raw_store) - 每次上传写新库文件、落盘、0444,事务里写构建、导入记录、快照后才切指针;失败时旧版完整可查 - 原件按哈希存档,不提供下载,可以清除;上传源不许改连接字段、不再重新探查;手工 SQLite 源不许指向上传目录和应用自己的数据文件(按文件身份判断) - 启动时清理半成品与孤儿文件、给老上传补初始版本;被运行引用的版本不回收,发起运行与回收互斥 - 上传接口的解析移到线程池 运行固定数据版本 - 发起时把图里(含子工作流)用到的上传源的当前快照记进运行记录和 run.started,续跑、审批恢复沿用;数据源工具按固定的快照生成描述、表结构并查询 - 固定的版本没了、源被删除后同名重建,都明确报错,不改用当前版本或同名的新源;查询快照记 data_version 证据层 - 新报告记 entity_syntax: 2,[[t:明细]]、[[c:明细.金额]] 和 SQL 里不加引号的中文表名都认得,名字按 SQLite 的口径查找;老报告按原规则复核,结果不变 - 写作规则改为中文表名、字段名一律用 [[t:]] / [[c:]];未规整的表在工具描述、写作目录和裁判摘录里标出 前端 - 上传弹窗接上导入前的决定(混合列、交叉表)和导入回执(跳过的隐藏表、类型转换、警告、原表头对照、未规整标记),上传前告知原件会保存 - 数据源卡片按 origin 认上传的表格,显示当前版本;上传的表格不再给「探查结构」
77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""名字规范化:导入和证据层共用的口径。"""
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
from app.data.names import canon, collide_key, name_key, name_problem, to_sql_name
|
||
|
||
|
||
@pytest.mark.parametrize(("raw", "expected"), [
|
||
("8-9", "8-9"),
|
||
("8–9", "8-9"), # en dash
|
||
("8—9", "8-9"), # em dash
|
||
("8−9", "8-9"), # 减号
|
||
("8-9", "8-9"), # 全角
|
||
(" 18-22 时合计 ", "18-22 时合计"),
|
||
("金额", "金额"), # 零宽空格(Cf)
|
||
("(人次)", "(人次)"),
|
||
])
|
||
def test_canon(raw, expected):
|
||
assert canon(raw) == expected
|
||
|
||
|
||
@pytest.mark.parametrize("name", ["日客流", "sales", "Sales_2026", "时段客流_表内合计", "a"])
|
||
def test_good_names(name):
|
||
assert name_problem(name) is None
|
||
|
||
|
||
@pytest.mark.parametrize("name", [
|
||
"", "日客流ㅤ", "ABC", "①期", "a b", "2026销售", "金额(元)", "sqlite_master", "select",
|
||
"x" * 49, "ab", "é",
|
||
])
|
||
def test_bad_names(name):
|
||
assert name_problem(name)
|
||
|
||
|
||
def test_name_key_matches_sqlite():
|
||
"""name_key 和 SQLite 比较标识符完全一致:只对 ASCII 不区分大小写,不做 NFKC。逐对拿真的 SQLite 对照。"""
|
||
import sqlite3
|
||
|
||
pairs = [("ABC", "abc", True), ("Дата", "дата", False), ("ABC", "abc", False), ("明细", "明细", True),
|
||
("Orders", "orders", False), ("Sales_2026", "SALES_2026", True)]
|
||
for a, b, same in pairs:
|
||
db = sqlite3.connect(":memory:")
|
||
db.execute(f'CREATE TABLE "{a}" (x)')
|
||
try:
|
||
db.execute(f'CREATE TABLE "{b}" (y)')
|
||
sqlite_same = False
|
||
except sqlite3.OperationalError: # table … already exists:SQLite 认为是同一个名字
|
||
sqlite_same = True
|
||
db.close()
|
||
assert sqlite_same is same, (a, b)
|
||
assert (name_key(a) == name_key(b)) is same, (a, b)
|
||
|
||
|
||
def test_collide_key_is_stricter():
|
||
assert collide_key("Дата") == collide_key("дата")
|
||
assert collide_key("ABC") == collide_key("abc")
|
||
|
||
|
||
@pytest.mark.parametrize(("header", "expected"), [
|
||
("金额(元)", "金额_元"),
|
||
("销量\n(件)", "销量_件"),
|
||
("单价[元]", "单价_元"),
|
||
("Unit Price", "Unit_Price"),
|
||
("2026年", "c_2026年"),
|
||
("order", "order_"),
|
||
("sqlite_x", "t_sqlite_x"),
|
||
("", "col_3"),
|
||
(None, "col_3"),
|
||
("()", "col_3"),
|
||
("全日客流(人次)", "全日客流_人次"),
|
||
])
|
||
def test_to_sql_name(header, expected):
|
||
out = to_sql_name(header, fallback="col_3")
|
||
assert out == expected
|
||
assert name_problem(out) is None
|