在pgsql中封装一个json函数,让它完全模拟mysql中的json_set

环境:

  • pgsql:PostgreSQL 18.0 on x86_64-windows, compiled by msvc-19.44.35215, 64-bit
  • mysql:8.4.6

问题:

本人在写从c#的表达式翻译到sql时,遇到 mysqlpgsql 在操作json时的差异,而这种差异导致操作 pgsql 很不顺畅,所以研究怎么在pgsql中写一个函数模拟mysql中的json_set的行为。

一、重点关注的行为:

行为1:如果目标是null,或jsonpath不匹配则不起作用

sql 复制代码
select json_set(null,'$.name','"jack"') -- 返回 null
select json_set('null','$.name','"jack"') -- 返回 'null'
select json_set('[]','$.name','"jack"') -- 返回 '[]'

行为2: 对于json null 会赋值,而不是省略

sql 复制代码
select json_set ('{}','$.name',null) -- 返回 {"name": null}

行为3:不会自动扩充深层对象

sql 复制代码
-- mysql
select json_set('{}','$.ext.name','jack') -- 返回 '{}'

--pgsql
select jsonb_set('{}'::jsonb,'{"ext","name"}','"jack"'::jsonb) -- 返回 {}
select jsonb_set('{}'::jsonb,'{"ext"}','"jack"'::jsonb) -- 返回 {"ext": "jack"}
select jsonb_set('{"ext":{}}'::jsonb,'{"ext"}','"jack"'::jsonb) -- 返回 {"ext": "jack"}

行为4:数组超出的在末尾追加

sql 复制代码
-- mysql:
select json_set('[]','$[3]','jack') -- 返回 ["jack"]
select json_set('["a","b"]','$[5]','jack') -- 返回 ["a", "b", "jack"]
-- pgsql
select jsonb_set('[]'::jsonb,'{3}','"jack"'::jsonb) -- 返回 ["jack"]

行为5:将目标当做数组赋值时

  • 如果目标属性不存在则不做任何改动
  • 如果目标属性存在(包括值为null)但不是数组
    • 如果要设置的索引大于0,则将原值和新值合并成一个数组
    • 如果要设置的索引等于0,则直接将父元素替换为这个新值
  • 如果目标属性存在且是数组则观察 行为4
sql 复制代码
-- mysql
select json_set('{}','$."ext"[5]','name') -- 返回 {}
select json_set('{"ext":null}','$."ext"[1]','name') --返回 {"ext": [null, "name"]}
select json_set('{"ext":2}','$."ext"[0]','name') -- 返回 {"ext": "name"}
select json_set('{"ext":2}','$."ext"[1]','name') -- 返回 {"ext": [2, "name"]}
select json_set('{"ext":{}}','$."ext"[1]','name') -- 返回 {"ext": [{}, "name"]}

-- pgsql
select jsonb_set('{}'::jsonb,'{"ext",5}','"name"'::jsonb) -- 返回 {}
select jsonb_set('{"ext":null}'::jsonb,'{"ext",1}','"name"'::jsonb) -- 返回(行为不一致) {"ext": null}
select jsonb_set('{"ext":2}'::jsonb,'{"ext",1}','"name"'::jsonb) -- 返回(行为不一致) {"ext": 2}
select jsonb_set('{"ext":{}}'::jsonb,'{"ext",1}','"name"'::jsonb) -- 返回(行为不一致) {"ext": {"1": "name"}}

二、基础函数:dbutil_jsonpath_to_tokens

将 jsonpath 转为 pgsql 中的数组

sql 复制代码
CREATE OR REPLACE FUNCTION dbutil_jsonpath_to_tokens(path text)
RETURNS text[]
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
    p text;
    tokens text[] := '{}';
    key text;
    idx text;
    i int;
    ch char;
    in_quote boolean;
    cur_key text;
