diff --git a/flake.lock b/flake.lock index ca935bd..704715e 100644 --- a/flake.lock +++ b/flake.lock @@ -18,16 +18,16 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1764521362, - "narHash": "sha256-M101xMtWdF1eSD0xhiR8nG8CXRlHmv6V+VoY65Smwf4=", + "lastModified": 1780145889, + "narHash": "sha256-md0zn0RnwNvPyASas1yG5YUuwQ4ALA6ucL50l0DvqCo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "871b9fd269ff6246794583ce4ee1031e1da71895", + "rev": "8c50a710ddca43d7a530fb805ad55bde8d0141c5", "type": "github" }, "original": { "owner": "NixOS", - "ref": "25.11", + "ref": "26.05", "repo": "nixpkgs", "type": "github" } @@ -43,16 +43,16 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1785529904, - "narHash": "sha256-DDIeJCHRtmvB7yocWTo1MISIGf0DsUmyLw6ISuEV4e0=", + "lastModified": 1791394750, + "narHash": "sha256-SMv4EKUaGmfUyXyeGHW3AiX1vbef7El56RKTIxio8jQ=", "owner": "supabase", "repo": "xpg", - "rev": "e7d47eb58f19168b5facc6b78a64f6cc7f04bbcb", + "rev": "8f2a2005e8e020e0db5a7cbb94a87a9dd3227411", "type": "github" }, "original": { "owner": "supabase", - "ref": "v2.5.0", + "ref": "v2.6.0", "repo": "xpg", "type": "github" } diff --git a/flake.nix b/flake.nix index e66b190..f34b991 100644 --- a/flake.nix +++ b/flake.nix @@ -4,8 +4,9 @@ inputs = { # 2025-11-13 nixpkgs.url = "github:NixOS/nixpkgs/91c9a64ce2a84e648d0cf9671274bb9c2fb9ba60"; + xpg = { - url = "github:supabase/xpg/v2.5.0"; + url = "github:supabase/xpg/v2.6.0"; }; }; diff --git a/nix/plpgsql-check.nix b/nix/plpgsql-check.nix new file mode 100644 index 0000000..3031ce4 --- /dev/null +++ b/nix/plpgsql-check.nix @@ -0,0 +1,40 @@ +{ + lib, + stdenv, + fetchFromGitHub, + postgresql, +}: + +stdenv.mkDerivation rec { + pname = "plpgsql-check"; + version = "2.10.15"; + + src = fetchFromGitHub { + owner = "okbob"; + repo = "plpgsql_check"; + rev = "v${version}"; + hash = "sha256-war+GVHMDegfDXjxdIsUkrPPDntKeyb/bGxibHVbpDo="; + }; + + buildInputs = [ postgresql ]; + + installPhase = '' + install -D -t $out *${postgresql.dlSuffix} + install -D -t $out *.sql + install -D -t $out *.control + + # Needed for postgres 18+ native implementation of extension_search_path + mkdir $out/extension + install -D -t $out/extension *.sql + install -D -t $out/extension *.control + ''; + + meta = with lib; { + description = "Linter tool for language PL/pgSQL"; + homepage = "https://github.com/okbob/plpgsql_check"; + changelog = "https://github.com/okbob/plpgsql_check/releases/tag/v${version}"; + platforms = postgresql.meta.platforms; + license = licenses.mit; + maintainers = [ maintainers.marsam ]; + }; +} diff --git a/shell.nix b/shell.nix index b46044e..0694c07 100644 --- a/shell.nix +++ b/shell.nix @@ -19,6 +19,25 @@ in cassert ? true, }: let + plpgsqlcheck14 = pkgs.callPackage ./nix/plpgsql-check.nix { + postgresql = xpgPkgs.postgresql_14; + }; + plpgsqlcheck15 = pkgs.callPackage ./nix/plpgsql-check.nix { + postgresql = xpgPkgs.postgresql_15; + }; + plpgsqlcheck16 = pkgs.callPackage ./nix/plpgsql-check.nix { + postgresql = xpgPkgs.postgresql_16; + }; + plpgsqlcheck17 = pkgs.callPackage ./nix/plpgsql-check.nix { + postgresql = xpgPkgs.postgresql_17; + }; + plpgsqlcheck18 = pkgs.callPackage ./nix/plpgsql-check.nix { + postgresql = xpgPkgs.postgresql_18; + }; + plpgsqlcheck19 = pkgs.callPackage ./nix/plpgsql-check.nix { + postgresql = xpgPkgs.postgresql_19; + }; + style = pkgs.writeShellScriptBin "vault-style" '' ${pkgs.clang-tools}/bin/clang-format -i src/* ${pkgs.findutils}/bin/find . -name '*.nix' -exec ${pkgs.nixfmt}/bin/nixfmt {} \+ @@ -34,16 +53,22 @@ pkgs.mkShell { (xpgPkgs.xpg.withExtensions { extensions = { "14" = [ + plpgsqlcheck14 ]; "15" = [ + plpgsqlcheck15 ]; "16" = [ + plpgsqlcheck16 ]; "17" = [ + plpgsqlcheck17 ]; "18" = [ + plpgsqlcheck18 ]; "19" = [ + plpgsqlcheck19 ]; }; }) diff --git a/sql/supabase_vault--0.3.1--0.3.2.sql b/sql/supabase_vault--0.3.1--0.3.2.sql index 9d6d736..78d4ea4 100644 --- a/sql/supabase_vault--0.3.1--0.3.2.sql +++ b/sql/supabase_vault--0.3.1--0.3.2.sql @@ -1,3 +1,33 @@ -- Search pg_temp last so callers can't shadow types (e.g. text, bytea) with temp objects that run as the function owner ALTER FUNCTION vault.create_secret(text, text, text, uuid) SET search_path = pg_catalog, pg_temp; -ALTER FUNCTION vault.update_secret(uuid, text, text, text, uuid) SET search_path = pg_catalog, pg_temp; + +CREATE OR REPLACE FUNCTION vault.update_secret( + secret_id uuid, + new_secret text = NULL, + new_name text = NULL, + new_description text = NULL, + -- unused + new_key_id uuid = NULL +) +RETURNS void +SECURITY DEFINER +LANGUAGE plpgsql +SET search_path = 'pg_catalog, pg_temp' +AS $$ +BEGIN + UPDATE vault.secrets s + SET + secret = CASE WHEN new_secret IS NULL THEN s.secret + ELSE encode(vault._crypto_aead_det_encrypt( + message := convert_to(new_secret, 'utf8'), + additional := convert_to(s.id::text, 'utf8'), + key_id := 0, + context := 'pgsodium'::bytea, + nonce := s.nonce + ), 'base64') END, + name = coalesce(new_name, s.name), + description = coalesce(new_description, s.description), + updated_at = now() + WHERE s.id = secret_id; +END +$$; diff --git a/test/expected/test.out b/test/expected/test.out index ca6e401..1517987 100644 --- a/test/expected/test.out +++ b/test/expected/test.out @@ -119,3 +119,34 @@ WITH security_definer_fn_settings AS ( -------------+---------------+----------------- (0 rows) +-- below query is sourced from from pgsql_check's git repo, link below +-- https://github.com/okbob/plpgsql_check/blob/0ac96adc13b34d071c4be8ba0ec4f73e6e7520f5/README.md?plain=1#L310 +-- check all plpgsql functions (functions or trigger functions with defined triggers) +SELECT + (pcf).functionid::regprocedure, (pcf).lineno, (pcf).statement, + (pcf).sqlstate, (pcf).message, (pcf).detail, (pcf).hint, (pcf).level, + (pcf)."position", (pcf).query, (pcf).context +FROM +( + SELECT + plpgsql_check_function_tb(pg_proc.oid, COALESCE(pg_trigger.tgrelid, 0), + oldtable=>pg_trigger.tgoldtable, + newtable=>pg_trigger.tgnewtable) AS pcf + FROM pg_proc + LEFT JOIN pg_trigger + ON (pg_trigger.tgfoid = pg_proc.oid) + WHERE + prolang = (SELECT lang.oid FROM pg_language lang WHERE lang.lanname = 'plpgsql') AND + pronamespace <> (SELECT nsp.oid FROM pg_namespace nsp WHERE nsp.nspname = 'pg_catalog') AND + -- ignore unused triggers + (pg_proc.prorettype <> (SELECT typ.oid FROM pg_type typ WHERE typ.typname = 'trigger') OR + pg_trigger.tgfoid IS NOT NULL) + OFFSET 0 +) ss +ORDER BY (pcf).functionid::regprocedure::text, (pcf).lineno; + functionid | lineno | statement | sqlstate | message | detail | hint | level | position | query | context +-----------------------------------------------+--------+-----------+----------+-------------------------------+--------+------+---------------+----------+-------+--------- + vault.create_secret(text,text,text,uuid) | | | 00000 | unused parameter "new_key_id" | | | warning extra | | | + vault.update_secret(uuid,text,text,text,uuid) | | | 00000 | unused parameter "new_key_id" | | | warning extra | | | +(2 rows) + diff --git a/test/init.sql b/test/init.sql index 4bd8803..2943d18 100644 --- a/test/init.sql +++ b/test/init.sql @@ -1,5 +1,6 @@ CREATE ROLE bob login password 'bob'; +CREATE EXTENSION plpgsql_check; CREATE EXTENSION supabase_vault CASCADE; GRANT USAGE ON SCHEMA vault TO bob WITH GRANT OPTION; diff --git a/test/sql/test.sql b/test/sql/test.sql index 778bbb8..f9aa172 100644 --- a/test/sql/test.sql +++ b/test/sql/test.sql @@ -97,3 +97,29 @@ WITH security_definer_fn_settings AS ( WHERE array_position(s.search_path_ary, 'pg_temp') < array_position(s.search_path_ary, 'pg_catalog') AND array_position(s.search_path_ary, 'pg_temp') IS NOT NULL AND array_position(s.search_path_ary, 'pg_catalog') IS NOT NULL; + +-- below query is sourced from from pgsql_check's git repo, link below +-- https://github.com/okbob/plpgsql_check/blob/0ac96adc13b34d071c4be8ba0ec4f73e6e7520f5/README.md?plain=1#L310 +-- check all plpgsql functions (functions or trigger functions with defined triggers) +SELECT + (pcf).functionid::regprocedure, (pcf).lineno, (pcf).statement, + (pcf).sqlstate, (pcf).message, (pcf).detail, (pcf).hint, (pcf).level, + (pcf)."position", (pcf).query, (pcf).context +FROM +( + SELECT + plpgsql_check_function_tb(pg_proc.oid, COALESCE(pg_trigger.tgrelid, 0), + oldtable=>pg_trigger.tgoldtable, + newtable=>pg_trigger.tgnewtable) AS pcf + FROM pg_proc + LEFT JOIN pg_trigger + ON (pg_trigger.tgfoid = pg_proc.oid) + WHERE + prolang = (SELECT lang.oid FROM pg_language lang WHERE lang.lanname = 'plpgsql') AND + pronamespace <> (SELECT nsp.oid FROM pg_namespace nsp WHERE nsp.nspname = 'pg_catalog') AND + -- ignore unused triggers + (pg_proc.prorettype <> (SELECT typ.oid FROM pg_type typ WHERE typ.typname = 'trigger') OR + pg_trigger.tgfoid IS NOT NULL) + OFFSET 0 +) ss +ORDER BY (pcf).functionid::regprocedure::text, (pcf).lineno;