SQLite json_set 路径边界:缺少父对象能补上,父位置是 null 为什么却不更新
一批配置里有的记录没有 prefs,有的把 prefs 写成 JSON null。给它们统一执行 json_set(doc, '$.prefs.theme', 'dark'),结果前者增加了对象,后者仍然原样返回。函数能创建缺失内容,并不意味着会把路径途中已经存在的任意值强制改成对象。
JSON 的缺失成员与值为 null 的成员有不同结构。已有数字、数组等也不能自动当成同一种父对象。更新配置之前,应先决定遇到旧类型时是拒绝、迁移还是覆盖;成功得到一段合法 JSON,并不能单独证明期望字段已经写进去。
可直接运行的对照实验
下面用 Python 标准库驱动内存 SQLite,保存为 demo.py 运行。代码在 SQLite 3.53.1 验证,启动时先试调用 json,确认当前构建提供所需能力。输入与路径均作为绑定参数传入,结果用 Python JSON 解析后比较结构,避免受输出排版影响。
AI概念插图:可下钻的空容器能够接入新分支,已占位的实心值需要先处理结构。图片不是运行截图,也不表示实测性能。
import json
import sqlite3
con = sqlite3.connect(":memory:")
try:
assert con.execute("SELECT json('{}')").fetchone()[0] == "{}"
samples = [{}, {"prefs": None}, {"prefs": 7}, {"prefs": []}]
for sample in samples:
text = json.dumps(sample)
output = con.execute("SELECT json_set(?, ?, ?)",
(text, "$.prefs.theme", "dark")).fetchone()[0]
result = json.loads(output)
expected = {"prefs": {"theme": "dark"}} if sample == {} else sample
assert result == expected
target_type = con.execute("SELECT json_type(?, ?)",
(output, "$.prefs.theme")).fetchone()[0]
print("input:", text, "target type:", target_type)
gap = con.execute("SELECT json_set('[]', '$[2]', 9)").fetchone()[0]
end = con.execute("SELECT json_set('[]', '$[#]', 9)").fetchone()[0]
assert json.loads(gap) == [] and json.loads(end) == [9]
print("array gap / append:", gap, end)
changed = con.execute("""
SELECT json_set('{"prefs":null}',
'$.prefs', json('{}'), '$.prefs.theme', 'dark')
""").fetchone()[0]
assert json.loads(changed) == {"prefs": {"theme": "dark"}}
print("explicit parent conversion:", changed)
finally:
con.close()区分没有父成员与父成员不能下钻
第一组空对象得到 {"prefs":{"theme":"dark"}}。其余三组的 prefs 分别是 null、整数和数组,更新后结构都保持原样,target type 输出 None,表示目标路径没有值。这里的 None 来自 SQL NULL,不能与 json_type 返回的文字 null 混为一谈。
这些是本次版本和样本上的实测行为。若业务必须确保更新成功,可以在同一业务操作中读回 json_type 与目标值进行核对;仅检查执行没有抛异常,容易把未发生的结构修改报成成功。批量迁移还应统计每种原始类型,先用代表样本确定处理政策。
数组不会自动补出任意空洞
第二部分在空数组的索引二设置数值,仍得到空数组;使用末尾位置 $[#] 才追加出单个元素。这说明数组索引不是可以随意跳过的对象字段名。若调用方拿到了很大的编号,不能直接把它塞进数组路径并期待中间自动补齐。
对象键和数组位置需要分别建模。若编号本身是稀疏身份标识,用明确的对象映射或关系表通常更容易表达;若数据确实按连续位置排列,就应检查索引与当前长度的关系。例子没有给输入做通用路径拼接,实际接口应限制允许更新的字段。
先处理父类型,再改孩子
最后先把已有 null 的 prefs 显式设置为 JSON 对象,再在同一次调用中写入 theme。路径和值的配对按从左到右依次处理,所以后一个编辑能够看到前一个编辑建立的父对象。顺序是操作语义的一部分,重排参数可能改变结果。
这里使用 json('{}') 表达对象,目的是执行已决定的结构迁移。不要把这种强制替换作为所有异常类型的默认修复:旧 prefs 若含有其他有效成员,直接替换可能丢失它们。实际迁移应按原始类型分支,保存必要数据,再验证最终契约。
测试需要覆盖字段缺失、JSON null、合法对象、错误标量以及数组越界。把修改后的目标值、目标类型和未涉及字段一起断言,能发现静默不变与过度覆盖两种相反问题。示例只在内存里改变自造数据,没有执行生产迁移,也不提供并发更新完整方案。
资料核对日期:2026年10月2日(北京时间)。代码在 CPython 3.12.14、SQLite 3.53.1 中独立运行。