BEGIN
    IF path IS NULL OR path = '' THEN
        RAISE EXCEPTION 'invalid json path';
    END IF;

    IF left(path, 1) <> '$' THEN
        RAISE EXCEPTION 'json path must start with $';
    END IF;

    p := substr(path, 2); -- 去掉开头的 $

    WHILE length(p) > 0 LOOP
        -- 处理点号
        IF left(p, 1) = '.' THEN
            p := substr(p, 2); -- 去掉点号

            -- 检查是否带引号的键名
            IF left(p, 1) = '"' THEN
                -- 解析带转义的双引号键名
                i := 2; -- 从第一个引号之后开始
                in_quote := true;
                cur_key := '';

                WHILE i <= length(p) AND in_quote LOOP
                    ch := substr(p, i, 1);
                    IF ch = '\' THEN
                        -- 转义字符:将下一个字符原样加入(无论是什么)
                        i := i + 1;
                        IF i > length(p) THEN
                            RAISE EXCEPTION 'incomplete escape at end of quoted key in path: %', path;
                        END IF;
                        ch := substr(p, i, 1);
                        cur_key := cur_key || ch;
                        i := i + 1;
                    ELSE
                        IF ch = '"' THEN
                            in_quote := false;
                            -- 结束引号,跳过它
                            p := substr(p, i + 1);
                        ELSE
                            cur_key := cur_key || ch;
                            i := i + 1;
                        END IF;
                    END IF;
                END LOOP;

                IF in_quote THEN
                    RAISE EXCEPTION 'unclosed quoted key in json path: %', path;
                END IF;

                key := cur_key;
            ELSE
                -- 普通键名(无引号),以点号或左括号为界
                key := regexp_replace(p, '^([^\.\[]+).*', '\1');
                p := substr(p, length(key) + 1);
            END IF;

            tokens := tokens || key;
            CONTINUE;
        END IF;

        -- 处理数组索引
        IF left(p, 1) = '[' THEN
            idx := regexp_replace(p, '^\[([0-9]+)\].*', '\1');
            tokens := tokens || idx;
            p := substr(p, length(idx) + 3); -- 跳过 [ 索引 ]
            CONTINUE;
        END IF;

        RAISE EXCEPTION 'invalid json path syntax: %', path;
    END LOOP;

    RETURN tokens;
END;
$$;

测试效果如下:

sql 复制代码
-- 注意: pgsql 中数组的写法如: select array['name']
select dbutil_jsonpath_to_tokens('$.name') -- 返回 ['name']
select dbutil_jsonpath_to_tokens('$."name".ext') -- 返回 ['name','ext']
select dbutil_jsonpath_to_tokens('$."na.me"') -- 返回 ['na.me']
select dbutil_jsonpath_to_tokens('$."na\"me"') -- 返回 ['na"me']
select dbutil_jsonpath_to_tokens('$."na[me"') -- 返回 ['na[me']

三、基础类型:dbutil_json_set_pair

sql 复制代码
create type dbutil_json_set_pair as ( path text, value jsonb );

四、最终函数:dbutil_json_set

sql 复制代码
create or replace function dbutil_json_set(
    target jsonb,
    variadic pairs dbutil_json_set_pair[]
)
returns jsonb
language plpgsql
as
$$
declare
    result jsonb := target;

    pair json_set_pair;

    path text;
    value jsonb;

    tokens text[];
    parent_path text[];
    last_token text;

    parent jsonb;
    idx int;
    isExistParent bool;
    isOpArray bool;
begin
    if target is null then
        return target;
    end if;

    if pairs is null then
        return result;
    end if;

    foreach pair in array pairs
    loop

        path := rtrim(pair.path);
        value := pair.value;

        tokens := dbutil_jsonpath_to_tokens(path);

        if array_length(tokens,1) = 1 then
            parent_path := '{}';
            last_token := tokens[1];
        else
            parent_path := tokens[1:array_length(tokens,1)-1];
            last_token := tokens[array_length(tokens,1)];
        end if;

        parent := result #> parent_path;
        isExistParent := parent is not null;

        if isExistParent then

            if last_token ~ '^[0-9]+$' then

                idx := last_token::int;
                isOpArray := right(path,1) = ']';

                if jsonb_typeof(parent) <> 'array' and isOpArray then

                    if coalesce(array_length(parent_path,1),0) = 0 then
                        if idx = 0 then
                            result := value;
                        else
                            result := jsonb_build_array(parent,value);
                        end if;
                    else
                        if idx = 0 then
                            result := jsonb_set(result,parent_path,value);
                        else
                            result := jsonb_set(result,parent_path,jsonb_build_array(parent,value));
                        end if;
                    end if;

                else

                    result := jsonb_set(
                        result,
                        tokens,
                        value,
                        true
                    );

                end if;

            else

                result := jsonb_set(
                    result,
                    tokens,
                    value,
                    true
                );

            end if;

        end if;

    end loop;

    return result;

end;
$$;

四、测试案例

4.1 常见正常操作

sql 复制代码
-- 返回 [NULL]
-- 如果目标对象为 NULL 则直接返回 NULL

-- mysql
select json_set(null,'$.name','jack')
-- pgsql
select dbutil_json_set(null,row('$.name','"jack"'))
sql 复制代码
-- 正常的简单操作

