diff --git a/.github/scripts/wait-for-postgres.sh b/.github/scripts/wait-for-postgres.sh new file mode 100755 index 0000000..41d6275 --- /dev/null +++ b/.github/scripts/wait-for-postgres.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +until pg_isready &>/dev/null; do + sleep 1 +done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a8eef5..57f18a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/README.md b/README.md index 2de11f4..b578a52 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/flake.nix b/flake.nix index c8c9b55..5be1e59 100644 --- a/flake.nix +++ b/flake.nix @@ -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; }; }; } diff --git a/pg_jsonpatch--1.0.0.sql b/pg_jsonpatch--1.0.0.sql index 9e742b4..61b64db 100644 --- a/pg_jsonpatch--1.0.0.sql +++ b/pg_jsonpatch--1.0.0.sql @@ -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[]) @@ -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; @@ -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; diff --git a/test.sql b/test.sql index 1e92f3e..0fa7813 100644 --- a/test.sql +++ b/test.sql @@ -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;