Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/scripts/wait-for-postgres.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
until pg_isready &>/dev/null; do
sleep 1
done
18 changes: 11 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@ on:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
concurrency:
group: ${{ github.workflow }}-${{ matrix.pg }}-${{ github.ref }}
cancel-in-progress: true
strategy:
matrix:
pg:
- v17
- default
- postgresql_17
- postgresql_16
- postgresql_15
- postgresql_14
runs-on: namespace-profile-cached-amd64-lg
steps:
- uses: actions/checkout@v4
Expand All @@ -34,6 +38,6 @@ jobs:
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- run: nix flake check
- run: nix build .#${{ matrix.pg }}
- run: nix run .#devenv -- up -D
- run: nix develop -c pg_prove test.sql
- run: nix run .#devenv down
- run: nix run .#${{ matrix.pg }}-compose -- up -D && nix develop .#${{ matrix.pg }} -c ./.github/scripts/wait-for-postgres.sh
- run: nix develop .#${{ matrix.pg }} -c pg_prove test.sql
- run: nix run .#${{ matrix.pg }}-compose down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ select jsonb_patch(
(1 row)
```

See [./test.sql] for more examples.
Compatible (I think) with PostgreSQL 14+.
See [./test.sql](./test.sql) for more examples.
I have not yet written tests for A.11-A.15 from the RFC.
76 changes: 39 additions & 37 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -35,50 +35,52 @@
pkgs,
system,
...
}: {
devShells.default = pkgs.mkShell {
name = "pg_jsonpatch";
buildInputs = with pkgs; [
postgresql
perlPackages.TAPParserSourceHandlerpgTAP # pg_prove
];
PGDATA = "data/default";
PGHOST = "localhost";
PGDATABASE = "postgres";
};

}: let
packages = {
default = pkgs.callPackage ./package.nix {inherit (pkgs) postgresql;};
v17 = pkgs.callPackage ./package.nix {postgresql = pkgs.postgresql_17;};
v16 = pkgs.callPackage ./package.nix {postgresql = pkgs.postgresql_16;};
v15 = pkgs.callPackage ./package.nix {postgresql = pkgs.postgresql_15;};
v14 = pkgs.callPackage ./package.nix {postgresql = pkgs.postgresql_14;};
postgresql_17 = pkgs.callPackage ./package.nix {postgresql = pkgs.postgresql_17;};
postgresql_16 = pkgs.callPackage ./package.nix {postgresql = pkgs.postgresql_16;};
postgresql_15 = pkgs.callPackage ./package.nix {postgresql = pkgs.postgresql_15;};
postgresql_14 = pkgs.callPackage ./package.nix {postgresql = pkgs.postgresql_14;};
};
in {
devShells = builtins.mapAttrs (name: drv:
pkgs.mkShell {
name = "pg_jsonpatch";
buildInputs = [
drv.passthru.postgresql
pkgs.perlPackages.TAPParserSourceHandlerpgTAP # pg_prove
];
PGDATA = "data/${name}";
PGHOST = "localhost";
PGDATABASE = "postgres";
})
packages;

process-compose.devenv = {
imports = [
inputs.services.processComposeModules.default
];
inherit packages;

cli.options.no-server = false;
process-compose = lib.mapAttrs' (name: drv:
lib.nameValuePair "${name}-compose" {
imports = [
inputs.services.processComposeModules.default
];

services.postgres.default = {
enable = true;
extensions = exts:
with exts; [
config.packages.default
pgtap
];
initialScript.after = ''
create extension pgtap;
'';
package = config.packages.default.postgresql;
settings = {
log_statement = "all";
logging_collector = false;
cli.options.no-server = false;

services.postgres.${name} = {
enable = true;
extensions = exts: [drv exts.pgtap];
initialScript.after = ''
create extension pgtap;
'';
package = drv.passthru.postgresql;
settings = {
log_statement = "all";
logging_collector = false;
};
};
};
};
})
packages;
};
};
}
101 changes: 38 additions & 63 deletions pg_jsonpatch--1.0.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,20 @@
create or replace function jsonb_patch_add(target jsonb, path text[], value jsonb)
returns jsonb
as $$
with x as (
select jsonb_typeof(jsonb_extract_path(target, variadic trim_array(path, 1))) as t
)
select jsonb_set(target, path, value, true)
from x
where x.t = 'object'
union all
select jsonb_insert(target, path, value)
from x
where x.t = 'array' and path[array_length(path, 1)] is distinct from '-'
union all
select jsonb_insert(target, trim_array(path, 1) || array['-1'], value, true)
from x
where x.t = 'array' and path[array_length(path, 1)] = '-'
limit 1
$$
language sql
declare
target_type text := jsonb_typeof(jsonb_extract_path(target, variadic trim_array(path, 1)));
begin
case
when target_type = 'object'
then return jsonb_set(target, path, value, true);
when target_type = 'array' and path[array_length(path, 1)] = '-'
then return jsonb_insert(target, trim_array(path, 1) || array['-1'], value, true);
when target_type = 'array'
then return jsonb_insert(target, path, value);
else return null;
end case;
end $$
language plpgsql
immutable;

create or replace function jsonb_patch_remove(target jsonb, path text[])
Expand All @@ -36,22 +33,13 @@ immutable;

create or replace function jsonb_patch_move(target jsonb, _from text[], path text[])
returns jsonb
as $$
with old as (
select jsonb_extract_path(target, variadic _from) as value
)

select @extschema@.jsonb_patch_add(@extschema@.jsonb_patch_remove(target, _from), path, value)
from old
$$
as $$ select @extschema@.jsonb_patch_add(@extschema@.jsonb_patch_remove(target, _from), path, jsonb_extract_path(target, variadic _from)) $$
language sql
immutable;

create or replace function jsonb_patch_copy(target jsonb, _from text[], path text[])
returns jsonb
as $$
select @extschema@.jsonb_patch_add(target, path, jsonb_extract_path(target, variadic _from))
$$
as $$ select @extschema@.jsonb_patch_add(target, path, jsonb_extract_path(target, variadic _from)) $$
language sql
immutable;

Expand All @@ -67,53 +55,40 @@ as $$ select array_remove(string_to_array(path, '/'), '') $$
language sql
immutable;

-- Apply a single patch operation to an object.
create or replace function jsonb_patch_apply(target jsonb, patch jsonb)
returns jsonb
as $$
declare
op text := patch->>'op';
path text[] := @extschema@.jsonb_patch_split_path(patch->>'path');
rv jsonb;
begin
case patch->>'op'
when 'add' then rv := @extschema@.jsonb_patch_add(target, path, patch->'value');
when 'remove' then rv := @extschema@.jsonb_patch_remove(target, path);
when 'replace' then rv := @extschema@.jsonb_patch_replace(target, path, patch->'value');
when 'move' then rv := @extschema@.jsonb_patch_move(target, @extschema@.jsonb_patch_split_path(patch->>'from'), path);
when 'copy' then rv := @extschema@.jsonb_patch_copy(target, @extschema@.jsonb_patch_split_path(patch->>'from'), path);
when 'test' then rv := @extschema@.jsonb_patch_test(target, path, patch->'value');
when 'add' then return @extschema@.jsonb_patch_add(target, path, patch->'value');
when 'remove' then return @extschema@.jsonb_patch_remove(target, path);
when 'replace' then return @extschema@.jsonb_patch_replace(target, path, patch->'value');
when 'move' then return @extschema@.jsonb_patch_move(target, @extschema@.jsonb_patch_split_path(patch->>'from'), path);
when 'copy' then return @extschema@.jsonb_patch_copy(target, @extschema@.jsonb_patch_split_path(patch->>'from'), path);
when 'test' then return @extschema@.jsonb_patch_test(target, path, patch->'value');
else return null;
end case;

return rv;
end $$
language plpgsql
immutable;

create or replace function jsonb_coalesce(variadic jsonb[])
returns jsonb
as $$
select value
from unnest($1) as value
where value is not null and jsonb_typeof(value) != 'null'
limit 1
$$
language sql
immutable;

create or replace function jsonb_patch_agg(target jsonb, patch jsonb, base jsonb)
returns jsonb
as $$ select @extschema@.jsonb_patch_apply(@extschema@.jsonb_coalesce(target, base), patch) $$
language sql
immutable;

create or replace aggregate jsonb_patch_agg(jsonb, jsonb) (
sfunc = @extschema@.jsonb_patch_agg,
stype = jsonb,
initcond = null
);

create or replace function jsonb_patch(target jsonb, patches jsonb)
returns jsonb
as $$ select jsonb_patch_agg(value, target) from jsonb_array_elements(patches) $$
language sql
as $$
declare
patch jsonb;
result jsonb := target;
begin
for patch in select * from jsonb_array_elements(patches) loop
result := @extschema@.jsonb_patch_apply(result, patch);
if result is null then
return null;
end if;
end loop;
return result;
end $$
language plpgsql
immutable;
3 changes: 2 additions & 1 deletion test.sql
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ select
from (
values
('{"op":"replace","path":"/a/b/c","value":42}'::jsonb),
('{"op":"test","path":"/a/b/c","value":"C"}')
('{"op":"test","path":"/a/b/c","value":"C"}'),
('{"op":"add","path":"/a/b/d","value":"foo"}'::jsonb)
) as patches(patch);

select plan(count(*)::int) from test_cases;
Expand Down