- {% include inner_template %}
+ {{ include inner_template }}
- {% name %}
+ {{ name }}
- {% for item in array do
- some text: {% name2 %}
- {% item.name %}
- %}
+ {{ for item in array }}
+ some text: {{ name2 }}
+ {{ item.name }}
+ {{ end }}
code insertion:
- {% execute
+ {{ execute
context + '{"name3": "zalupa"}';
IF context->condition THEN
@@ -168,7 +168,7 @@ Enables calling functions with arguments, or execute code. Have hardcoded contex
RETURN 'some text';
END
RETURN 'some other text';
- %}
+ }}
```
\ No newline at end of file
diff --git a/package/c/hemar/hemar--0.1.sql b/package/c/hemar/hemar--0.1.sql
index b8f5818..f187dbe 100755
--- a/package/c/hemar/hemar--0.1.sql
+++ b/package/c/hemar/hemar--0.1.sql
@@ -13,3 +13,9 @@ CREATE FUNCTION "hemar"."jsonb_get_by_path"("json" jsonb, "path" text)
RETURNS jsonb
LANGUAGE C STRICT
AS 'hemar', 'pg_jsonb_get_by_path';
+
+-- Template rendering function
+CREATE FUNCTION "hemar"."render"("define" jsonb, "template" text)
+RETURNS text
+LANGUAGE C STRICT
+AS 'hemar', 'pg_template_render';
diff --git a/package/c/hemar/hemar.c b/package/c/hemar/hemar.c
index 71a1564..191ab1f 100755
--- a/package/c/hemar/hemar.c
+++ b/package/c/hemar/hemar.c
@@ -21,6 +21,8 @@
/* Forward declarations */
static void template_node_to_string(TemplateNode *node, StringInfo result, int indent);
+static void render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryContext context);
+static void render_execute_tag(const char *code, Jsonb *define, StringInfo result, MemoryContext context);
static const char *
jbt_type_to_string(JsonbIteratorToken type)
@@ -494,9 +496,9 @@ template_parse_section(MemoryContext context, const char **s_ptr,
{
/* This is our matching end tag */
end_tag_start = *s;
- break;
- }
- }
+ break;
+ }
+ }
}
(*s)++;
@@ -679,7 +681,7 @@ template_parse_execute(MemoryContext context, const char **s_ptr,
/* If we've reached the matching closing brace, we're done */
if (brace_level == 0) {
- break;
+ break;
}
*s += strlen(config->Syntax.Braces.close) - 1; /* -1 because we'll increment s below */
@@ -1094,6 +1096,258 @@ template_node_to_string(TemplateNode *node, StringInfo result, int indent)
}
}
+/* Template rendering function */
+PG_FUNCTION_INFO_V1(pg_template_render);
+Datum
+pg_template_render(PG_FUNCTION_ARGS)
+{
+ Jsonb *define = PG_GETARG_JSONB_P(0);
+ text *template_text = PG_GETARG_TEXT_PP(1);
+ char *template_str = text_to_cstring(template_text);
+ const char *template_ptr = template_str;
+ MemoryContext old_context, render_context;
+ TemplateConfig config;
+ TemplateNode *root = NULL;
+ TemplateErrorCode error_code = TEMPLATE_ERROR_NONE;
+ StringInfoData result;
+ text *result_text = NULL;
+
+ /* Create a memory context for rendering */
+ render_context = AllocSetContextCreate(CurrentMemoryContext,
+ "Template Render Context",
+ ALLOCSET_DEFAULT_SIZES);
+
+ /* Switch to the new context for rendering */
+ old_context = MemoryContextSwitchTo(render_context);
+
+ /* Initialize default config */
+ config = template_default_config(render_context);
+
+ PG_TRY();
+ {
+ /* Parse the template */
+ root = template_parse(render_context, &template_ptr, &config, false, &error_code);
+
+ /* Check for parsing errors */
+ if (error_code != TEMPLATE_ERROR_NONE || !root)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("Template parsing error: %s", template_error_to_string(error_code, &config))));
+ }
+
+ /* Initialize the result buffer */
+ initStringInfo(&result);
+
+ /* Render the template */
+ render_template(root, define, &result, render_context);
+
+ /* Switch back to the original memory context */
+ MemoryContextSwitchTo(old_context);
+
+ /* Return the result */
+ result_text = cstring_to_text(result.data);
+ pfree(result.data);
+ }
+ PG_CATCH();
+ {
+ /* Switch back to the original memory context for error handling */
+ MemoryContextSwitchTo(old_context);
+
+ /* Clean up */
+ if (template_str)
+ pfree(template_str);
+
+ /* Delete the render context */
+ MemoryContextDelete(render_context);
+
+ PG_RE_THROW();
+ }
+ PG_END_TRY();
+
+ /* Clean up */
+ MemoryContextDelete(render_context);
+ pfree(template_str);
+
+ PG_RETURN_TEXT_P(result_text);
+}
+
+/* Helper function to render a template node */
+static void
+render_template(TemplateNode *node, Jsonb *define, StringInfo result, MemoryContext context)
+{
+ TemplateNode *current = node;
+
+ while (current)
+ {
+ switch (current->type)
+ {
+ case TEMPLATE_NODE_TEXT:
+ if (current->value->text.content)
+ appendStringInfoString(result, current->value->text.content);
+ break;
+
+ case TEMPLATE_NODE_EXECUTE:
+ render_execute_tag(current->value->execute.code, define, result, context);
+ break;
+
+ /* We'll implement these later */
+ case TEMPLATE_NODE_INTERPOLATE:
+ case TEMPLATE_NODE_SECTION:
+ case TEMPLATE_NODE_INCLUDE:
+ default:
+ /* Skip for now */
+ break;
+ }
+
+ current = current->next;
+ }
+}
+
+/* Helper function to calculate a simple hash of a string */
+static uint32_t
+calculate_string_hash(const char *str)
+{
+ uint32_t hash = 5381;
+ int c;
+
+ while ((c = *str++))
+ hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
+
+ return hash;
+}
+
+/* Helper function to render an execute tag */
+static void
+render_execute_tag(const char *code, Jsonb *define, StringInfo result, MemoryContext context)
+{
+ int ret;
+ StringInfoData query;
+ StringInfoData exec_result;
+ char *trimmed_code;
+ size_t code_len;
+ uint32_t code_hash;
+ char func_name[64];
+ bool isnull;
+ bool function_exists;
+
+ /* Connect to SPI */
+ if ((ret = SPI_connect()) < 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_CONNECTION_EXCEPTION),
+ errmsg("SPI_connect failed: %s", SPI_result_code_string(ret))));
+
+ /* Create the query with the context variable */
+ initStringInfo(&query);
+ initStringInfo(&exec_result);
+
+ /* Trim trailing semicolon if present to avoid double semicolons */
+ code_len = strlen(code);
+ trimmed_code = pstrdup(code);
+ while (code_len > 0 && (trimmed_code[code_len-1] == ';' || isspace((unsigned char)trimmed_code[code_len-1]))) {
+ trimmed_code[--code_len] = '\0';
+ }
+
+ /* Calculate hash of the code */
+ code_hash = calculate_string_hash(trimmed_code);
+ snprintf(func_name, sizeof(func_name), "cache-%x", code_hash);
+
+ /* Check if function exists */
+ appendStringInfo(&query,
+ "SELECT EXISTS (SELECT 1 FROM pg_proc p "
+ "JOIN pg_namespace n ON p.pronamespace = n.oid "
+ "WHERE n.nspname = 'hemar' AND p.proname = '%s');",
+ func_name);
+
+ ret = SPI_execute(query.data, true, 0);
+ if (ret != SPI_OK_SELECT)
+ {
+ SPI_finish();
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("Failed to check function existence: %s", SPI_result_code_string(ret))));
+ }
+
+ function_exists = DatumGetBool(SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isnull));
+
+ /* Reset query buffer for function creation */
+ resetStringInfo(&query);
+
+ /* Only create function if it doesn't exist */
+ if (!function_exists)
+ {
+ elog(NOTICE, "Caching function %s", func_name);
+ elog(DEBUG1, "Content: %s", trimmed_code);
+
+ appendStringInfo(&query,
+ "CREATE OR REPLACE FUNCTION \"hemar\".\"%s\"(context jsonb) RETURNS text LANGUAGE plpgsql AS $$ "
+ "BEGIN "
+ " %s; "
+ " RETURN '';" // NOTICE(yukkop): Trailing return in case user does not return anything
+ "END $$;",
+ func_name,
+ trimmed_code);
+
+ /* Execute the query */
+ ret = SPI_execute(query.data, false, 0);
+
+ if (ret != SPI_OK_UTILITY)
+ {
+ SPI_finish();
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("Failed to execute SQL in template: %s", SPI_result_code_string(ret))));
+ }
+ }
+
+ /* Reset query buffer for function execution */
+ resetStringInfo(&query);
+
+ /* Execute the function */
+ appendStringInfo(&query, "SELECT \"hemar\".\"%s\"($1);", func_name);
+
+ /* Prepare arguments for SPI_execute_with_args */
+ Oid argtypes[1] = {JSONBOID};
+ Datum argvalues[1] = {JsonbPGetDatum(define)};
+
+ ret = SPI_execute_with_args(query.data, 1, argtypes, argvalues, NULL, true, 0);
+
+ if (ret != SPI_OK_SELECT)
+ {
+ SPI_finish();
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("Failed to execute function: %s", SPI_result_code_string(ret))));
+ }
+
+ /* Get the result */
+ if (SPI_processed > 0)
+ {
+ Datum content = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isnull);
+
+ if (!isnull)
+ {
+ char *content_str = TextDatumGetCString(content);
+ appendStringInfoString(&exec_result, content_str);
+ pfree(content_str);
+ }
+ }
+
+ /* Append any captured output to the result */
+ if (exec_result.len > 0)
+ {
+ appendStringInfoString(result, exec_result.data);
+ }
+
+ /* Clean up */
+ pfree(query.data);
+ pfree(exec_result.data);
+ pfree(trimmed_code);
+
+ /* Disconnect from SPI */
+ SPI_finish();
+}
+
/* Function declarations */
PG_FUNCTION_INFO_V1(pg_jsonb_get_by_path);
diff --git a/package/c/hemar/test/mod.sql b/package/c/hemar/test/mod.sql
new file mode 100755
index 0000000..9ee06ff
--- /dev/null
+++ b/package/c/hemar/test/mod.sql
@@ -0,0 +1,5 @@
+BEGIN;
+ \ir test_jsonb_path.sql
+ \ir test_template_parser.sql
+ \ir test_render_exec.sql
+ROLLBACK;
\ No newline at end of file
diff --git a/package/c/hemar/test/test_jsonb_path.sql b/package/c/hemar/test/test_jsonb_path.sql
index d247438..286a6c7 100755
--- a/package/c/hemar/test/test_jsonb_path.sql
+++ b/package/c/hemar/test/test_jsonb_path.sql
@@ -482,12 +482,12 @@ BEGIN
-- Print summary
IF passed_tests = total_tests THEN
RAISE NOTICE '------------------------------------';
- RAISE NOTICE 'SUMMARY: % of % tests passed (100%%)',
+ RAISE NOTICE 'SUMMARY: % of % jsonb_get_by_path tests passed (100%%)',
passed_tests, total_tests;
RAISE NOTICE '------------------------------------';
ELSE
RAISE WARNING '------------------------------------';
- RAISE WARNING 'SUMMARY: % of % tests passed (%)',
+ RAISE WARNING 'SUMMARY: % of % jsonb_get_by_path tests passed (%)',
passed_tests,
total_tests,
round((passed_tests::numeric / total_tests::numeric) * 100, 2) || '%';
@@ -495,6 +495,6 @@ BEGIN
END IF;
IF passed_tests != total_tests THEN
- RAISE EXCEPTION 'Tests failed: % of % tests did not pass', (total_tests - passed_tests), total_tests;
+ RAISE EXCEPTION 'Tests failed: % of % jsonb_get_by_path tests did not pass', (total_tests - passed_tests), total_tests;
END IF;
-END $$;
+END $$;
\ No newline at end of file
diff --git a/package/c/hemar/test/test_render_exec.sql b/package/c/hemar/test/test_render_exec.sql
new file mode 100755
index 0000000..a122c0a
--- /dev/null
+++ b/package/c/hemar/test/test_render_exec.sql
@@ -0,0 +1,343 @@
+-- Test the render function with execute tags
+CREATE EXTENSION IF NOT EXISTS hemar;
+
+DO $$
+DECLARE
+ total_tests INT := 0;
+ passed_tests INT := 0;
+ test_result TEXT;
+ expected TEXT;
+ passed BOOLEAN;
+BEGIN
+ -- Test 1: Simple execute tag that sets a variable
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"name": "John", "age": 30}'::jsonb,
+ 'Hello {{ exec PERFORM 1; }}'
+ );
+ expected := 'Hello ';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Simple execute tag: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Simple execute tag: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Test 2: Execute tag with context access
+ total_tests := total_tests + 1;
+ DROP TABLE IF EXISTS test_output;
+ CREATE TEMP TABLE test_output (value TEXT);
+
+ test_result := hemar.render(
+ '{"name": "John", "age": 30}'::jsonb,
+ $expected$Hello {{ exec INSERT INTO test_output VALUES (context->'name'); }}$expected$
+ );
+
+ SELECT value INTO expected FROM test_output;
+ passed := expected = '"John"';
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with context access: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with context access: FAILED. Expected "John", got "%"',
+ total_tests, expected;
+ END IF;
+
+ -- Test 3: Execute tag with quotes and complex SQL
+ total_tests := total_tests + 1;
+ DROP TABLE IF EXISTS test_output;
+ CREATE TEMP TABLE test_output (value TEXT);
+
+ test_result := hemar.render(
+ '{"items": [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]}'::jsonb,
+ $expected$Items: {{ exec
+ INSERT INTO test_output
+ SELECT jsonb_array_elements(context->'items')->>'name';
+ }}$expected$
+ );
+
+ SELECT string_agg(value, ', ' ORDER BY value) INTO expected FROM test_output;
+ passed := expected = 'Item 1, Item 2';
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with complex SQL: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with complex SQL: FAILED. Expected "Item 1, Item 2", got "%"',
+ total_tests, expected;
+ END IF;
+
+ -- Test 4: Execute tag with output capture
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"name": "John", "age": 30}'::jsonb,
+ $expected$Hello {{ exec RETURN context->>'name'; }}$expected$
+ );
+ expected := 'Hello John';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with output capture: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with output capture: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Test 5: Execute tag with complex output
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"items": [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]}'::jsonb,
+ $expected$Items: {{ exec
+ RETURN (SELECT string_agg(value, ', ')
+ FROM (
+ SELECT jsonb_array_elements(context->'items')->>'name' as value
+ ) t);
+ }}$expected$
+ );
+ expected := 'Items: Item 1, Item 2';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with complex output: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with complex output: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Test 6: Execute tag with multiple statements
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"name": "John", "age": 30}'::jsonb,
+ $expected$Hello {{ exec
+ DECLARE
+ v_name TEXT;
+ BEGIN
+ v_name := context->>'name';
+ RETURN v_name;
+ END;
+ }}$expected$
+ );
+ expected := 'Hello John';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with multiple statements: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with multiple statements: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Test 7: Execute tag with array operations
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"numbers": [1, 2, 3, 4, 5]}'::jsonb,
+ $expected$Sum: {{ exec
+ RETURN (SELECT sum(value::int)
+ FROM jsonb_array_elements_text(context->'numbers') as value);
+ }}$expected$
+ );
+ expected := 'Sum: 15';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with array operations: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with array operations: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Test 8: Execute tag with nested JSON operations
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"user": {"profile": {"settings": {"theme": "dark", "notifications": true}}}}'::jsonb,
+ $expected$Settings: {{ exec
+ RETURN context->'user'->'profile'->'settings'->>'theme';
+ }}$expected$
+ );
+ expected := 'Settings: dark';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with nested JSON operations: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with nested JSON operations: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Test 9: Execute tag with conditional logic
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"age": 25, "country": "US"}'::jsonb,
+ $expected$Status: {{ exec
+ DECLARE
+ v_status TEXT;
+ BEGIN
+ IF (context->>'age')::int >= 21 AND context->>'country' = 'US' THEN
+ v_status := 'Adult in US';
+ ELSE
+ v_status := 'Other';
+ END IF;
+ RETURN v_status;
+ END;
+ }}$expected$
+ );
+ expected := 'Status: Adult in US';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with conditional logic: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with conditional logic: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Test 10: Execute tag with string manipulation
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"text": "hello world"}'::jsonb,
+ $expected$Text: {{ exec
+ RETURN upper(context->>'text');
+ }}$expected$
+ );
+ expected := 'Text: HELLO WORLD';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with string manipulation: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with string manipulation: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Test 11: Execute tag with date operations
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"date": "2024-03-15"}'::jsonb,
+ $expected$Date: {{ exec
+ RETURN to_char((context->>'date')::date, 'Month DD, YYYY');
+ }}$expected$
+ );
+ expected := 'Date: March 15, 2024';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with date operations: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with date operations: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Test 12: Execute tag with aggregation
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"scores": [85, 92, 78, 95, 88]}'::jsonb,
+ $expected$Stats: {{ exec
+ RETURN (SELECT format('Avg: %s, Max: %s',
+ round(avg(value::float)::numeric, 1),
+ max(value::int))
+ FROM jsonb_array_elements_text(context->'scores') as value);
+ }}$expected$
+ );
+ expected := 'Stats: Avg: 87.6, Max: 95';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with aggregation: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with aggregation: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Test 13: Execute tag with error handling
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"value": "not_a_number"}'::jsonb,
+ $expected$Result: {{ exec
+ BEGIN
+ RETURN (context->>'value')::int::text;
+ EXCEPTION WHEN OTHERS THEN
+ RETURN 'Error: Invalid number';
+ END;
+ }}$expected$
+ );
+ expected := 'Result: Error: Invalid number';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with error handling: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with error handling: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Test 14: Execute tag with complex JSON transformation
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"users": [{"name": "Alice", "roles": ["admin", "user"]}, {"name": "Bob", "roles": ["user"]}]}'::jsonb,
+ $expected$Users: {{ exec
+ RETURN (SELECT string_agg(
+ format('%s (%s)',
+ user_data->>'name',
+ (SELECT string_agg(role, ', ')
+ FROM jsonb_array_elements_text(user_data->'roles') as role)
+ ),
+ '; '
+ )
+ FROM jsonb_array_elements(context->'users') as user_data);
+ }}$expected$
+ );
+ expected := 'Users: Alice (admin, user); Bob (user)';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with complex JSON transformation: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with complex JSON transformation: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Test 15: Execute tag with empty/null handling
+ total_tests := total_tests + 1;
+ test_result := hemar.render(
+ '{"name": null, "items": []}'::jsonb,
+ $expected$Result: {{ exec
+ DECLARE
+ v_name TEXT;
+ v_count INT;
+ BEGIN
+ v_name := COALESCE(context->>'name', 'Unknown');
+ v_count := jsonb_array_length(context->'items');
+ RETURN format('Name: %s, Items: %s', v_name, v_count);
+ END;
+ }}$expected$
+ );
+ expected := 'Result: Name: Unknown, Items: 0';
+ passed := test_result = expected;
+ passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
+ IF passed THEN
+ RAISE NOTICE 'Test %: Execute tag with empty/null handling: PASSED', total_tests;
+ ELSE
+ RAISE WARNING 'Test %: Execute tag with empty/null handling: FAILED. Expected "%", got "%"',
+ total_tests, expected, test_result;
+ END IF;
+
+ -- Print summary
+ IF passed_tests = total_tests THEN
+ RAISE NOTICE '------------------------------------';
+ RAISE NOTICE 'SUMMARY: % of % render exec tests passed (100%%)',
+ passed_tests, total_tests;
+ RAISE NOTICE '------------------------------------';
+ ELSE
+ RAISE WARNING '------------------------------------';
+ RAISE WARNING 'SUMMARY: % of % render exec tests passed (%)',
+ passed_tests,
+ total_tests,
+ round((passed_tests::numeric / total_tests::numeric) * 100, 2) || '%';
+ RAISE WARNING '------------------------------------';
+ END IF;
+
+ IF passed_tests != total_tests THEN
+ RAISE EXCEPTION 'Tests failed: % of % render exec tests did not pass', (total_tests - passed_tests), total_tests;
+ END IF;
+END $$;
\ No newline at end of file
diff --git a/package/c/hemar/test/test_template_parser.sql b/package/c/hemar/test/test_template_parser.sql
index f406c06..5d4783d 100755
--- a/package/c/hemar/test/test_template_parser.sql
+++ b/package/c/hemar/test/test_template_parser.sql
@@ -1064,12 +1064,12 @@ SECTION: iterator="section", collection="page.sections"$expected40$
-- Print summary
IF passed_tests = total_tests THEN
RAISE NOTICE '------------------------------------';
- RAISE NOTICE 'SUMMARY: % of % tests passed (100%%)',
+ RAISE NOTICE 'SUMMARY: % of % template parser tests passed (100%%)',
passed_tests, total_tests;
RAISE NOTICE '------------------------------------';
ELSE
RAISE WARNING '------------------------------------';
- RAISE WARNING 'SUMMARY: % of % tests passed (%)',
+ RAISE WARNING 'SUMMARY: % of % template parser tests passed (%)',
passed_tests,
total_tests,
round((passed_tests::numeric / total_tests::numeric) * 100, 2) || '%';
@@ -1077,6 +1077,6 @@ SECTION: iterator="section", collection="page.sections"$expected40$
END IF;
IF passed_tests != total_tests THEN
- RAISE EXCEPTION 'Tests failed: % of % tests did not pass', (total_tests - passed_tests), total_tests;
+ RAISE EXCEPTION 'Tests failed: % of % template parser tests did not pass', (total_tests - passed_tests), total_tests;
END IF;
END $$;
\ No newline at end of file