From dca559ee4369a69d3497ea47fb99bbdd23787f85 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 14:10:36 -0500 Subject: [PATCH 01/11] Adapt to pgxntool 2.4.0 so `make test` runs and passes on PG17 The old pgxntool passed `--load-language=plpgsql` to pg_regress, which PG17 rejects, and ignored installcheck errors, so `make test` exited 0 without running anything. pgxntool 2.4.0 runs the suite and fails on regressions, which surfaced: - Makefile: drop the local asciidoc rules; `base.mk` now builds and installs docs itself, and keeping both made `make install` fail with "will not overwrite just-created". - test/expected: remove the `base_94*`, `base_95*` and `base_961` variant files, which have no matching `test/sql` file and fail pgxntool's stale-expected check. Regenerated the rest with `make results`: the installed trunklet now emits a `function__arg_types_text()` deprecation warning on load, and pgTap numbers the `threw` checks in `base` in a different order. - sql/trunklet-format--0.2.0.sql: pgxntool now tracks the current version's generated install script instead of ignoring it. Co-Authored-By: Claude Sonnet 5.5 --- Makefile | 15 -- sql/trunklet-format--0.2.0.sql | 286 +++++++++++++++++++++++++++++++++ test/expected/base.out | 32 +++- test/expected/base_94.out | 94 ----------- test/expected/base_941.out | 94 ----------- test/expected/base_95.out | 94 ----------- test/expected/base_951.out | 94 ----------- test/expected/base_961.out | 97 ----------- test/expected/build.out | 16 ++ test/expected/reload.out | 16 ++ test/expected/search_path.out | 32 ++++ 11 files changed, 374 insertions(+), 496 deletions(-) create mode 100644 sql/trunklet-format--0.2.0.sql delete mode 100644 test/expected/base_94.out delete mode 100644 test/expected/base_941.out delete mode 100644 test/expected/base_95.out delete mode 100644 test/expected/base_951.out delete mode 100644 test/expected/base_961.out diff --git a/Makefile b/Makefile index 44803f2..5b353bf 100644 --- a/Makefile +++ b/Makefile @@ -5,21 +5,6 @@ testdeps: $(TEST_SQL_FILES) $(TEST_SOURCE_FILES) testdeps: $(TESTDIR)/deps.sql $(TESTDIR)/load.sql -# -# Docs -# -ifeq (,$(ASCIIDOC)) -ASCIIDOC = $(shell which asciidoc) -endif # ASCIIDOC -ifneq (,$(ASCIIDOC)) -DOCS_built := $(DOCS:.asc=.html) $(DOCS:.adoc=.html) -DOCS += $(DOCS_built) - -install: $(DOCS_built) -%.html: %.asc - asciidoc $< -endif # ASCIIDOC - # # OTHER DEPS # diff --git a/sql/trunklet-format--0.2.0.sql b/sql/trunklet-format--0.2.0.sql new file mode 100644 index 0000000..606bca1 --- /dev/null +++ b/sql/trunklet-format--0.2.0.sql @@ -0,0 +1,286 @@ +/* DO NOT EDIT - AUTO-GENERATED FILE */ +SELECT extension_drop__add( + 'trunklet-format' + -- WARNING! Our language name is format, not trunklet-format! + , $sql$SELECT trunklet.template_language__remove('format', ignore_missing_functions := true)$sql$ +) + /* + * Only register the drop if we're being run as part of extension creation. + * It might look like this would be bad if the extension *already* existed + * before we ran, but in that case we're going to get an error from __add(). + */ + WHERE EXISTS( SELECT 1 FROM pg_extension WHERE extname = 'trunklet-format' ) +; + +/* +CREATE FUNCTION _trunklet._extension_drop__format( +) RETURNS event_trigger LANGUAGE plpgsql AS $body$ +BEGIN + RAISE WARNING 'drop'; + IF EXISTS(SELECT 1 FROM pg_catalog.pg_event_trigger_dropped_objects() WHERE object_type = 'EXTENSION' AND object_identifier = 'trunklet-format') THEN +--SELECT trunklet.template_language__remove( + DELETE FROM _trunklet.language WHERE language_name = 'format'; + END IF; +END +$body$; +CREATE EVENT TRIGGER trunklet__format_drop + ON sql_drop + WHEN tag IN ( 'DROP EXTENSION' ) + EXECUTE PROCEDURE _trunklet._extension_drop__format() +; +*/ + +SELECT trunklet.template_language__add( + language_name := 'format' + , parameter_type := 'jsonb' + , template_type := 'text' + , process_function_options := 'LANGUAGE plpgsql' + , process_function_body := $process_body$ +DECLARE + c_param CONSTANT jsonb := parameters::jsonb; + v_return text; +BEGIN + -- DUPLICATED IN BOTH FUNCTIONS + IF jsonb_typeof(c_param) <> 'object' THEN + RAISE EXCEPTION 'parameters must be a JSON object, not %', jsonb_typeof(c_param) + USING DETAIL = format('parameters = %L', c_param) + ; + END IF; + + -- Sanity-check parameters + -- DUPLICATED IN BOTH FUNCTIONS + DECLARE + parameter_name text; + value text; + v_type text; + BEGIN + FOR parameter_name, value IN SELECT * FROM jsonb_each( c_param ) + LOOP + IF parameter_name ~ '%' THEN + RAISE EXCEPTION + USING DETAIL = 'parameter_name ' || parameter_name + -- MESSAGE instead of trying to escape % + , MESSAGE = 'parameter names may not contain "%"' + ; + END IF; + + v_type := jsonb_typeof( c_param->parameter_name ); + IF v_type IN ( 'array', 'object' ) THEN + RAISE EXCEPTION '% is not supported as a parameter type', v_type + USING DETAIL = format( $$paramater %s = %s$$, parameter_name, value ) + ; + END IF; + END LOOP; + END; + + /* + * Look for things to replace + */ + DECLARE + c_parse CONSTANT text[] := string_to_array( template::text, '%' ); + c_alen CONSTANT int := array_length( c_parse, 1 ); + + /* + * TODO: Instead of trying to track exact template position, it would be + * easier to just track the accumulated length of every c_parse element + * we've seen. That can be converted to template position by simply adding + * (v_pos-1) * length('%'). + */ + v_template_pos int := 1 + length(c_parse[1]); -- Start at 1 and account for c_parse[1] + + v_pos int := 2; -- We handle first element specially + v_format_option_position int; + v_last_parameter_position int; + v_cur text = ''; -- Needs to start at '' for template position tracking + v_parameter text; + v_optional boolean; + v_format_option text; + v_value text; + BEGIN + /* + * We have to special-case the first element because we know it must be + * plain text with no paramaters, or empty if the template starts with '%%'. + */ + v_return := c_parse[1]; + WHILE v_pos <= c_alen LOOP + v_template_pos := v_template_pos + 1; -- We just consumed a % + v_cur := c_parse[v_pos]; + --RAISE WARNING 'v_template_pos = %, v_pos = %, v_cur = %', v_template_pos, v_pos, v_cur; + + /* + * The %% escape gives us an empty v_cur. + */ + IF v_cur = '' THEN + /* + * If we have the template '1 %% 2 %param%s' then when we hit this + * condition we're 'inside' the %% (v_pos = 2, v_cur = ''). The code + * below this IF block depends on v_cur being a parameter name. This + * means we have to consume the NEXT array element (v_pos = 3) before + * continuing, because it could ALSO be empty (ie: '%%%param%s'). It + * could NOT be a parameter though. + */ + + -- Update our position + v_template_pos = v_template_pos + length(c_parse[v_pos + 1]) + 1; + -- v_cur will immediately be reset + + -- Append the % and the next array element + v_return := v_return || '%' || c_parse[v_pos + 1]; + v_pos := v_pos + 2; + CONTINUE; + END IF; + + /* + * Since we don't allow % in a parameter name, we know that v_cur IS THE NAME OF A PARAMETER. + */ + v_last_parameter_position := v_template_pos - 1; -- -1 for the '%' + v_parameter := v_cur; + v_pos := v_pos + 1; + IF v_pos > c_alen THEN + RAISE EXCEPTION 'Reached end of template while processing parameters' + USING HINT = E'Parameters must match the format "%parameter_name%[O]T" where T is s, I or L\n' + || 'and O (if present) means the parameter is optional.' + ; + END IF; + + v_template_pos = v_template_pos + length(v_cur) + 1; + v_cur := c_parse[v_pos]; + --RAISE WARNING 'v_template_pos = %, v_pos = %, v_cur = %', v_template_pos, v_pos, v_cur; + /* + * At this point v_cur IS THE OPTIONS SPECIFIER + */ + + -- This is where to insert support for other format option handling + -- These ugly shenanigans are to avoid copying a potentially large string multiple times + v_optional := substr( v_cur, 1, 1 ) = 'O'; + v_format_option_position := CASE v_optional WHEN true THEN 2 WHEN false THEN 1 END; + v_template_pos = v_template_pos + v_format_option_position - 1; -- -1 because we haven't technically consumed the format specifier yet + v_format_option = substr( v_cur, v_format_option_position, 1 ); + --RAISE WARNING 'FORMAT OPTION %, v_template_pos = %, v_pos = %, v_format_option_position = %, v_cur = %', v_format_option, v_template_pos, v_pos, v_format_option_position, v_cur; + + --RAISE WARNING 'v_pos = %, v_cur = %, v_format_option = %', v_pos, v_cur, v_format_option; + IF v_format_option NOT IN ( 's', 'I', 'L' ) THEN + RAISE EXCEPTION 'Unexpected character "%" in format specifier "%"' + , v_format_option + , substr(v_cur, 1, v_format_option_position ) + USING DETAIL = format( 'parameter "%s" at template position %s', v_parameter, v_last_parameter_position ) + ; + END IF; + + IF v_optional THEN + IF v_format_option = 'I' THEN + RAISE EXCEPTION 'SQL identifier format option ("I") not allowed with optional parameters' + USING ERRCODE = 'null_value_not_allowed' + , HINT = 'SQL identifiers can not be NULL or empty, so identifier formats may not be optional.' + , DETAIL = format( 'parameter "%s" at template position %s', v_parameter, v_last_parameter_position ) + ; + END IF; + -- c_param->v_parameter will ONLY be NULL if v_parameter doesn't exist in the document + ELSIF (c_param->v_parameter) IS NULL THEN + RAISE EXCEPTION 'parameter "%" not found', v_parameter + USING DETAIL = format( 'at template position %s', v_last_parameter_position ) + ; + END IF; + + + -- Now that we've consumed the format specifier, update v_template_pos and grab our value + v_template_pos := v_template_pos + 1; + v_value := c_param->>v_parameter; + + /* + RAISE WARNING 'v_template_pos = %, v_pos = %, v_parameter = %, v_format_option = %, v_value = %, v_format_option_position = %' + , v_template_pos, v_pos, v_parameter, v_format_option, v_value, v_format_option_position + USING DETAIL = format( + 'prior "%s", next: "%s"' + , substr(template, v_template_pos-20, 20) + , substr(template, v_template_pos, 20) + ) + ; + */ + v_return := v_return + -- This is the actual replacement + || format( + '%' || v_format_option + , v_value + ) + + -- Remainder of document, up to next '%' + || substr( v_cur, v_format_option_position + 1 ) + ; + v_template_pos := v_template_pos + length(v_cur) - v_format_option_position; + v_pos := v_pos + 1; + END LOOP; + END; + + RETURN v_return; +END +$process_body$ + , extract_parameters_options := 'LANGUAGE plpgsql' + , extract_parameters_body := $extract_body$ +DECLARE + c_param CONSTANT jsonb := parameters::jsonb; + a_as text[]; + sql text; + r record; +BEGIN + -- DUPLICATED IN BOTH FUNCTIONS + IF jsonb_typeof(c_param) <> 'object' THEN + RAISE EXCEPTION 'parameters must be a JSON object, not %', jsonb_typeof(c_param) + USING DETAIL = format('parameters = %L', c_param) + ; + END IF; + + -- Sanity-check parameters + -- DUPLICATED IN BOTH FUNCTIONS + DECLARE + parameter_name text; + value text; + v_type text; + BEGIN + FOR parameter_name, value IN SELECT * FROM jsonb_each( c_param ) + LOOP + IF parameter_name ~ '%' THEN + RAISE EXCEPTION + USING DETAIL = 'parameter_name ' || parameter_name + -- MESSAGE instead of trying to escape % + , MESSAGE = 'parameter names may not contain "%"' + ; + END IF; + + v_type := jsonb_typeof( c_param->parameter_name ); + IF v_type IN ( 'array', 'object' ) THEN + RAISE EXCEPTION '% is not supported as a parameter type', v_type + USING DETAIL = format( $$paramater %s = %s$$, parameter_name, value ) + ; + END IF; + + -- THIS IS SPECIFIC TO extract_parametrs + IF parameter_name = ANY( extract_list ) THEN + a_as := a_as || format( + '%I %s' + , parameter_name + , CASE + WHEN v_type IN( 'string', 'null' ) THEN 'text' + WHEN v_type = 'number' THEN 'numeric' + -- array and object can't happen + ELSE v_type + END + ); + END IF; + END LOOP; + END; + + -- Nothing matched + IF a_as IS NULL THEN + RETURN NULL; + END IF; + + sql := 'SELECT * FROM jsonb_to_record($1) AS j(' || array_to_string( a_as, ', ' ) || ')'; + RAISE DEBUG 'sql = %', sql; + EXECUTE sql INTO r USING c_param; + RETURN row_to_json(r)::jsonb; +END +$extract_body$ +); + +-- vi: expandtab ts=2 sw=2 diff --git a/test/expected/base.out b/test/expected/base.out index a077c24..ef6398b 100644 --- a/test/expected/base.out +++ b/test/expected/base.out @@ -1,4 +1,20 @@ \set ECHO none +psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 ok 1 ok 2 @@ -18,14 +34,14 @@ ok 15 - test optional handling with L and text ok 16 - test optional handling with L and true ok 17 - threw 22004: SQL identifier format option ("I") not allowed with optional parameters ok 18 - threw Unexpected character "" in format specifier "" -ok 19 - threw Unexpected character "" in format specifier "O" -ok 20 - threw Unexpected character "a" in format specifier "a" -ok 21 - threw Unexpected character "a" in format specifier "Oa" -ok 22 - threw Unexpected character "S" in format specifier "S" -ok 23 - threw Unexpected character "S" in format specifier "OS" -ok 24 - threw Unexpected character "i" in format specifier "i" -ok 25 - threw Unexpected character "i" in format specifier "Oi" -ok 26 - threw Unexpected character "l" in format specifier "l" +ok 19 - threw Unexpected character "a" in format specifier "a" +ok 20 - threw Unexpected character "S" in format specifier "S" +ok 21 - threw Unexpected character "i" in format specifier "i" +ok 22 - threw Unexpected character "l" in format specifier "l" +ok 23 - threw Unexpected character "" in format specifier "O" +ok 24 - threw Unexpected character "a" in format specifier "Oa" +ok 25 - threw Unexpected character "S" in format specifier "OS" +ok 26 - threw Unexpected character "i" in format specifier "Oi" ok 27 - threw Unexpected character "l" in format specifier "Ol" ok 28 - Should throw exception like 'parameter "mia" not found' ok 29 - Should throw exception like 'parameter "mia" not found' diff --git a/test/expected/base_94.out b/test/expected/base_94.out deleted file mode 100644 index 7595aca..0000000 --- a/test/expected/base_94.out +++ /dev/null @@ -1,94 +0,0 @@ -\set ECHO none -ok 1 -ok 2 -ok 3 -ok 4 -ok 5 -ok 6 -ok 7 - test optional handling with s and 1.1 -ok 8 - test optional handling with s and -ok 9 - test optional handling with s and text -ok 10 - test optional handling with s and true -ok 11 - test optional handling with L and 1.1 -ok 12 - test optional handling with L and -ok 13 - test optional handling with L and text -ok 14 - test optional handling with L and true -ok 15 - threw 22004: SQL identifier format option ("I") not allowed with optional parameters -THIS ERROR IS OK! -DETAIL: parameter "test parameter" at template position 11 -ERROR: SQL identifier format option ("I") not allowed with optional parameters -DETAIL: parameter "test parameter" at template position 11 -HINT: SQL identifiers can not be NULL or empty, so identifier formats may not be optional. -CONTEXT: SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 27 at EXECUTE statement -ok 16 - threw Unexpected character "" in format specifier "" -ok 17 - threw Unexpected character "" in format specifier "O" -ok 18 - threw Unexpected character "a" in format specifier "a" -ok 19 - threw Unexpected character "a" in format specifier "Oa" -ok 20 - threw Unexpected character "S" in format specifier "S" -ok 21 - threw Unexpected character "S" in format specifier "OS" -ok 22 - threw Unexpected character "i" in format specifier "i" -ok 23 - threw Unexpected character "i" in format specifier "Oi" -ok 24 - threw Unexpected character "l" in format specifier "l" -ok 25 - threw Unexpected character "l" in format specifier "Ol" -THIS ERROR IS OK! -DETAIL: parameter "Moo" at template position 5 -ERROR: Unexpected character "S" in format specifier "OS" -DETAIL: parameter "Moo" at template position 5 -CONTEXT: SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 27 at EXECUTE statement -THIS ERROR IS OK! -DETAIL: parameter "Moo" at template position 5 -ERROR: Unexpected character "l" in format specifier "l" -DETAIL: parameter "Moo" at template position 5 -CONTEXT: SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 27 at EXECUTE statement -ok 26 - Should throw exception like 'parameter "mia" not found' -ok 27 - Should throw exception like 'parameter "mia" not found' -ok 28 - Should throw exception like 'parameter "mia" not found' -ok 29 - Should throw exception like 'parameter "mia" not found' -ok 30 - Should throw exception like 'parameter "mia" not found' -ok 31 - Should throw exception like 'parameter "mia" not found' -ok 32 - Should throw exception like 'parameter "mia" not found' -ok 33 - Should throw exception like 'parameter "mia" not found' -ok 34 - Should throw exception like 'parameter "mia" not found' -ok 35 - Should throw exception like 'parameter "mia" not found' -ok 36 - Should throw exception like 'parameter "mia" not found' -ok 37 - Should throw exception like 'parameter "mia" not found' -ok 38 - Should throw exception like 'parameter "mia" not found' -ok 39 - Should throw exception like 'parameter "mia" not found' -ok 40 - Should throw exception like 'parameter "mia" not found' -ok 41 - Should throw exception like 'parameter "mia" not found' -ok 42 - Should throw exception like 'parameter "mia" not found' -ok 43 - Should throw exception like 'parameter "mia" not found' -ok 44 - Should throw exception like 'parameter "mia" not found' -ok 45 - Should throw exception like 'parameter "mia" not found' -ok 46 - Should throw exception like 'parameter "mia" not found' -ok 47 - Should throw exception like 'parameter "mia" not found' -ok 48 - Should throw exception like 'parameter "mia" not found' -ok 49 - Should throw exception like 'parameter "mia" not found' -ok 50 - Test extract_paramaters() for single string -ok 51 - Test extract_paramaters() for number -ok 52 - Test extract_paramaters() for multiple -ok 53 - Test extract_paramaters() for num & bogus -ok 54 - Test extract_paramaters() for bogus -ok 55 - SELECT trunklet.process_language('format', 'template', ('{"value": 1.1}'::jsonb)->'value') -ok 56 - SELECT trunklet.process_language('format', 'template', ('{"value": "text"}'::jsonb)->'value') -ok 57 - SELECT trunklet.process_language('format', 'template', ('{"value": true}'::jsonb)->'value') -ok 58 - SELECT trunklet.process_language('format', 'template', ('{"value": [1, 2, 3]}'::jsonb)->'value') -ok 59 - SELECT trunklet.extract_parameters('format', ('{"value": 1.1}'::jsonb)->'value', '{a}'::text[]) -ok 60 - SELECT trunklet.extract_parameters('format', ('{"value": "text"}'::jsonb)->'value', '{a}'::text[]) -ok 61 - SELECT trunklet.extract_parameters('format', ('{"value": true}'::jsonb)->'value', '{a}'::text[]) -ok 62 - SELECT trunklet.extract_parameters('format', ('{"value": [1, 2, 3]}'::jsonb)->'value', '{a}'::text[]) -ok 63 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 64 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 65 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 66 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 67 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 68 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 69 - threw object is not supported as a parameter type -ok 70 - threw array is not supported as a parameter type -ok 71 - threw object is not supported as a parameter type -ok 72 - threw array is not supported as a parameter type -1..72 -# TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/base_941.out b/test/expected/base_941.out deleted file mode 100644 index 2b873f3..0000000 --- a/test/expected/base_941.out +++ /dev/null @@ -1,94 +0,0 @@ -\set ECHO none -ok 1 -ok 2 -ok 3 -ok 4 -ok 5 -ok 6 -ok 7 - test optional handling with s and 1.1 -ok 8 - test optional handling with s and -ok 9 - test optional handling with s and text -ok 10 - test optional handling with s and true -ok 11 - test optional handling with L and 1.1 -ok 12 - test optional handling with L and -ok 13 - test optional handling with L and text -ok 14 - test optional handling with L and true -ok 15 - threw 22004: SQL identifier format option ("I") not allowed with optional parameters -THIS ERROR IS OK! -DETAIL: parameter "test parameter" at template position 11 -ERROR: SQL identifier format option ("I") not allowed with optional parameters -DETAIL: parameter "test parameter" at template position 11 -HINT: SQL identifiers can not be NULL or empty, so identifier formats may not be optional. -CONTEXT: SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 44 at EXECUTE statement -ok 16 - threw Unexpected character "" in format specifier "" -ok 17 - threw Unexpected character "" in format specifier "O" -ok 18 - threw Unexpected character "a" in format specifier "a" -ok 19 - threw Unexpected character "a" in format specifier "Oa" -ok 20 - threw Unexpected character "S" in format specifier "S" -ok 21 - threw Unexpected character "S" in format specifier "OS" -ok 22 - threw Unexpected character "i" in format specifier "i" -ok 23 - threw Unexpected character "i" in format specifier "Oi" -ok 24 - threw Unexpected character "l" in format specifier "l" -ok 25 - threw Unexpected character "l" in format specifier "Ol" -THIS ERROR IS OK! -DETAIL: parameter "Moo" at template position 5 -ERROR: Unexpected character "S" in format specifier "OS" -DETAIL: parameter "Moo" at template position 5 -CONTEXT: SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 44 at EXECUTE statement -THIS ERROR IS OK! -DETAIL: parameter "Moo" at template position 5 -ERROR: Unexpected character "l" in format specifier "l" -DETAIL: parameter "Moo" at template position 5 -CONTEXT: SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 44 at EXECUTE statement -ok 26 - Should throw exception like 'parameter "mia" not found' -ok 27 - Should throw exception like 'parameter "mia" not found' -ok 28 - Should throw exception like 'parameter "mia" not found' -ok 29 - Should throw exception like 'parameter "mia" not found' -ok 30 - Should throw exception like 'parameter "mia" not found' -ok 31 - Should throw exception like 'parameter "mia" not found' -ok 32 - Should throw exception like 'parameter "mia" not found' -ok 33 - Should throw exception like 'parameter "mia" not found' -ok 34 - Should throw exception like 'parameter "mia" not found' -ok 35 - Should throw exception like 'parameter "mia" not found' -ok 36 - Should throw exception like 'parameter "mia" not found' -ok 37 - Should throw exception like 'parameter "mia" not found' -ok 38 - Should throw exception like 'parameter "mia" not found' -ok 39 - Should throw exception like 'parameter "mia" not found' -ok 40 - Should throw exception like 'parameter "mia" not found' -ok 41 - Should throw exception like 'parameter "mia" not found' -ok 42 - Should throw exception like 'parameter "mia" not found' -ok 43 - Should throw exception like 'parameter "mia" not found' -ok 44 - Should throw exception like 'parameter "mia" not found' -ok 45 - Should throw exception like 'parameter "mia" not found' -ok 46 - Should throw exception like 'parameter "mia" not found' -ok 47 - Should throw exception like 'parameter "mia" not found' -ok 48 - Should throw exception like 'parameter "mia" not found' -ok 49 - Should throw exception like 'parameter "mia" not found' -ok 50 - Test extract_paramaters() for single string -ok 51 - Test extract_paramaters() for number -ok 52 - Test extract_paramaters() for multiple -ok 53 - Test extract_paramaters() for num & bogus -ok 54 - Test extract_paramaters() for bogus -ok 55 - SELECT trunklet.process_language('format', 'template', ('{"value": 1.1}'::jsonb)->'value') -ok 56 - SELECT trunklet.process_language('format', 'template', ('{"value": "text"}'::jsonb)->'value') -ok 57 - SELECT trunklet.process_language('format', 'template', ('{"value": true}'::jsonb)->'value') -ok 58 - SELECT trunklet.process_language('format', 'template', ('{"value": [1, 2, 3]}'::jsonb)->'value') -ok 59 - SELECT trunklet.extract_parameters('format', ('{"value": 1.1}'::jsonb)->'value', '{a}'::text[]) -ok 60 - SELECT trunklet.extract_parameters('format', ('{"value": "text"}'::jsonb)->'value', '{a}'::text[]) -ok 61 - SELECT trunklet.extract_parameters('format', ('{"value": true}'::jsonb)->'value', '{a}'::text[]) -ok 62 - SELECT trunklet.extract_parameters('format', ('{"value": [1, 2, 3]}'::jsonb)->'value', '{a}'::text[]) -ok 63 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 64 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 65 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 66 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 67 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 68 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 69 - threw object is not supported as a parameter type -ok 70 - threw array is not supported as a parameter type -ok 71 - threw object is not supported as a parameter type -ok 72 - threw array is not supported as a parameter type -1..72 -# TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/base_95.out b/test/expected/base_95.out deleted file mode 100644 index 809148d..0000000 --- a/test/expected/base_95.out +++ /dev/null @@ -1,94 +0,0 @@ -\set ECHO none -ok 1 -ok 2 -ok 3 -ok 4 -ok 5 -ok 6 -ok 7 - test optional handling with s and 1.1 -ok 8 - test optional handling with s and -ok 9 - test optional handling with s and text -ok 10 - test optional handling with s and true -ok 11 - test optional handling with L and 1.1 -ok 12 - test optional handling with L and -ok 13 - test optional handling with L and text -ok 14 - test optional handling with L and true -ok 15 - threw 22004: SQL identifier format option ("I") not allowed with optional parameters -THIS ERROR IS OK! -DETAIL: parameter "test parameter" at template position 11 -ERROR: SQL identifier format option ("I") not allowed with optional parameters -DETAIL: parameter "test parameter" at template position 11 -HINT: SQL identifiers can not be NULL or empty, so identifier formats may not be optional. -CONTEXT: SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 27 at EXECUTE -ok 16 - threw Unexpected character "" in format specifier "" -ok 17 - threw Unexpected character "" in format specifier "O" -ok 18 - threw Unexpected character "a" in format specifier "a" -ok 19 - threw Unexpected character "a" in format specifier "Oa" -ok 20 - threw Unexpected character "S" in format specifier "S" -ok 21 - threw Unexpected character "S" in format specifier "OS" -ok 22 - threw Unexpected character "i" in format specifier "i" -ok 23 - threw Unexpected character "i" in format specifier "Oi" -ok 24 - threw Unexpected character "l" in format specifier "l" -ok 25 - threw Unexpected character "l" in format specifier "Ol" -THIS ERROR IS OK! -DETAIL: parameter "Moo" at template position 5 -ERROR: Unexpected character "S" in format specifier "OS" -DETAIL: parameter "Moo" at template position 5 -CONTEXT: SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 27 at EXECUTE -THIS ERROR IS OK! -DETAIL: parameter "Moo" at template position 5 -ERROR: Unexpected character "l" in format specifier "l" -DETAIL: parameter "Moo" at template position 5 -CONTEXT: SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 27 at EXECUTE -ok 26 - Should throw exception like 'parameter "mia" not found' -ok 27 - Should throw exception like 'parameter "mia" not found' -ok 28 - Should throw exception like 'parameter "mia" not found' -ok 29 - Should throw exception like 'parameter "mia" not found' -ok 30 - Should throw exception like 'parameter "mia" not found' -ok 31 - Should throw exception like 'parameter "mia" not found' -ok 32 - Should throw exception like 'parameter "mia" not found' -ok 33 - Should throw exception like 'parameter "mia" not found' -ok 34 - Should throw exception like 'parameter "mia" not found' -ok 35 - Should throw exception like 'parameter "mia" not found' -ok 36 - Should throw exception like 'parameter "mia" not found' -ok 37 - Should throw exception like 'parameter "mia" not found' -ok 38 - Should throw exception like 'parameter "mia" not found' -ok 39 - Should throw exception like 'parameter "mia" not found' -ok 40 - Should throw exception like 'parameter "mia" not found' -ok 41 - Should throw exception like 'parameter "mia" not found' -ok 42 - Should throw exception like 'parameter "mia" not found' -ok 43 - Should throw exception like 'parameter "mia" not found' -ok 44 - Should throw exception like 'parameter "mia" not found' -ok 45 - Should throw exception like 'parameter "mia" not found' -ok 46 - Should throw exception like 'parameter "mia" not found' -ok 47 - Should throw exception like 'parameter "mia" not found' -ok 48 - Should throw exception like 'parameter "mia" not found' -ok 49 - Should throw exception like 'parameter "mia" not found' -ok 50 - Test extract_paramaters() for single string -ok 51 - Test extract_paramaters() for number -ok 52 - Test extract_paramaters() for multiple -ok 53 - Test extract_paramaters() for num & bogus -ok 54 - Test extract_paramaters() for bogus -ok 55 - SELECT trunklet.process_language('format', 'template', ('{"value": 1.1}'::jsonb)->'value') -ok 56 - SELECT trunklet.process_language('format', 'template', ('{"value": "text"}'::jsonb)->'value') -ok 57 - SELECT trunklet.process_language('format', 'template', ('{"value": true}'::jsonb)->'value') -ok 58 - SELECT trunklet.process_language('format', 'template', ('{"value": [1, 2, 3]}'::jsonb)->'value') -ok 59 - SELECT trunklet.extract_parameters('format', ('{"value": 1.1}'::jsonb)->'value', '{a}'::text[]) -ok 60 - SELECT trunklet.extract_parameters('format', ('{"value": "text"}'::jsonb)->'value', '{a}'::text[]) -ok 61 - SELECT trunklet.extract_parameters('format', ('{"value": true}'::jsonb)->'value', '{a}'::text[]) -ok 62 - SELECT trunklet.extract_parameters('format', ('{"value": [1, 2, 3]}'::jsonb)->'value', '{a}'::text[]) -ok 63 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 64 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 65 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 66 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 67 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 68 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 69 - threw object is not supported as a parameter type -ok 70 - threw array is not supported as a parameter type -ok 71 - threw object is not supported as a parameter type -ok 72 - threw array is not supported as a parameter type -1..72 -# TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/base_951.out b/test/expected/base_951.out deleted file mode 100644 index a894538..0000000 --- a/test/expected/base_951.out +++ /dev/null @@ -1,94 +0,0 @@ -\set ECHO none -ok 1 -ok 2 -ok 3 -ok 4 -ok 5 -ok 6 -ok 7 - test optional handling with s and 1.1 -ok 8 - test optional handling with s and -ok 9 - test optional handling with s and text -ok 10 - test optional handling with s and true -ok 11 - test optional handling with L and 1.1 -ok 12 - test optional handling with L and -ok 13 - test optional handling with L and text -ok 14 - test optional handling with L and true -ok 15 - threw 22004: SQL identifier format option ("I") not allowed with optional parameters -THIS ERROR IS OK! -DETAIL: parameter "test parameter" at template position 11 -ERROR: SQL identifier format option ("I") not allowed with optional parameters -DETAIL: parameter "test parameter" at template position 11 -HINT: SQL identifiers can not be NULL or empty, so identifier formats may not be optional. -CONTEXT: SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 44 at EXECUTE -ok 16 - threw Unexpected character "" in format specifier "" -ok 17 - threw Unexpected character "" in format specifier "O" -ok 18 - threw Unexpected character "a" in format specifier "a" -ok 19 - threw Unexpected character "a" in format specifier "Oa" -ok 20 - threw Unexpected character "S" in format specifier "S" -ok 21 - threw Unexpected character "S" in format specifier "OS" -ok 22 - threw Unexpected character "i" in format specifier "i" -ok 23 - threw Unexpected character "i" in format specifier "Oi" -ok 24 - threw Unexpected character "l" in format specifier "l" -ok 25 - threw Unexpected character "l" in format specifier "Ol" -THIS ERROR IS OK! -DETAIL: parameter "Moo" at template position 5 -ERROR: Unexpected character "S" in format specifier "OS" -DETAIL: parameter "Moo" at template position 5 -CONTEXT: SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 44 at EXECUTE -THIS ERROR IS OK! -DETAIL: parameter "Moo" at template position 5 -ERROR: Unexpected character "l" in format specifier "l" -DETAIL: parameter "Moo" at template position 5 -CONTEXT: SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 44 at EXECUTE -ok 26 - Should throw exception like 'parameter "mia" not found' -ok 27 - Should throw exception like 'parameter "mia" not found' -ok 28 - Should throw exception like 'parameter "mia" not found' -ok 29 - Should throw exception like 'parameter "mia" not found' -ok 30 - Should throw exception like 'parameter "mia" not found' -ok 31 - Should throw exception like 'parameter "mia" not found' -ok 32 - Should throw exception like 'parameter "mia" not found' -ok 33 - Should throw exception like 'parameter "mia" not found' -ok 34 - Should throw exception like 'parameter "mia" not found' -ok 35 - Should throw exception like 'parameter "mia" not found' -ok 36 - Should throw exception like 'parameter "mia" not found' -ok 37 - Should throw exception like 'parameter "mia" not found' -ok 38 - Should throw exception like 'parameter "mia" not found' -ok 39 - Should throw exception like 'parameter "mia" not found' -ok 40 - Should throw exception like 'parameter "mia" not found' -ok 41 - Should throw exception like 'parameter "mia" not found' -ok 42 - Should throw exception like 'parameter "mia" not found' -ok 43 - Should throw exception like 'parameter "mia" not found' -ok 44 - Should throw exception like 'parameter "mia" not found' -ok 45 - Should throw exception like 'parameter "mia" not found' -ok 46 - Should throw exception like 'parameter "mia" not found' -ok 47 - Should throw exception like 'parameter "mia" not found' -ok 48 - Should throw exception like 'parameter "mia" not found' -ok 49 - Should throw exception like 'parameter "mia" not found' -ok 50 - Test extract_paramaters() for single string -ok 51 - Test extract_paramaters() for number -ok 52 - Test extract_paramaters() for multiple -ok 53 - Test extract_paramaters() for num & bogus -ok 54 - Test extract_paramaters() for bogus -ok 55 - SELECT trunklet.process_language('format', 'template', ('{"value": 1.1}'::jsonb)->'value') -ok 56 - SELECT trunklet.process_language('format', 'template', ('{"value": "text"}'::jsonb)->'value') -ok 57 - SELECT trunklet.process_language('format', 'template', ('{"value": true}'::jsonb)->'value') -ok 58 - SELECT trunklet.process_language('format', 'template', ('{"value": [1, 2, 3]}'::jsonb)->'value') -ok 59 - SELECT trunklet.extract_parameters('format', ('{"value": 1.1}'::jsonb)->'value', '{a}'::text[]) -ok 60 - SELECT trunklet.extract_parameters('format', ('{"value": "text"}'::jsonb)->'value', '{a}'::text[]) -ok 61 - SELECT trunklet.extract_parameters('format', ('{"value": true}'::jsonb)->'value', '{a}'::text[]) -ok 62 - SELECT trunklet.extract_parameters('format', ('{"value": [1, 2, 3]}'::jsonb)->'value', '{a}'::text[]) -ok 63 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 64 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 65 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 66 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 67 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 68 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 69 - threw object is not supported as a parameter type -ok 70 - threw array is not supported as a parameter type -ok 71 - threw object is not supported as a parameter type -ok 72 - threw array is not supported as a parameter type -1..72 -# TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/base_961.out b/test/expected/base_961.out deleted file mode 100644 index 7334212..0000000 --- a/test/expected/base_961.out +++ /dev/null @@ -1,97 +0,0 @@ -\set ECHO none -ok 1 -ok 2 -ok 3 -ok 4 -ok 5 -ok 6 -ok 7 - test optional handling with s and 1.1 -ok 8 - test optional handling with s and -ok 9 - test optional handling with s and text -ok 10 - test optional handling with s and true -ok 11 - test optional handling with L and 1.1 -ok 12 - test optional handling with L and -ok 13 - test optional handling with L and text -ok 14 - test optional handling with L and true -ok 15 - threw 22004: SQL identifier format option ("I") not allowed with optional parameters -THIS ERROR IS OK! -DETAIL: parameter "test parameter" at template position 11 -ERROR: SQL identifier format option ("I") not allowed with optional parameters -DETAIL: parameter "test parameter" at template position 11 -HINT: SQL identifiers can not be NULL or empty, so identifier formats may not be optional. -CONTEXT: PL/pgSQL function _trunklet_functions.language_id_0000000001__process(text,jsonb) line 135 at RAISE -SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 44 at EXECUTE -ok 16 - threw Unexpected character "" in format specifier "" -ok 17 - threw Unexpected character "" in format specifier "O" -ok 18 - threw Unexpected character "a" in format specifier "a" -ok 19 - threw Unexpected character "a" in format specifier "Oa" -ok 20 - threw Unexpected character "S" in format specifier "S" -ok 21 - threw Unexpected character "S" in format specifier "OS" -ok 22 - threw Unexpected character "i" in format specifier "i" -ok 23 - threw Unexpected character "i" in format specifier "Oi" -ok 24 - threw Unexpected character "l" in format specifier "l" -ok 25 - threw Unexpected character "l" in format specifier "Ol" -THIS ERROR IS OK! -DETAIL: parameter "Moo" at template position 5 -ERROR: Unexpected character "S" in format specifier "OS" -DETAIL: parameter "Moo" at template position 5 -CONTEXT: PL/pgSQL function _trunklet_functions.language_id_0000000001__process(text,jsonb) line 126 at RAISE -SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 44 at EXECUTE -THIS ERROR IS OK! -DETAIL: parameter "Moo" at template position 5 -ERROR: Unexpected character "l" in format specifier "l" -DETAIL: parameter "Moo" at template position 5 -CONTEXT: PL/pgSQL function _trunklet_functions.language_id_0000000001__process(text,jsonb) line 126 at RAISE -SQL statement "SELECT _trunklet_functions.language_id_0000000001__process( CAST($1 AS text), CAST($2 AS jsonb) )" -PL/pgSQL function trunklet.process_language(character varying,text,anyelement) line 44 at EXECUTE -ok 26 - Should throw exception like 'parameter "mia" not found' -ok 27 - Should throw exception like 'parameter "mia" not found' -ok 28 - Should throw exception like 'parameter "mia" not found' -ok 29 - Should throw exception like 'parameter "mia" not found' -ok 30 - Should throw exception like 'parameter "mia" not found' -ok 31 - Should throw exception like 'parameter "mia" not found' -ok 32 - Should throw exception like 'parameter "mia" not found' -ok 33 - Should throw exception like 'parameter "mia" not found' -ok 34 - Should throw exception like 'parameter "mia" not found' -ok 35 - Should throw exception like 'parameter "mia" not found' -ok 36 - Should throw exception like 'parameter "mia" not found' -ok 37 - Should throw exception like 'parameter "mia" not found' -ok 38 - Should throw exception like 'parameter "mia" not found' -ok 39 - Should throw exception like 'parameter "mia" not found' -ok 40 - Should throw exception like 'parameter "mia" not found' -ok 41 - Should throw exception like 'parameter "mia" not found' -ok 42 - Should throw exception like 'parameter "mia" not found' -ok 43 - Should throw exception like 'parameter "mia" not found' -ok 44 - Should throw exception like 'parameter "mia" not found' -ok 45 - Should throw exception like 'parameter "mia" not found' -ok 46 - Should throw exception like 'parameter "mia" not found' -ok 47 - Should throw exception like 'parameter "mia" not found' -ok 48 - Should throw exception like 'parameter "mia" not found' -ok 49 - Should throw exception like 'parameter "mia" not found' -ok 50 - Test extract_paramaters() for single string -ok 51 - Test extract_paramaters() for number -ok 52 - Test extract_paramaters() for multiple -ok 53 - Test extract_paramaters() for num & bogus -ok 54 - Test extract_paramaters() for bogus -ok 55 - SELECT trunklet.process_language('format', 'template', ('{"value": 1.1}'::jsonb)->'value') -ok 56 - SELECT trunklet.process_language('format', 'template', ('{"value": "text"}'::jsonb)->'value') -ok 57 - SELECT trunklet.process_language('format', 'template', ('{"value": true}'::jsonb)->'value') -ok 58 - SELECT trunklet.process_language('format', 'template', ('{"value": [1, 2, 3]}'::jsonb)->'value') -ok 59 - SELECT trunklet.extract_parameters('format', ('{"value": 1.1}'::jsonb)->'value', '{a}'::text[]) -ok 60 - SELECT trunklet.extract_parameters('format', ('{"value": "text"}'::jsonb)->'value', '{a}'::text[]) -ok 61 - SELECT trunklet.extract_parameters('format', ('{"value": true}'::jsonb)->'value', '{a}'::text[]) -ok 62 - SELECT trunklet.extract_parameters('format', ('{"value": [1, 2, 3]}'::jsonb)->'value', '{a}'::text[]) -ok 63 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 64 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 65 - process_language('format', 'template', %L::jsonb): parameter names may not contain "%" -ok 66 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 67 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 68 - extract_parameters('format', %L::jsonb, '{a}'::text[]): parameter names may not contain "%" -ok 69 - threw object is not supported as a parameter type -ok 70 - threw array is not supported as a parameter type -ok 71 - threw object is not supported as a parameter type -ok 72 - threw array is not supported as a parameter type -1..72 -# TRANSACTION INTENTIONALLY LEFT OPEN! diff --git a/test/expected/build.out b/test/expected/build.out index d9c8483..70f966e 100644 --- a/test/expected/build.out +++ b/test/expected/build.out @@ -1,4 +1,20 @@ \set ECHO none +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 _create --------- diff --git a/test/expected/reload.out b/test/expected/reload.out index cc997dd..46ae27c 100644 --- a/test/expected/reload.out +++ b/test/expected/reload.out @@ -1,5 +1,21 @@ \set ECHO none 1..8 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 ok 1 - Create extension ok 2 - A second extension_drop__add() call fails ok 3 - Drop extension diff --git a/test/expected/search_path.out b/test/expected/search_path.out index b8a70c0..926ba22 100644 --- a/test/expected/search_path.out +++ b/test/expected/search_path.out @@ -1,7 +1,39 @@ \set ECHO none 1..2 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 ok 1 - verify search_path has not changed without cascade +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +LOCATION: exec_stmt_raise, pl_exec.c:3897 ok 2 - verify search_path has not changed with cascade # TRANSACTION INTENTIONALLY LEFT OPEN! From 6ea63863d0f60353a1584d033dc6a007ef1c2011 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 14:27:33 -0500 Subject: [PATCH 02/11] Make test output independent of the PostgreSQL version trunklet's deprecation warning is printed with a `LOCATION:` line naming a `pl_exec.c` line number, which differs between PostgreSQL majors, so the expected output only matched one of them. `test/deps.sql` now sets terse verbosity; expected output regenerated with `make results`. Co-Authored-By: Claude Sonnet 5.5 --- test/deps.sql | 4 +++ test/expected/base.out | 24 ++++++------------ test/expected/build.out | 24 ++++++------------ test/expected/reload.out | 24 ++++++------------ test/expected/search_path.out | 48 ++++++++++++----------------------- 5 files changed, 44 insertions(+), 80 deletions(-) diff --git a/test/deps.sql b/test/deps.sql index 93dfda4..9885ee5 100644 --- a/test/deps.sql +++ b/test/deps.sql @@ -1,5 +1,9 @@ -- Note: pgTap is loaded by setup.sql +-- Terse: verbose output adds a LOCATION line (pl_exec.c line number) to trunklet's +-- deprecation warning when the extension loads, which differs between PostgreSQL versions +\set VERBOSITY terse + -- Add any test dependency statements here --CREATE EXTENSION IF NOT EXISTS trunklet; DO $$ BEGIN diff --git a/test/expected/base.out b/test/expected/base.out index ef6398b..4b6d88a 100644 --- a/test/expected/base.out +++ b/test/expected/base.out @@ -1,20 +1,12 @@ \set ECHO none -psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -psql:test/load.sql:3: WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 +psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead ok 1 ok 2 diff --git a/test/expected/build.out b/test/expected/build.out index 70f966e..1efb039 100644 --- a/test/expected/build.out +++ b/test/expected/build.out @@ -1,20 +1,12 @@ \set ECHO none -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead _create --------- diff --git a/test/expected/reload.out b/test/expected/reload.out index 46ae27c..fed1801 100644 --- a/test/expected/reload.out +++ b/test/expected/reload.out @@ -1,21 +1,13 @@ \set ECHO none 1..8 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead ok 1 - Create extension ok 2 - A second extension_drop__add() call fails ok 3 - Drop extension diff --git a/test/expected/search_path.out b/test/expected/search_path.out index 926ba22..f5f2da9 100644 --- a/test/expected/search_path.out +++ b/test/expected/search_path.out @@ -1,39 +1,23 @@ \set ECHO none 1..2 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead ok 1 - verify search_path has not changed without cascade -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 -WARNING: 01000: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -LOCATION: exec_stmt_raise, pl_exec.c:3897 +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead ok 2 - verify search_path has not changed with cascade # TRANSACTION INTENTIONALLY LEFT OPEN! From 6d2fd4a8257524f972d1dab8c02370e5409d8b06 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 14:37:57 -0500 Subject: [PATCH 03/11] Make test order deterministic Order each multi-row test input explicitly (by WITH ORDINALITY / position column, not by value) so pgTAP test numbers don't depend on the join order the planner picks. Each throws_ok/is query reads an ordered subquery or view as its only FROM item so ordering holds on 9.5. Co-Authored-By: Claude Sonnet 5.5 --- test/expected/base.out | 32 ++++++------- test/sql/base.sql | 106 ++++++++++++++++++++++------------------- 2 files changed, 72 insertions(+), 66 deletions(-) diff --git a/test/expected/base.out b/test/expected/base.out index 4b6d88a..d77098f 100644 --- a/test/expected/base.out +++ b/test/expected/base.out @@ -1,12 +1,12 @@ \set ECHO none -psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead -psql:test/load.sql:3: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:5: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:5: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:5: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:5: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:5: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:5: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:5: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead +psql:test/load.sql:5: WARNING: function__arg_types_text() is deprecated, use routine__parse_arg_types_text instead ok 1 ok 2 @@ -26,14 +26,14 @@ ok 15 - test optional handling with L and text ok 16 - test optional handling with L and true ok 17 - threw 22004: SQL identifier format option ("I") not allowed with optional parameters ok 18 - threw Unexpected character "" in format specifier "" -ok 19 - threw Unexpected character "a" in format specifier "a" -ok 20 - threw Unexpected character "S" in format specifier "S" -ok 21 - threw Unexpected character "i" in format specifier "i" -ok 22 - threw Unexpected character "l" in format specifier "l" -ok 23 - threw Unexpected character "" in format specifier "O" -ok 24 - threw Unexpected character "a" in format specifier "Oa" -ok 25 - threw Unexpected character "S" in format specifier "OS" -ok 26 - threw Unexpected character "i" in format specifier "Oi" +ok 19 - threw Unexpected character "" in format specifier "O" +ok 20 - threw Unexpected character "a" in format specifier "a" +ok 21 - threw Unexpected character "a" in format specifier "Oa" +ok 22 - threw Unexpected character "S" in format specifier "S" +ok 23 - threw Unexpected character "S" in format specifier "OS" +ok 24 - threw Unexpected character "i" in format specifier "i" +ok 25 - threw Unexpected character "i" in format specifier "Oi" +ok 26 - threw Unexpected character "l" in format specifier "l" ok 27 - threw Unexpected character "l" in format specifier "Ol" ok 28 - Should throw exception like 'parameter "mia" not found' ok 29 - Should throw exception like 'parameter "mia" not found' diff --git a/test/sql/base.sql b/test/sql/base.sql index 9e47424..7c92a0d 100644 --- a/test/sql/base.sql +++ b/test/sql/base.sql @@ -93,6 +93,7 @@ This is the end. $$ ); +-- Tests read from an explicitly ordered subquery or view so pgTAP test numbers don't depend on join order SELECT is( trunklet.process_language( 'format' @@ -110,13 +111,13 @@ SELECT is( ), 2 ) , format( 'test optional handling with %s and %s', replace, j->>'value' ) ) - FROM unnest('{s,L}'::text[]) r(replace) - , (SELECT row_to_json(v) AS j FROM (VALUES - (to_json(1.1)) - , (to_json(NULL::int)) - , (to_json('text'::text)) - , (to_json(true)) - ) v(value) ) j + FROM ( + SELECT replace, json_build_object('value', value) AS j + FROM unnest('{s,L}'::text[]) WITH ORDINALITY r(replace, rn) + , unnest(array[to_json(1.1), to_json(NULL::int), to_json('text'::text), to_json(true)]) + WITH ORDINALITY v(value, vn) + ORDER BY rn, vn + ) optional_test ; SELECT throws_ok( @@ -149,8 +150,12 @@ SELECT throws_ok( , optional || specifier ) ) - FROM unnest('{"",a,S,i,l}'::text[]) s(specifier) - , unnest('{"",O}'::text[]) o(optional) + FROM ( + SELECT specifier, optional + FROM unnest('{"",a,S,i,l}'::text[]) WITH ORDINALITY s(specifier, sn) + , unnest('{"",O}'::text[]) WITH ORDINALITY o(optional, opn) + ORDER BY sn, opn + ) specifier_test ; -- This is meant to ensure the DETAIL is correct, but it doesn't work on travis :( /* @@ -177,26 +182,25 @@ SELECT throws_like( ) , 'parameter "mia" not found' ) - FROM (VALUES - ('%mia%s ') - , (' %mia%s') - , (' %mia%s ') - , ('%mia%s') - ) input( input ) - , (VALUES ('{"Moo":1}'), (NULL) - ) param( param ) - , unnest('{s,I,L}'::text[]) u(replacement) + FROM ( + SELECT input, param, replacement + FROM unnest(array['%mia%s ', ' %mia%s', ' %mia%s ', '%mia%s']) WITH ORDINALITY i(input, inn) + , unnest(array['{"Moo":1}', NULL]) WITH ORDINALITY p(param, pn) + , unnest('{s,I,L}'::text[]) WITH ORDINALITY u(replacement, un) + ORDER BY inn, pn, un + ) missing_param_test ; CREATE TEMP VIEW extract_test AS SELECT * FROM ( VALUES - ( '{s1}'::text[], '{ "s1": "string 1" }'::jsonb , 'single string'::text ) - , ( '{num}' , '{ "num": 1.1 }' , 'number' ) - , ( '{t,f,"null"}', '{ "t":true,"f":false,"null":null }' , 'multiple' ) - , ( '{num,bogus}' , '{ "num": 1.1 }' , 'num & bogus' ) - , ( '{bogus}' , NULL , 'bogus' ) - ) AS v( extract_list, expected, description ) + ( 1, '{s1}'::text[], '{ "s1": "string 1" }'::jsonb , 'single string'::text ) + , ( 2, '{num}' , '{ "num": 1.1 }' , 'number' ) + , ( 3, '{t,f,"null"}', '{ "t":true,"f":false,"null":null }' , 'multiple' ) + , ( 4, '{num,bogus}' , '{ "num": 1.1 }' , 'num & bogus' ) + , ( 5, '{bogus}' , NULL , 'bogus' ) + ) AS v( pos, extract_list, expected, description ) + ORDER BY pos ; SELECT is( @@ -232,17 +236,16 @@ SELECT throws_ok( , j ) ) - FROM - unnest(array[ - $$process_language('format', 'template', (%L::jsonb)->'value')$$ - , $$extract_parameters('format', (%L::jsonb)->'value', '{a}'::text[])$$ - ]) f(format) - , (SELECT row_to_json(v)::jsonb AS j FROM (VALUES - (to_json(1.1)) - , (to_json('text'::text)) - , (to_json(true)) - , (to_json(array[1,2,3])) - ) v(value) ) j + FROM ( + SELECT format, jsonb_build_object('value', value) AS j + FROM unnest(array[ + $$process_language('format', 'template', (%L::jsonb)->'value')$$ + , $$extract_parameters('format', (%L::jsonb)->'value', '{a}'::text[])$$ + ]) WITH ORDINALITY f(format, fn) + , unnest(array[to_json(1.1), to_json('text'::text), to_json(true), to_json(array[1,2,3])]) + WITH ORDINALITY v(value, vn) + ORDER BY fn, vn + ) non_object_test ; SELECT throws_ok( format( @@ -253,12 +256,15 @@ SELECT throws_ok( , 'parameter names may not contain "%"' , format || ': parameter names may not contain "%"' ) - FROM - unnest(array[ - $$process_language('format', 'template', %L::jsonb)$$ - , $$extract_parameters('format', %L::jsonb, '{a}'::text[])$$ - ]) f(format) - , unnest('{% key,key %,k%k}'::text[]) k(key) + FROM ( + SELECT format, key + FROM unnest(array[ + $$process_language('format', 'template', %L::jsonb)$$ + , $$extract_parameters('format', %L::jsonb, '{a}'::text[])$$ + ]) WITH ORDINALITY f(format, fn) + , unnest('{% key,key %,k%k}'::text[]) WITH ORDINALITY k(key, kn) + ORDER BY fn, kn + ) bad_key_test ; SELECT throws_ok( format( @@ -267,15 +273,15 @@ SELECT throws_ok( ) , format( '%s is not supported as a parameter type', jsonb_typeof(j->'value') ) ) - FROM - unnest(array[ - $$process_language('format', 'template', %L::jsonb)$$ - , $$extract_parameters('format', %L::jsonb, '{a}'::text[])$$ - ]) f(format) - , (SELECT row_to_json(v)::jsonb AS j FROM (VALUES - ('{"key":"value"}'::json) - , (to_json(array[1,2,3])) - ) v(value) ) j + FROM ( + SELECT format, jsonb_build_object('value', value) AS j + FROM unnest(array[ + $$process_language('format', 'template', %L::jsonb)$$ + , $$extract_parameters('format', %L::jsonb, '{a}'::text[])$$ + ]) WITH ORDINALITY f(format, fn) + , unnest(array['{"key":"value"}'::json, to_json(array[1,2,3])]) WITH ORDINALITY v(value, vn) + ORDER BY fn, vn + ) bad_type_test ; \i test/pgxntool/finish.sql From 6b06e222499e4241de606eccffd106062a564fb4 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 14:37:57 -0500 Subject: [PATCH 04/11] Scope terse verbosity to extension-creation statements Setting it in test/deps.sql applied it suite-wide. Only the statements that create extensions emit the deprecation warning whose LOCATION line varies by PostgreSQL version, so wrap just those. Co-Authored-By: Claude Sonnet 5.5 --- test/deps.sql | 9 +++++---- test/load.sql | 3 +++ test/sql/build.sql | 3 +++ test/sql/reload.sql | 6 ++++++ test/sql/search_path.sql | 6 ++++++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/test/deps.sql b/test/deps.sql index 9885ee5..209a8f3 100644 --- a/test/deps.sql +++ b/test/deps.sql @@ -1,11 +1,11 @@ -- Note: pgTap is loaded by setup.sql --- Terse: verbose output adds a LOCATION line (pl_exec.c line number) to trunklet's --- deprecation warning when the extension loads, which differs between PostgreSQL versions -\set VERBOSITY terse - -- Add any test dependency statements here --CREATE EXTENSION IF NOT EXISTS trunklet; +-- Terse around statements that create extensions: verbose output adds a LOCATION line +-- (pl_exec.c line number, varies by PostgreSQL version) to extension_drop's deprecation warning +\set saved_verbosity :VERBOSITY +\set VERBOSITY terse DO $$ BEGIN IF current_setting('server_version_num')::int < 100000 THEN CREATE EXTENSION IF NOT EXISTS cat_tools; @@ -13,6 +13,7 @@ DO $$ BEGIN CREATE EXTENSION IF NOT EXISTS extension_drop; END IF; END$$; +\set VERBOSITY :saved_verbosity -- Have to reset our search_path --SET search_path = public,tap; diff --git a/test/load.sql b/test/load.sql index 4d97909..7910163 100644 --- a/test/load.sql +++ b/test/load.sql @@ -1,6 +1,9 @@ \i test/deps.sql +\set saved_verbosity :VERBOSITY +\set VERBOSITY terse SELECT pg_temp._create(); +\set VERBOSITY :saved_verbosity -- BUG: someone is screwing up the search path... SET search_path = public, tap; diff --git a/test/sql/build.sql b/test/sql/build.sql index beb97ad..86bf8da 100644 --- a/test/sql/build.sql +++ b/test/sql/build.sql @@ -5,7 +5,10 @@ BEGIN; \i test/deps.sql -- This is just an easy way to get the dependencies pulled in on 10.0+ +\set saved_verbosity :VERBOSITY +\set VERBOSITY terse SELECT pg_temp._create(); +\set VERBOSITY :saved_verbosity DROP EXTENSION "trunklet-format"; \i sql/trunklet-format.sql diff --git a/test/sql/reload.sql b/test/sql/reload.sql index b0039c0..eb3fa0d 100644 --- a/test/sql/reload.sql +++ b/test/sql/reload.sql @@ -12,10 +12,13 @@ SELECT plan( --SET client_min_messages = WARNING; -- Necessary due to CASCADE -- Drop-reload +\set saved_verbosity :VERBOSITY +\set VERBOSITY terse SELECT lives_ok( 'SELECT pg_temp._create();' , 'Create extension' ); +\set VERBOSITY :saved_verbosity -- We assume this works, so test it... SELECT throws_ok( $throws$ @@ -34,10 +37,13 @@ SELECT lives_ok( 'DROP EXTENSION "trunklet-format";' , 'Drop extension' ); +\set saved_verbosity :VERBOSITY +\set VERBOSITY terse SELECT lives_ok( 'SELECT pg_temp._create();' , 'Create extension' ); +\set VERBOSITY :saved_verbosity SET client_min_messages = NOTICE; -- Can switch back now that we're done creating extensions -- Drop with registered template diff --git a/test/sql/search_path.sql b/test/sql/search_path.sql index ef0bcda..e7b0218 100644 --- a/test/sql/search_path.sql +++ b/test/sql/search_path.sql @@ -13,7 +13,10 @@ CREATE TEMP TABLE before AS SELECT current_setting('search_path') ; +\set saved_verbosity :VERBOSITY +\set VERBOSITY terse SELECT pg_temp._create(); +\set VERBOSITY :saved_verbosity CREATE TEMP TABLE after AS SELECT current_setting('search_path') @@ -34,7 +37,10 @@ CREATE TEMP TABLE before AS SELECT current_setting('search_path') ; +\set saved_verbosity :VERBOSITY +\set VERBOSITY terse SELECT pg_temp._create(); +\set VERBOSITY :saved_verbosity CREATE TEMP TABLE after AS SELECT current_setting('search_path') From 91c6bbc98847803e3d3bdad6748ae5f34755e125 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 15:06:12 -0500 Subject: [PATCH 05/11] Adopt the stable pseudo-version Set default_version and the META.json versions to `stable`, so editing the source regenerates the gitignored sql/trunklet-format--stable.sql instead of a released version's install script. Restore sql/trunklet-format--0.2.0.sql to the script that shipped as 0.2.0 (identical to the PGXN 0.2.0 release), and add the 0.2.0--stable update script, which brings a 0.2.0 install up to the current source. Updating from 0.2.0 needs extension_drop created first, because 0.2.0 did not require it; HISTORY.asc and README.md say so. Co-Authored-By: Claude Opus 5.5 --- HISTORY.asc | 12 +- META.in.json | 4 +- META.json | 4 +- README.md | 6 + sql/trunklet-format--0.2.0--stable.sql | 307 +++++++++++++++++++++++++ sql/trunklet-format--0.2.0.sql | 177 ++++++++++++++ trunklet-format.control | 2 +- 7 files changed, 505 insertions(+), 7 deletions(-) create mode 100644 sql/trunklet-format--0.2.0--stable.sql create mode 100644 sql/trunklet-format--0.2.0.sql diff --git a/HISTORY.asc b/HISTORY.asc index 887bce7..36f6154 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -1,3 +1,11 @@ -0.2.1 ------ +STABLE +------ === Fix bug where extension could not be dropped +The extension now requires extension_drop. 0.2.0 did not, so before updating from 0.2.0 run `CREATE EXTENSION extension_drop CASCADE;`, then `ALTER EXTENSION "trunklet-format" UPDATE;`. + +=== Add optional parameters +A parameter written as `%name%Os` or `%name%OL` may be absent from the parameters, in which case it is treated as NULL. Identifiers (`I`) cannot be optional. + +=== Other changes +* `extract_parameters()` returns NULL instead of an error when none of the requested parameters are present. +* Template parse errors report the position in the template. diff --git a/META.in.json b/META.in.json index f62b8a1..18d80d7 100644 --- a/META.in.json +++ b/META.in.json @@ -16,7 +16,7 @@ "name": "trunklet-format", "X_comment": "REQUIRED. Version of the distribution. http://pgxn.org/spec/#version", - "version": "0.2.0", + "version": "stable", "X_comment": "REQUIRED. Short description of distribution.", "abstract": "A format()-based template language for trunklet", @@ -41,7 +41,7 @@ "file": "sql/trunklet-format.sql", "X_comment": "REQUIRED. Version the extension is at.", - "version": "0.2.0", + "version": "stable", "X_comment": "Optional: \"abstract\": Description of the extension.", "abstract": "A format()-based template language for trunklet", diff --git a/META.json b/META.json index 94b02ca..df07d53 100644 --- a/META.json +++ b/META.json @@ -16,7 +16,7 @@ "name": "trunklet-format", "X_comment": "REQUIRED. Version of the distribution. http://pgxn.org/spec/#version", - "version": "0.2.0", + "version": "stable", "X_comment": "REQUIRED. Short description of distribution.", "abstract": "A format()-based template language for trunklet", @@ -41,7 +41,7 @@ "file": "sql/trunklet-format.sql", "X_comment": "REQUIRED. Version the extension is at.", - "version": "0.2.0", + "version": "stable", "X_comment": "Optional: \"abstract\": Description of the extension.", "abstract": "A format()-based template language for trunklet", diff --git a/README.md b/README.md index af242ac..78ed3f7 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,12 @@ super user and running: CREATE EXTENSION "trunklet-format"; +To update an existing 0.2.0 install, first create extension_drop, which 0.2.0 +did not require: + + CREATE EXTENSION extension_drop CASCADE; + ALTER EXTENSION "trunklet-format" UPDATE; + If you want to install trunklet-format and all of its supporting objects into a specific schema, use the `PGOPTIONS` environment variable to specify the schema, like so: diff --git a/sql/trunklet-format--0.2.0--stable.sql b/sql/trunklet-format--0.2.0--stable.sql new file mode 100644 index 0000000..a7b8b24 --- /dev/null +++ b/sql/trunklet-format--0.2.0--stable.sql @@ -0,0 +1,307 @@ +/* + * 0.2.0 could not be dropped. Register the same drop command the install + * script does; this is why extension_drop is now a required extension. + */ +SELECT extension_drop__add( + 'trunklet-format' + -- WARNING! Our language name is format, not trunklet-format! + , $sql$SELECT trunklet.template_language__remove('format', ignore_missing_functions := true)$sql$ +); + +/* + * trunklet has no public API to replace a language, and removing then + * re-adding it fails on stored templates (or, with cascade, deletes them), so + * update trunklet's private tables and functions the way + * template_language__add() does. + */ +SELECT _trunklet.language__get_id('format'); -- Throws if the language is missing + +UPDATE _trunklet.language + SET process_function_body = $process_body$ +DECLARE + c_param CONSTANT jsonb := parameters::jsonb; + v_return text; +BEGIN + -- DUPLICATED IN BOTH FUNCTIONS + IF jsonb_typeof(c_param) <> 'object' THEN + RAISE EXCEPTION 'parameters must be a JSON object, not %', jsonb_typeof(c_param) + USING DETAIL = format('parameters = %L', c_param) + ; + END IF; + + -- Sanity-check parameters + -- DUPLICATED IN BOTH FUNCTIONS + DECLARE + parameter_name text; + value text; + v_type text; + BEGIN + FOR parameter_name, value IN SELECT * FROM jsonb_each( c_param ) + LOOP + IF parameter_name ~ '%' THEN + RAISE EXCEPTION + USING DETAIL = 'parameter_name ' || parameter_name + -- MESSAGE instead of trying to escape % + , MESSAGE = 'parameter names may not contain "%"' + ; + END IF; + + v_type := jsonb_typeof( c_param->parameter_name ); + IF v_type IN ( 'array', 'object' ) THEN + RAISE EXCEPTION '% is not supported as a parameter type', v_type + USING DETAIL = format( $$paramater %s = %s$$, parameter_name, value ) + ; + END IF; + END LOOP; + END; + + /* + * Look for things to replace + */ + DECLARE + c_parse CONSTANT text[] := string_to_array( template::text, '%' ); + c_alen CONSTANT int := array_length( c_parse, 1 ); + + /* + * TODO: Instead of trying to track exact template position, it would be + * easier to just track the accumulated length of every c_parse element + * we've seen. That can be converted to template position by simply adding + * (v_pos-1) * length('%'). + */ + v_template_pos int := 1 + length(c_parse[1]); -- Start at 1 and account for c_parse[1] + + v_pos int := 2; -- We handle first element specially + v_format_option_position int; + v_last_parameter_position int; + v_cur text = ''; -- Needs to start at '' for template position tracking + v_parameter text; + v_optional boolean; + v_format_option text; + v_value text; + BEGIN + /* + * We have to special-case the first element because we know it must be + * plain text with no paramaters, or empty if the template starts with '%%'. + */ + v_return := c_parse[1]; + WHILE v_pos <= c_alen LOOP + v_template_pos := v_template_pos + 1; -- We just consumed a % + v_cur := c_parse[v_pos]; + --RAISE WARNING 'v_template_pos = %, v_pos = %, v_cur = %', v_template_pos, v_pos, v_cur; + + /* + * The %% escape gives us an empty v_cur. + */ + IF v_cur = '' THEN + /* + * If we have the template '1 %% 2 %param%s' then when we hit this + * condition we're 'inside' the %% (v_pos = 2, v_cur = ''). The code + * below this IF block depends on v_cur being a parameter name. This + * means we have to consume the NEXT array element (v_pos = 3) before + * continuing, because it could ALSO be empty (ie: '%%%param%s'). It + * could NOT be a parameter though. + */ + + -- Update our position + v_template_pos = v_template_pos + length(c_parse[v_pos + 1]) + 1; + -- v_cur will immediately be reset + + -- Append the % and the next array element + v_return := v_return || '%' || c_parse[v_pos + 1]; + v_pos := v_pos + 2; + CONTINUE; + END IF; + + /* + * Since we don't allow % in a parameter name, we know that v_cur IS THE NAME OF A PARAMETER. + */ + v_last_parameter_position := v_template_pos - 1; -- -1 for the '%' + v_parameter := v_cur; + v_pos := v_pos + 1; + IF v_pos > c_alen THEN + RAISE EXCEPTION 'Reached end of template while processing parameters' + USING HINT = E'Parameters must match the format "%parameter_name%[O]T" where T is s, I or L\n' + || 'and O (if present) means the parameter is optional.' + ; + END IF; + + v_template_pos = v_template_pos + length(v_cur) + 1; + v_cur := c_parse[v_pos]; + --RAISE WARNING 'v_template_pos = %, v_pos = %, v_cur = %', v_template_pos, v_pos, v_cur; + /* + * At this point v_cur IS THE OPTIONS SPECIFIER + */ + + -- This is where to insert support for other format option handling + -- These ugly shenanigans are to avoid copying a potentially large string multiple times + v_optional := substr( v_cur, 1, 1 ) = 'O'; + v_format_option_position := CASE v_optional WHEN true THEN 2 WHEN false THEN 1 END; + v_template_pos = v_template_pos + v_format_option_position - 1; -- -1 because we haven't technically consumed the format specifier yet + v_format_option = substr( v_cur, v_format_option_position, 1 ); + --RAISE WARNING 'FORMAT OPTION %, v_template_pos = %, v_pos = %, v_format_option_position = %, v_cur = %', v_format_option, v_template_pos, v_pos, v_format_option_position, v_cur; + + --RAISE WARNING 'v_pos = %, v_cur = %, v_format_option = %', v_pos, v_cur, v_format_option; + IF v_format_option NOT IN ( 's', 'I', 'L' ) THEN + RAISE EXCEPTION 'Unexpected character "%" in format specifier "%"' + , v_format_option + , substr(v_cur, 1, v_format_option_position ) + USING DETAIL = format( 'parameter "%s" at template position %s', v_parameter, v_last_parameter_position ) + ; + END IF; + + IF v_optional THEN + IF v_format_option = 'I' THEN + RAISE EXCEPTION 'SQL identifier format option ("I") not allowed with optional parameters' + USING ERRCODE = 'null_value_not_allowed' + , HINT = 'SQL identifiers can not be NULL or empty, so identifier formats may not be optional.' + , DETAIL = format( 'parameter "%s" at template position %s', v_parameter, v_last_parameter_position ) + ; + END IF; + -- c_param->v_parameter will ONLY be NULL if v_parameter doesn't exist in the document + ELSIF (c_param->v_parameter) IS NULL THEN + RAISE EXCEPTION 'parameter "%" not found', v_parameter + USING DETAIL = format( 'at template position %s', v_last_parameter_position ) + ; + END IF; + + + -- Now that we've consumed the format specifier, update v_template_pos and grab our value + v_template_pos := v_template_pos + 1; + v_value := c_param->>v_parameter; + + /* + RAISE WARNING 'v_template_pos = %, v_pos = %, v_parameter = %, v_format_option = %, v_value = %, v_format_option_position = %' + , v_template_pos, v_pos, v_parameter, v_format_option, v_value, v_format_option_position + USING DETAIL = format( + 'prior "%s", next: "%s"' + , substr(template, v_template_pos-20, 20) + , substr(template, v_template_pos, 20) + ) + ; + */ + v_return := v_return + -- This is the actual replacement + || format( + '%' || v_format_option + , v_value + ) + + -- Remainder of document, up to next '%' + || substr( v_cur, v_format_option_position + 1 ) + ; + v_template_pos := v_template_pos + length(v_cur) - v_format_option_position; + v_pos := v_pos + 1; + END LOOP; + END; + + RETURN v_return; +END +$process_body$ + , extract_parameters_body = $extract_body$ +DECLARE + c_param CONSTANT jsonb := parameters::jsonb; + a_as text[]; + sql text; + r record; +BEGIN + -- DUPLICATED IN BOTH FUNCTIONS + IF jsonb_typeof(c_param) <> 'object' THEN + RAISE EXCEPTION 'parameters must be a JSON object, not %', jsonb_typeof(c_param) + USING DETAIL = format('parameters = %L', c_param) + ; + END IF; + + -- Sanity-check parameters + -- DUPLICATED IN BOTH FUNCTIONS + DECLARE + parameter_name text; + value text; + v_type text; + BEGIN + FOR parameter_name, value IN SELECT * FROM jsonb_each( c_param ) + LOOP + IF parameter_name ~ '%' THEN + RAISE EXCEPTION + USING DETAIL = 'parameter_name ' || parameter_name + -- MESSAGE instead of trying to escape % + , MESSAGE = 'parameter names may not contain "%"' + ; + END IF; + + v_type := jsonb_typeof( c_param->parameter_name ); + IF v_type IN ( 'array', 'object' ) THEN + RAISE EXCEPTION '% is not supported as a parameter type', v_type + USING DETAIL = format( $$paramater %s = %s$$, parameter_name, value ) + ; + END IF; + + -- THIS IS SPECIFIC TO extract_parametrs + IF parameter_name = ANY( extract_list ) THEN + a_as := a_as || format( + '%I %s' + , parameter_name + , CASE + WHEN v_type IN( 'string', 'null' ) THEN 'text' + WHEN v_type = 'number' THEN 'numeric' + -- array and object can't happen + ELSE v_type + END + ); + END IF; + END LOOP; + END; + + -- Nothing matched + IF a_as IS NULL THEN + RETURN NULL; + END IF; + + sql := 'SELECT * FROM jsonb_to_record($1) AS j(' || array_to_string( a_as, ', ' ) || ')'; + RAISE DEBUG 'sql = %', sql; + EXECUTE sql INTO r USING c_param; + RETURN row_to_json(r)::jsonb; +END +$extract_body$ + WHERE language_name = 'format' +; + +SELECT _trunklet._language_function__create( + language_id + , language_name + , 'process' + , 'text' + , format( + $args$ + template %s + , parameters %s +$args$ + , template_type + , parameter_type + ) + , process_function_options + , process_function_body + ) + FROM _trunklet.language + WHERE language_name = 'format' +; + +SELECT _trunklet._language_function__create( + language_id + , language_name + , 'extract_parameters' + , parameter_type::text + , format( + $args$ + parameters %s + , extract_list text[] +$args$ + , parameter_type + ) + , extract_parameters_options + , extract_parameters_body + ) + FROM _trunklet.language + WHERE language_name = 'format' +; + +-- vi: expandtab ts=2 sw=2 diff --git a/sql/trunklet-format--0.2.0.sql b/sql/trunklet-format--0.2.0.sql new file mode 100644 index 0000000..c3bb967 --- /dev/null +++ b/sql/trunklet-format--0.2.0.sql @@ -0,0 +1,177 @@ +SELECT trunklet.template_language__add( + language_name := 'format' + , parameter_type := 'jsonb' + , template_type := 'text' + , process_function_options := 'LANGUAGE plpgsql' + , process_function_body := $process_body$ +DECLARE + c_param CONSTANT jsonb := parameters::jsonb; + v_return text; +BEGIN + -- DUPLICATED IN BOTH FUNCTIONS + IF jsonb_typeof(c_param) <> 'object' THEN + RAISE EXCEPTION 'parameters must be a JSON object, not %', jsonb_typeof(c_param) + USING DETAIL = 'parameters = ' || c_param + ; + END IF; + + -- Sanity-check parameters + -- DUPLICATED IN BOTH FUNCTIONS + DECLARE + parameter_name text; + value text; + v_type text; + BEGIN + FOR parameter_name, value IN SELECT * FROM jsonb_each( c_param ) + LOOP + IF parameter_name ~ '%' THEN + RAISE EXCEPTION + USING DETAIL = 'parameter_name ' || parameter_name + -- MESSAGE instead of trying to escape % + , MESSAGE = 'parameter names may not contain "%"' + ; + END IF; + + v_type := jsonb_typeof( c_param->parameter_name ); + IF v_type IN ( 'array', 'object' ) THEN + RAISE EXCEPTION '% is not supported as a parameter type', v_type + USING DETAIL = format( $$paramater %s = %s$$, parameter_name, value ) + ; + END IF; + END LOOP; + END; + + /* + * Look for things to replace + */ + DECLARE + c_parse CONSTANT text[] := string_to_array( template::text, '%' ); + c_alen CONSTANT int := array_length( c_parse, 1 ); + v_pos int := 2; -- We handle first element specially + v_cur text; + v_first text; + v_value text; + BEGIN + /* + * We have to special-case the first element because we know it must be + * plain text with no paramaters, or empty if the template starts with '%%'. + */ + v_return := c_parse[1]; + WHILE v_pos <= c_alen LOOP + v_cur := c_parse[v_pos]; + --RAISE WARNING 'v_pos = %, v_cur = %', v_pos, v_cur; + + /* + * The %% escape gives us an empty v_cur. + */ + IF v_cur = '' THEN + -- The next parse element won't be a + /* + * If we have the template '1 %% 2 %param%s' then when we hid this + * condition we're 'inside' the %% (v_pos = 2, v_cur = ''). The code + * below this IF block depends on v_cur being a parameter name. This + * means we have to consume v_pos = 3 before continuing. + */ + v_return := v_return || '%' || c_parse[v_pos + 1]; + v_pos := v_pos + 2; + CONTINUE; + END IF; + + /* + * Since we don't allow % in a parameter name, we know that v_cur is the name of a parameter. + */ + IF (c_param->v_cur) IS NULL THEN + RAISE EXCEPTION 'parameter "%" not found', v_cur + USING DETAIL = 'parse position ' || v_pos + ; + END IF; + v_value := CASE WHEN jsonb_typeof(c_param->v_cur) = 'null' THEN NULL ELSE c_param->>v_cur END; + v_pos := v_pos + 1; + IF v_pos > c_alen THEN + RAISE EXCEPTION 'Reached end of template while processing parameters' + USING HINT = 'Parameters must match the format "%parameter_name%T" where T is s, I or L.' + ; + END IF; + v_cur := c_parse[v_pos]; + + -- This is where to insert support for other format option handling + v_first := substr( v_cur, 1, 1 ); + --RAISE WARNING 'v_pos = %, v_cur = %, v_first = %', v_pos, v_cur, v_first; + IF v_first NOT IN ( 's', 'I', 'L' ) THEN + RAISE EXCEPTION 'Unexpected character "%" trailing parameter "%"', v_first, c_parse[v_pos - 1] + USING DETAIL = format( 'parse position %s, parameter name %s', v_pos, c_parse[v_pos - 1] ) + ; + END IF; + + v_return := v_return || format( '%' || v_first, v_value ) || substr( v_cur, 2 ); + v_pos := v_pos + 1; + END LOOP; + END; + + RETURN v_return; +END +$process_body$ + , extract_parameters_options := 'LANGUAGE plpgsql' + , extract_parameters_body := $extract_body$ +DECLARE + c_param CONSTANT jsonb := parameters::jsonb; + a_as text[]; + sql text; + r record; +BEGIN + -- DUPLICATED IN BOTH FUNCTIONS + IF jsonb_typeof(c_param) <> 'object' THEN + RAISE EXCEPTION 'parameters must be a JSON object, not %', jsonb_typeof(c_param) + USING DETAIL = 'parameters = ' || c_param + ; + END IF; + + -- Sanity-check parameters + -- DUPLICATED IN BOTH FUNCTIONS + DECLARE + parameter_name text; + value text; + v_type text; + BEGIN + FOR parameter_name, value IN SELECT * FROM jsonb_each( c_param ) + LOOP + IF parameter_name ~ '%' THEN + RAISE EXCEPTION + USING DETAIL = 'parameter_name ' || parameter_name + -- MESSAGE instead of trying to escape % + , MESSAGE = 'parameter names may not contain "%"' + ; + END IF; + + v_type := jsonb_typeof( c_param->parameter_name ); + IF v_type IN ( 'array', 'object' ) THEN + RAISE EXCEPTION '% is not supported as a parameter type', v_type + USING DETAIL = format( $$paramater %s = %s$$, parameter_name, value ) + ; + END IF; + + -- THIS IS SPECIFIC TO extract_parametrs + IF parameter_name = ANY( extract_list ) THEN + a_as := a_as || format( + '%I %s' + , parameter_name + , CASE + WHEN v_type IN( 'string', 'null' ) THEN 'text' + WHEN v_type = 'number' THEN 'numeric' + -- array and object can't happen + ELSE v_type + END + ); + END IF; + END LOOP; + END; + + sql := 'SELECT * FROM jsonb_to_record($1) AS j(' || array_to_string( a_as, ', ' ) || ')'; + RAISE DEBUG 'sql = %', sql; + EXECUTE sql INTO r USING c_param; + RETURN row_to_json(r)::jsonb; +END +$extract_body$ +); + +-- vi: expandtab ts=2 sw=2 diff --git a/trunklet-format.control b/trunklet-format.control index fe292bf..4faee9a 100644 --- a/trunklet-format.control +++ b/trunklet-format.control @@ -1,5 +1,5 @@ # trunklet-format extension comment = 'A format()-based template language for trunklet' -default_version = '0.2.0' +default_version = 'stable' relocatable = true requires = 'trunklet, extension_drop' From f993797fa2acb40f4bce5119520f3dc7f84c83fd Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 15:16:49 -0500 Subject: [PATCH 06/11] Require trunklet 0.3.3 and document it for updates from 0.2.0 The drop command registered by the install and update scripts calls trunklet.template_language__remove() with ignore_missing_functions, which trunklet 0.3.3 added, and the update script uses _trunklet._language_function__create(), added in 0.3.0. Raise both trunklet prereqs to 0.3.3, and say in HISTORY.asc and README.md that updating from 0.2.0 needs trunklet updated first. Co-Authored-By: Claude Opus 5.5 --- HISTORY.asc | 2 +- META.in.json | 6 +++--- META.json | 6 +++--- README.md | 6 ++++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/HISTORY.asc b/HISTORY.asc index 36f6154..c76e7f8 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -1,7 +1,7 @@ STABLE ------ === Fix bug where extension could not be dropped -The extension now requires extension_drop. 0.2.0 did not, so before updating from 0.2.0 run `CREATE EXTENSION extension_drop CASCADE;`, then `ALTER EXTENSION "trunklet-format" UPDATE;`. +The extension now requires extension_drop and trunklet 0.3.3 or later, neither of which 0.2.0 required. To update from 0.2.0 (PostgreSQL 12 or later), first update trunklet with `ALTER EXTENSION trunklet UPDATE;` and run `CREATE EXTENSION extension_drop CASCADE;`, then `ALTER EXTENSION "trunklet-format" UPDATE;`. === Add optional parameters A parameter written as `%name%Os` or `%name%OL` may be absent from the parameters, in which case it is treated as NULL. Identifiers (`I`) cannot be optional. diff --git a/META.in.json b/META.in.json index 18d80d7..67e359c 100644 --- a/META.in.json +++ b/META.in.json @@ -75,20 +75,20 @@ "X_comment": "Requires JSONB, which was released in 9.4", "X_comment": "trunklet currently requires 9.5", "X_comment": "extension_drop: 0.1.1 fixes bug with dependencies", - "X_comment": "Requires support for droping a language in trunklet; released in 0.3.0", + "X_comment": "trunklet 0.3.3: the drop command uses template_language__remove(ignore_missing_functions), added in 0.3.3", "prereqs": { "build": { "requires": { "PostgreSQL": "9.5.0", "extension_drop": ">= 0.1.1", - "trunklet": ">= 0.3.0" + "trunklet": ">= 0.3.3" } }, "runtime": { "requires": { "plpgsql": 0, - "trunklet": ">= 0.2.0" + "trunklet": ">= 0.3.3" } }, "test": { diff --git a/META.json b/META.json index df07d53..4475eb6 100644 --- a/META.json +++ b/META.json @@ -75,20 +75,20 @@ "X_comment": "Requires JSONB, which was released in 9.4", "X_comment": "trunklet currently requires 9.5", "X_comment": "extension_drop: 0.1.1 fixes bug with dependencies", - "X_comment": "Requires support for droping a language in trunklet; released in 0.3.0", + "X_comment": "trunklet 0.3.3: the drop command uses template_language__remove(ignore_missing_functions), added in 0.3.3", "prereqs": { "build": { "requires": { "PostgreSQL": "9.5.0", "extension_drop": ">= 0.1.1", - "trunklet": ">= 0.3.0" + "trunklet": ">= 0.3.3" } }, "runtime": { "requires": { "plpgsql": 0, - "trunklet": ">= 0.2.0" + "trunklet": ">= 0.3.3" } }, "test": { diff --git a/README.md b/README.md index 78ed3f7..50d45b2 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,11 @@ super user and running: CREATE EXTENSION "trunklet-format"; -To update an existing 0.2.0 install, first create extension_drop, which 0.2.0 -did not require: +To update an existing 0.2.0 install (PostgreSQL 12 or later), first update +trunklet to 0.3.3 or later and create extension_drop, neither of which 0.2.0 +required: + ALTER EXTENSION trunklet UPDATE; CREATE EXTENSION extension_drop CASCADE; ALTER EXTENSION "trunklet-format" UPDATE; From 9924aef7f14aa4405dd81e592870b279c0e5b2c3 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 15:18:32 -0500 Subject: [PATCH 07/11] Spell out the trunklet 0.2.0 case for updates from 0.2.0 trunklet has no update script from 0.2.0, so ALTER EXTENSION trunklet UPDATE only works from trunklet 0.2.1 or later. Co-Authored-By: Claude Opus 5.5 --- HISTORY.asc | 5 ++++- README.md | 10 +++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/HISTORY.asc b/HISTORY.asc index c76e7f8..1c3e53b 100644 --- a/HISTORY.asc +++ b/HISTORY.asc @@ -1,7 +1,10 @@ STABLE ------ === Fix bug where extension could not be dropped -The extension now requires extension_drop and trunklet 0.3.3 or later, neither of which 0.2.0 required. To update from 0.2.0 (PostgreSQL 12 or later), first update trunklet with `ALTER EXTENSION trunklet UPDATE;` and run `CREATE EXTENSION extension_drop CASCADE;`, then `ALTER EXTENSION "trunklet-format" UPDATE;`. +The extension now requires extension_drop and trunklet 0.3.3 or later, neither of which 0.2.0 required. Updating from trunklet-format 0.2.0 (PostgreSQL 12 or later) requires trunklet >= 0.3.3: + +* If you are on trunklet 0.2.1 or later, run `ALTER EXTENSION trunklet UPDATE;` first, then `CREATE EXTENSION extension_drop CASCADE;` and `ALTER EXTENSION "trunklet-format" UPDATE;`. +* trunklet 0.2.0 has no update path to later versions: drop and recreate it (this also drops trunklet-format and any trunklet data), then install trunklet-format at the stable version. === Add optional parameters A parameter written as `%name%Os` or `%name%OL` may be absent from the parameters, in which case it is treated as NULL. Identifiers (`I`) cannot be optional. diff --git a/README.md b/README.md index 50d45b2..9db7b39 100644 --- a/README.md +++ b/README.md @@ -53,14 +53,18 @@ super user and running: CREATE EXTENSION "trunklet-format"; -To update an existing 0.2.0 install (PostgreSQL 12 or later), first update -trunklet to 0.3.3 or later and create extension_drop, neither of which 0.2.0 -required: +Updating an existing trunklet-format 0.2.0 install (PostgreSQL 12 or later) +requires trunklet >= 0.3.3 and extension_drop, neither of which 0.2.0 required. +If you are on trunklet 0.2.1 or later, run: ALTER EXTENSION trunklet UPDATE; CREATE EXTENSION extension_drop CASCADE; ALTER EXTENSION "trunklet-format" UPDATE; +trunklet 0.2.0 has no update path to later versions: drop and recreate it (this +also drops trunklet-format and any trunklet data), then install trunklet-format +at the stable version. + If you want to install trunklet-format and all of its supporting objects into a specific schema, use the `PGOPTIONS` environment variable to specify the schema, like so: From 774831cab28b8a42a877dae93187e3a93133eca6 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 15:19:35 -0500 Subject: [PATCH 08/11] Fail the update from 0.2.0 when trunklet is older than 0.3.3 Co-Authored-By: Claude Opus 5.5 --- sql/trunklet-format--0.2.0--stable.sql | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sql/trunklet-format--0.2.0--stable.sql b/sql/trunklet-format--0.2.0--stable.sql index a7b8b24..1ab33c5 100644 --- a/sql/trunklet-format--0.2.0--stable.sql +++ b/sql/trunklet-format--0.2.0--stable.sql @@ -1,3 +1,17 @@ +-- The drop command below needs trunklet 0.3.3 (ignore_missing_functions); a non-numeric version such as stable passes +DO $$ +DECLARE + v_version CONSTANT text := (SELECT extversion FROM pg_catalog.pg_extension WHERE extname = 'trunklet'); +BEGIN + IF v_version ~ '^[0-9]+(\.[0-9]+)*$' AND string_to_array(v_version, '.')::int[] < '{0,3,3}' THEN + RAISE EXCEPTION 'trunklet >= 0.3.3 required; found %', v_version + USING ERRCODE = 'object_not_in_prerequisite_state' + , HINT = 'Update trunklet to 0.3.3 or later first.' + ; + END IF; +END +$$; + /* * 0.2.0 could not be dropped. Register the same drop command the install * script does; this is why extension_drop is now a required extension. From dbc3fa6ee06f63e2e94902313aeb63c6f7a70fd7 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Sun, 25 Oct 2026 15:24:58 -0500 Subject: [PATCH 09/11] Make the trunklet version-check hint cover trunklet 0.2.0 Co-Authored-By: Claude Opus 5.5 --- sql/trunklet-format--0.2.0--stable.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/trunklet-format--0.2.0--stable.sql b/sql/trunklet-format--0.2.0--stable.sql index 1ab33c5..2f24aa9 100644 --- a/sql/trunklet-format--0.2.0--stable.sql +++ b/sql/trunklet-format--0.2.0--stable.sql @@ -6,7 +6,7 @@ BEGIN IF v_version ~ '^[0-9]+(\.[0-9]+)*$' AND string_to_array(v_version, '.')::int[] < '{0,3,3}' THEN RAISE EXCEPTION 'trunklet >= 0.3.3 required; found %', v_version USING ERRCODE = 'object_not_in_prerequisite_state' - , HINT = 'Update trunklet to 0.3.3 or later first.' + , HINT = 'On trunklet 0.2.1 or later, run ALTER EXTENSION trunklet UPDATE first. trunklet 0.2.0 has no update path; see the trunklet-format README.' ; END IF; END From 5d02776529fd3c7eb382daff4cdcbb03f8bb5f70 Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Mon, 26 Oct 2026 16:07:26 -0500 Subject: [PATCH 10/11] Move ordering comment above ORDER BY in base.sql Co-Authored-By: Claude Sonnet 5.5 --- test/sql/base.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/sql/base.sql b/test/sql/base.sql index 7c92a0d..2da40a4 100644 --- a/test/sql/base.sql +++ b/test/sql/base.sql @@ -93,7 +93,6 @@ This is the end. $$ ); --- Tests read from an explicitly ordered subquery or view so pgTAP test numbers don't depend on join order SELECT is( trunklet.process_language( 'format' @@ -116,6 +115,7 @@ SELECT is( FROM unnest('{s,L}'::text[]) WITH ORDINALITY r(replace, rn) , unnest(array[to_json(1.1), to_json(NULL::int), to_json('text'::text), to_json(true)]) WITH ORDINALITY v(value, vn) + -- Ensure consistent ordering against planner changes ORDER BY rn, vn ) optional_test ; From 08d84836ba2fac33d09c6a91f05a609b9b01d00d Mon Sep 17 00:00:00 2001 From: jnasbyupgrade Date: Mon, 26 Oct 2026 16:11:12 -0500 Subject: [PATCH 11/11] Remove pre-PostgreSQL 10 branches from test/deps.sql The extension_drop/trunklet/cat_tools creates and the non-CASCADE CREATE EXTENSION path only ran on server_version_num < 100000, and the terse-VERBOSITY wrapper existed solely for that block. Co-Authored-By: Claude Sonnet 5.5 --- test/deps.sql | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/test/deps.sql b/test/deps.sql index 209a8f3..f9c6a6b 100644 --- a/test/deps.sql +++ b/test/deps.sql @@ -2,18 +2,6 @@ -- Add any test dependency statements here --CREATE EXTENSION IF NOT EXISTS trunklet; --- Terse around statements that create extensions: verbose output adds a LOCATION line --- (pl_exec.c line number, varies by PostgreSQL version) to extension_drop's deprecation warning -\set saved_verbosity :VERBOSITY -\set VERBOSITY terse -DO $$ BEGIN - IF current_setting('server_version_num')::int < 100000 THEN - CREATE EXTENSION IF NOT EXISTS cat_tools; - CREATE EXTENSION IF NOT EXISTS trunklet; - CREATE EXTENSION IF NOT EXISTS extension_drop; - END IF; -END$$; -\set VERBOSITY :saved_verbosity -- Have to reset our search_path --SET search_path = public,tap; @@ -23,11 +11,7 @@ SET client_min_messages = WARNING -- Necessary due to CASCADE AS $body$ BEGIN -- No IF NOT EXISTS because we'll be confused if we're not loading the new stuff - IF current_setting('server_version_num')::int < 100000 THEN - CREATE EXTENSION "trunklet-format" ; - ELSE - EXECUTE $exec$CREATE EXTENSION "trunklet-format" CASCADE$exec$; - END IF; + CREATE EXTENSION "trunklet-format" CASCADE; END $body$;