SQLite json_set 路径边界:缺少父对象能补上,父位置是 null 为什么却不更新

前天 4阅读

一批配置里有的记录没有 prefs,有的把 prefs 写成 JSON null。给它们统一执行 json_set(doc, '$.prefs.theme', 'dark'),结果前者增加了对象,后者仍然原样返回。函数能创建缺失内容,并不意味着会把路径途中已经存在的任意值强制改成对象。

JSON 的缺失成员与值为 null 的成员有不同结构。已有数字、数组等也不能自动当成同一种父对象。更新配置之前,应先决定遇到旧类型时是拒绝、迁移还是覆盖;成功得到一段合法 JSON,并不能单独证明期望字段已经写进去。

可直接运行的对照实验

下面用 Python 标准库驱动内存 SQLite,保存为 demo.py 运行。代码在 SQLite 3.53.1 验证,启动时先试调用 json,确认当前构建提供所需能力。输入与路径均作为绑定参数传入,结果用 Python JSON 解析后比较结构,避免受输出排版影响。

SQLite json_set 路径边界:缺少父对象能补上,父位置是 null 为什么却不更新

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 中独立运行。

参考资料

SQLite 官方文档:JSON 修改与路径规则

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。