-- mysql 
select json_set('{}','$.name','jack')

-- pgsql
select dbutil_json_set('{}'::jsonb,row('$."name"','"jack"'))
select dbutil_json_set('{}',
	row('$.name','"jack"'),
	row('$.age','20'),
	row('$.book','null'),
	row('$.active','true'),
	row('$.books','["math","english"]')
)

4.2 意外边界处理

sql 复制代码
-- 返回 {}
-- 不会自动扩充深度属性

-- mysql
select json_set('{}','$.ext.name','jack')
-- pgsql
select dbutil_json_set('{}'::jsonb,row('$.ext.name','"jack"'))
select dbutil_json_set('{}'::jsonb,row('$."ext"."name"','"jack"'))
sql 复制代码
-- 返回 {"name": null}
-- 对于json null 会赋值,而不是省略

-- mysql
select json_set('{}','$.name',null)
-- pgsql
select dbutil_json_set('{}'::jsonb,row('$.name','null'))
sql 复制代码
-- 第一个 返回 [{}, "jack"]
-- 第二个 返回 [{"age": 20}, "jack"]
-- 对于目标是数组, 但父节点不是数组且父节点是顶层且目标索引大于0时

-- mysql
select json_set('{}','$[1]','jack')
select json_set('{"age":20}','$[1]','jack')
-- pgsql
select dbutil_json_set('{}'::jsonb,row('$[1]','"jack"'))
select dbutil_json_set('{"age":20}'::jsonb,row('$[1]','"jack"'))
sql 复制代码
-- 第一个和第二个都返回 "jack"
-- 对于目标是数组, 但父节点不是数组且父节点是顶层且目标索引等于0时

-- mysql
select json_set('{}','$[0]','jack')
select json_set('{"age":20}','$[0]','jack')

-- pgsql
select dbutil_json_set('{}'::jsonb,row('$[0]','"jack"'))
select dbutil_json_set('{"age":20}'::jsonb,row('$[0]','"jack"'))
sql 复制代码
-- 返回 {"ext": "name"}
-- 对于目标是数组, 但父节点不是数组且父节点不是顶层且目标索引等于0时

-- mysql
select json_set('{"ext":2}','$."ext"[0]','name') 

-- pgsql
select dbutil_json_set('{"ext":2}'::jsonb,row('$."ext"[0]','"name"'))
sql 复制代码
-- 返回 {"ext": [2, "name"]}
-- 对于目标是数组, 但父节点不是数组且父节点不是顶层且目标索引大于0时

-- mysql
select json_set('{"ext":2}','$."ext"[5]','name')

-- pgsql
select dbutil_json_set('{"ext":2}'::jsonb,row('$."ext"[5]','"name"'))

4.2 结合场景使用

sql 复制代码
select dbutil_json_set('{}',row('$.info',jsonb_path_query_first('{"info": "jack"}'::jsonb,'$.info'))) -- {"info": "jack"}
select dbutil_json_set('{}',row('$.info',jsonb_path_query_first('{"info": 20}'::jsonb,'$.info'))) -- {"info": 20}
select dbutil_json_set('{}',row('$.info',jsonb_path_query_first('{"info": null}'::jsonb, '$.info'))) -- {"info": null}
select dbutil_json_set('{}',row('$.info',jsonb_path_query_first('{"info": true}'::jsonb,'$.info'))) -- {"info": true}
select dbutil_json_set('{}',row('$.info',jsonb_path_query_first('{"info": []}'::jsonb,'$.info'))) -- {"info": []}
select dbutil_json_set('{}',row('$.info',jsonb_path_query_first('{"info": {}}'::jsonb,'$.info'))) -- {"info": {}}
相关推荐
冬夜戏雪2 小时前
【学习日记】
java·开发语言·数据库
邓草2 小时前
phpStudy v8.1 离线版一键安装包(小皮面板)
运维·服务器·mysql
LaughingZhu2 小时前
Product Hunt 每日热榜 | 2026-03-11
大数据·数据库·人工智能·经验分享·搜索引擎
2301_767902642 小时前
mysql语言
数据库·mysql·oracle
她说..2 小时前
Redis 中常用的操作方法
java·数据库·spring boot·redis·缓存
倔强的石头_3 小时前
MySQL 兼容性深度解析:从内核级优化到“零修改”迁移工程实践
前端·数据库
水杉i3 小时前
Redis 使用笔记
数据库·redis·笔记
学不完的3 小时前
redis
数据库·redis·缓存·运维开发
木与长清3 小时前
人鼠同源基因离线转换
数据库·矩阵·数据分析·r语言