feat!: vpizdu

This commit is contained in:
2025-05-15 15:33:53 +00:00
parent a770215f0c
commit c28e7fcfd5
3 changed files with 643 additions and 629 deletions

View File

@@ -28,7 +28,32 @@ static Datum get_jsonb_include_template(Datum jsonb_context, const char *key, bo
static void get_include_data(Datum include_data, char **template_out, Datum *context_out); static void get_include_data(Datum include_data, char **template_out, Datum *context_out);
static void template_node_to_string(TemplateNode *node, StringInfo result, int indent); static void template_node_to_string(TemplateNode *node, StringInfo result, int indent);
static bool is_jsonb_container_valid(JsonbContainer *container); static bool is_jsonb_container_valid(JsonbContainer *container);
static JsonbValue *get_jsonb_value_by_path(Jsonb *jb, const char *path, bool *found);
static const char *
jbt_type_to_string(JsonbIteratorToken type)
{
switch (type)
{
case WJB_DONE:
return "WJB_DONE";
case WJB_KEY:
return "WJB_KEY";
case WJB_VALUE:
return "WJB_VALUE";
case WJB_ELEM:
return "WJB_ELEM";
case WJB_BEGIN_ARRAY:
return "WJB_BEGIN_ARRAY";
case WJB_END_ARRAY:
return "WJB_END_ARRAY";
case WJB_BEGIN_OBJECT:
return "WJB_BEGIN_OBJECT";
case WJB_END_OBJECT:
return "WJB_END_OBJECT";
default:
return "Unknown";
}
}
static char * static char *
jbv_type_to_string(enum jbvType type) jbv_type_to_string(enum jbvType type)
@@ -205,8 +230,8 @@ template_default_config(MemoryContext context)
{ {
TemplateConfig config; TemplateConfig config;
config.Syntax.Braces.open = "{%"; config.Syntax.Braces.open = "{{";
config.Syntax.Braces.close = "%}"; config.Syntax.Braces.close = "}}";
config.Syntax.Section.control = "for "; config.Syntax.Section.control = "for ";
config.Syntax.Section.source = "in "; config.Syntax.Section.source = "in ";
config.Syntax.Section.begin = "do "; config.Syntax.Section.begin = "do ";
@@ -915,7 +940,7 @@ template_render(MemoryContext context, TemplateNode *node, Datum jsonb_context,
/* First try to get as a direct path */ /* First try to get as a direct path */
/* Extract value from JSONB context */ /* Extract value from JSONB context */
value = get_jsonb_path_value(jsonb_context, current->value->interpolate.key, &found_interpolate); value = // ??
if (found_interpolate && value) if (found_interpolate && value)
{ {
@@ -1396,120 +1421,6 @@ template_render(MemoryContext context, TemplateNode *node, Datum jsonb_context,
return result.data; return result.data;
} }
/* Helper functions for JSONB handling */
static char *
get_jsonb_path_value(Datum jsonb_context, const char *path, bool *found)
{
Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_context);
JsonbValue *jbv_result;
JsonbIterator *it;
JsonbValue v;
JsonbIteratorToken token;
char *result = NULL;
*found = false;
if (!jb || !path)
{
elog(DEBUG1, "Null JSONB or path in get_jsonb_path_value");
return NULL;
}
/* Check if this is a valid Jsonb before proceeding */
if (!is_jsonb_container_valid(&jb->root))
{
elog(WARNING, "Invalid JSONB container in get_jsonb_path_value");
return NULL;
}
PG_TRY();
{
/* Use the new path traversal function */
jbv_result = get_jsonb_value_by_path(jb, path, found);
if (*found && jbv_result)
{
elog(DEBUG1, "GJVB: Found element (type=%s)", jbv_type_to_string(jbv_result->type));
if (jbv_result->type == jbvString)
{
result = pnstrdup(jbv_result->val.string.val, jbv_result->val.string.len);
elog(DEBUG1, "GJVB: Found string value for key %s: %s", path, result);
}
else if (jbv_result->type == jbvNumeric)
{
Numeric num = jbv_result->val.numeric;
result = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(num)));
elog(DEBUG1, "GJVB: Found numeric value for key %s: %s", path, result);
}
else if (jbv_result->type == jbvBool)
{
result = pstrdup(jbv_result->val.boolean ? "true" : "false");
elog(DEBUG1, "GJVB: Found boolean value for key %s: %s", path, result);
}
else if (jbv_result->type == jbvNull)
{
result = pstrdup("");
elog(DEBUG1, "GJVB: Found null value for key %s", path);
}
else if (jbv_result->type == jbvBinary)
{
/* Check if it's an array first */
if (is_jsonb_container_valid((JsonbContainer *)jbv_result->val.binary.data))
{
it = JsonbIteratorInit((JsonbContainer *)jbv_result->val.binary.data);
token = JsonbIteratorNext(&it, &v, false);
if (token == WJB_BEGIN_ARRAY)
{
/* For arrays, convert to "[Array]" placeholder */
result = pstrdup("[Array]");
elog(DEBUG1, "GJVB: Found array value for key %s", path);
}
else if (token == WJB_BEGIN_OBJECT)
{
/* For objects, convert to "{Object}" placeholder */
result = pstrdup("{Object}");
elog(DEBUG1, "GJVB: Found object value for key %s", path);
}
else
{
/* Convert binary type to string representation */
StringInfoData buf;
initStringInfo(&buf);
appendStringInfoString(&buf, "[Complex Value]");
result = buf.data;
elog(DEBUG1, "GJVB: Found complex value for key %s", path);
}
}
else
{
result = pstrdup("[Invalid Binary]");
elog(DEBUG1, "GJVB: Found invalid binary value for key %s", path);
}
}
}
else
{
elog(DEBUG1, "GJVB: Path %s not found in object", path);
}
}
PG_CATCH();
{
elog(WARNING, "GJVB: Exception while extracting value for path %s", path);
FlushErrorState();
*found = false;
if (result)
{
pfree(result);
result = NULL;
}
}
PG_END_TRY();
return result;
}
static Datum static Datum
get_jsonb_array(Datum jsonb_context, const char *path, bool *found) get_jsonb_array(Datum jsonb_context, const char *path, bool *found)
{ {
@@ -1543,7 +1454,7 @@ get_jsonb_array(Datum jsonb_context, const char *path, bool *found)
{ {
/* Use the path traversal function */ /* Use the path traversal function */
bool path_found = false; bool path_found = false;
jbv_result = get_jsonb_value_by_path(jb, path, &path_found); jbv_result = // TODO: ?;
if (path_found && jbv_result) if (path_found && jbv_result)
{ {
@@ -2188,233 +2099,3 @@ template_node_to_string(TemplateNode *node, StringInfo result, int indent)
current = current->next; current = current->next;
} }
} }
/* Function to get JsonbValue by dot-separated path */
static JsonbValue *
get_jsonb_value_by_path(Jsonb *jb, const char *path, bool *found)
{
JsonbValue *result = NULL;
char *path_copy, *token, *saveptr;
char *array_index_start, *array_index_end;
JsonbValue key;
JsonbContainer *container;
int array_index;
*found = false;
if (!jb || !path || !is_jsonb_container_valid(&jb->root))
{
elog(DEBUG1, "GJVB: Invalid JSONB or path in get_jsonb_value_by_path");
return NULL;
}
/* Make a copy of the path to tokenize */
path_copy = pstrdup(path);
container = &jb->root;
/* Use strtok_r to split the path by dots */
token = strtok_r(path_copy, ".", &saveptr);
while (token != NULL)
{
elog(DEBUG1, "GJVB: Processing token: %s", token);
/* Check if we're dealing with an array index: something[N] */
array_index_start = strchr(token, '[');
elog(DEBUG1, "GJVB: Token: %s, array_index_start: %s", token, array_index_start);
if (array_index_start != NULL)
{
/* We have an array index pattern */
array_index_end = strchr(array_index_start, ']');
if (array_index_end == NULL || array_index_end <= array_index_start + 1)
{
elog(DEBUG1, "GJVB: Invalid array index syntax in '%s'", token);
pfree(path_copy);
return NULL;
}
/* Extract the property name before the [ */
*array_index_start = '\0';
/* If we have a property name before the [, get that object first */
if (token[0] != '\0')
{
/* Check if we're still working with an object */
if (!(container->header & JB_FOBJECT))
{
elog(DEBUG1, "GJVB: Path segment '%s' cannot be applied to non-object", token);
pfree(path_copy);
return NULL;
}
/* Set up the key to search for */
key.type = jbvString;
key.val.string.val = token;
key.val.string.len = strlen(token);
/* Find the value for this key */
result = findJsonbValueFromContainer(container, JB_FOBJECT, &key);
if (!result)
{
elog(DEBUG1, "GJVB: Key '%s' not found in object", token);
pfree(path_copy);
return NULL;
}
/* We need to go deeper, so the current result must be a container */
if (result->type != jbvBinary)
{
elog(DEBUG1, "GJVB: Path segment '%s' points to a non-container value", token);
pfree(path_copy);
return NULL;
}
/* Move to the next container, which should be an array for the index */
container = (JsonbContainer *)result->val.binary.data;
/* Validate the container */
if (!is_jsonb_container_valid(container))
{
elog(WARNING, "GJVB: Invalid JSONB container during path traversal");
pfree(path_copy);
return NULL;
}
}
/* Now get the array index */
*array_index_end = '\0';
array_index = atoi(array_index_start + 1);
elog(DEBUG1, "GJVB: Array index: %d", array_index);
/* Check if container is an array */
if (!(container->header & JB_FARRAY))
{
elog(DEBUG1, "GJVB: Path segment '%s[%d]' cannot be applied to non-array", token, array_index);
pfree(path_copy);
return NULL;
}
if (array_index < 0)
{
elog(DEBUG1, "GJVB: Invalid negative array index %d", array_index);
pfree(path_copy);
return NULL;
}
/* Get array element by index using iterator */
JsonbIterator *it = JsonbIteratorInit(container);
JsonbValue v;
JsonbIteratorToken itToken;
int current_index = 0;
/* Skip the WJB_BEGIN_ARRAY token */
itToken = JsonbIteratorNext(&it, &v, false);
if (itToken != WJB_BEGIN_ARRAY)
{
elog(DEBUG1, "GJVB: Expected array start token but got %d", itToken);
pfree(path_copy);
return NULL;
}
/* Find the element at the specified index */
result = NULL;
while ((itToken = JsonbIteratorNext(&it, &v, false)) != WJB_DONE)
{
if (itToken == WJB_ELEM)
{
if (current_index == array_index)
{
/* We found our element */
result = palloc(sizeof(JsonbValue));
*result = v;
break;
}
current_index++;
}
}
if (!result)
{
elog(DEBUG1, "GJVB: Array index %d out of bounds (max: %d)", array_index, current_index - 1);
pfree(path_copy);
return NULL;
}
/* If the result is a binary container, update our container for next segment */
if (result->type == jbvBinary)
{
container = (JsonbContainer *)result->val.binary.data;
/* Validate the container */
if (!is_jsonb_container_valid(container))
{
elog(WARNING, "GJVB: Invalid JSONB container during path traversal");
pfree(path_copy);
return NULL;
}
}
}
else
{
/* Standard property access */
/* Check if we're still working with an object */
if (!(container->header & JB_FOBJECT))
{
elog(DEBUG1, "GJVB: Path segment '%s' cannot be applied to non-object", token);
pfree(path_copy);
return NULL;
}
/* Set up the key to search for */
key.type = jbvString;
key.val.string.val = token;
key.val.string.len = strlen(token);
/* Find the value for this key */
result = findJsonbValueFromContainer(container, JB_FOBJECT, &key);
if (!result)
{
elog(DEBUG1, "GJVB: Key '%s' not found in object", token);
pfree(path_copy);
return NULL;
}
/* If this isn't the last token, prepare for the next path segment */
if (saveptr && *saveptr != '\0')
{
/* We need to go deeper, so the current result must be a container */
if (result->type != jbvBinary)
{
elog(DEBUG1, "GJVB: Path segment '%s' points to a non-container value", token);
pfree(path_copy);
return NULL;
}
/* Move to the next container */
container = (JsonbContainer *)result->val.binary.data;
/* Validate the container */
if (!is_jsonb_container_valid(container))
{
elog(WARNING, "GJVB: Invalid JSONB container during path traversal");
pfree(path_copy);
return NULL;
}
}
}
/* Get next token */
token = strtok_r(NULL, ".", &saveptr);
elog(DEBUG1, "GJVB: Next token: %s", token ? token : "NULL");
}
/* If we got here, we found the value */
*found = true;
pfree(path_copy);
return result;
}

View File

@@ -0,0 +1,333 @@
-- Test file for hemar.jsonb_get_by_path function
-- Run with: psql -f test_jsonb_path.sql
-- Load extension if not already loaded
-- CREATE EXTENSION IF NOT EXISTS hemar;
-- Create sample test data
DO $$
DECLARE
test_json jsonb;
result jsonb;
passed boolean;
total_tests integer := 0;
passed_tests integer := 0;
BEGIN
test_json := jsonb_build_object(
'name', 'John Doe',
'age', 30,
'is_active', true,
'tags', jsonb_build_array('developer', 'postgresql', 'jsonb'),
'address', jsonb_build_object(
'street', '123 Main St',
'city', 'New York',
'zip', '10001'
),
'contacts', jsonb_build_array(
jsonb_build_object(
'type', 'email',
'value', 'john@example.com'
),
jsonb_build_object(
'type', 'phone',
'value', '555-1234',
'verified', true
)
),
'skills', jsonb_build_array(
jsonb_build_array('PostgreSQL', 5),
jsonb_build_array('Python', 4),
jsonb_build_array('JavaScript', 3)
)
);
-- Test basic field access
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'name');
passed := result = '"John Doe"'::jsonb;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Simple field access (string): % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Simple field access (string): % | PASSED: % (expected: "John Doe")',
total_tests, result, passed;
END IF;
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'age');
passed := result = '30'::jsonb;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Numeric field access: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Numeric field access: % | PASSED: % (expected: 30)',
total_tests, result, passed;
END IF;
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'is_active');
passed := result = 'true'::jsonb;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Boolean field access: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Boolean field access: % | PASSED: % (expected: true)',
total_tests, result, passed;
END IF;
-- Test nested field access
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'address.city');
passed := result = '"New York"'::jsonb;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Nested object field access: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Nested object field access: % | PASSED: % (expected: "New York")',
total_tests, result, passed;
END IF;
-- Test array access
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'tags[1]');
passed := result = '"postgresql"'::jsonb;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Simple array access: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Simple array access: % | PASSED: % (expected: "postgresql")',
total_tests, result, passed;
END IF;
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'contacts[0].type');
passed := result = '"email"'::jsonb;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Object in array access: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Object in array access: % | PASSED: % (expected: "email")',
total_tests, result, passed;
END IF;
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'skills[1][0]');
passed := result = '"Python"'::jsonb;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Nested array access: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Nested array access: % | PASSED: % (expected: "Python")',
total_tests, result, passed;
END IF;
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'contacts[1].value');
passed := result = '"555-1234"'::jsonb;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Complex path with multiple array indices: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Complex path with multiple array indices: % | PASSED: % (expected: "555-1234")',
total_tests, result, passed;
END IF;
-- Test object and array returns
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'address');
passed := jsonb_typeof(result) = 'object';
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Path to object: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Path to object: % | PASSED: % (expected type: object, got: %)',
total_tests, result, passed, jsonb_typeof(result);
END IF;
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'contacts');
passed := jsonb_typeof(result) = 'array';
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Path to array: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Path to array: % | PASSED: % (expected type: array, got: %)',
total_tests, result, passed, jsonb_typeof(result);
END IF;
-- Test error cases
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'unknown_field');
passed := result IS NULL;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Non-existent field: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Non-existent field: % | PASSED: % (expected: NULL)',
total_tests, result, passed;
END IF;
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'address.country');
passed := result IS NULL;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Non-existent nested field: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Non-existent nested field: % | PASSED: % (expected: NULL)',
total_tests, result, passed;
END IF;
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'tags[10]');
passed := result IS NULL;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Array index out of bounds: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Array index out of bounds: % | PASSED: % (expected: NULL)',
total_tests, result, passed;
END IF;
-- Test edge cases
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, '');
passed := result IS NULL;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Empty path: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Empty path: % | PASSED: % (expected: NULL)',
total_tests, result, passed;
END IF;
total_tests := total_tests + 1;
result := hemar.jsonb_get_by_path(test_json, 'skills[0][1]');
passed := result = '5'::jsonb;
passed_tests := passed_tests + (CASE WHEN passed THEN 1 ELSE 0 END);
IF passed THEN
RAISE NOTICE 'Test %: Multiple array indices: % | PASSED: %',
total_tests, result, passed;
ELSE
RAISE WARNING 'Test %: Multiple array indices: % | PASSED: % (expected: 5)',
total_tests, result, passed;
END IF;
-- Print summary
RAISE NOTICE '------------------------------------';
IF passed_tests = total_tests THEN
RAISE NOTICE 'SUMMARY: % of % tests passed (100%%)',
passed_tests, total_tests;
ELSE
RAISE WARNING 'SUMMARY: % of % tests passed (%)',
passed_tests,
total_tests,
round((passed_tests::numeric / total_tests::numeric) * 100, 2) || '%';
END IF;
RAISE NOTICE '------------------------------------';
IF passed_tests != total_tests THEN
RAISE EXCEPTION 'Tests failed: % of % tests did not pass', (total_tests - passed_tests), total_tests;
END IF;
END $$;