feat!: vpizdu
This commit is contained in:
@@ -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 template_node_to_string(TemplateNode *node, StringInfo result, int indent);
|
||||
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 *
|
||||
jbv_type_to_string(enum jbvType type)
|
||||
@@ -205,8 +230,8 @@ template_default_config(MemoryContext context)
|
||||
{
|
||||
TemplateConfig config;
|
||||
|
||||
config.Syntax.Braces.open = "{%";
|
||||
config.Syntax.Braces.close = "%}";
|
||||
config.Syntax.Braces.open = "{{";
|
||||
config.Syntax.Braces.close = "}}";
|
||||
config.Syntax.Section.control = "for ";
|
||||
config.Syntax.Section.source = "in ";
|
||||
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 */
|
||||
/* Extract value from JSONB context */
|
||||
value = get_jsonb_path_value(jsonb_context, current->value->interpolate.key, &found_interpolate);
|
||||
value = // ??
|
||||
|
||||
if (found_interpolate && value)
|
||||
{
|
||||
@@ -1396,120 +1421,6 @@ template_render(MemoryContext context, TemplateNode *node, Datum jsonb_context,
|
||||
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
|
||||
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 */
|
||||
bool path_found = false;
|
||||
jbv_result = get_jsonb_value_by_path(jb, path, &path_found);
|
||||
jbv_result = // TODO: ?;
|
||||
|
||||
if (path_found && jbv_result)
|
||||
{
|
||||
@@ -2188,233 +2099,3 @@ template_node_to_string(TemplateNode *node, StringInfo result, int indent)
|
||||
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;
|
||||
}
|
||||
333
package/c/hemar/test/test_jsonb_path.sql
Executable file
333
package/c/hemar/test/test_jsonb_path.sql
Executable 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 $$;
|
||||
Reference in New Issue
Block a user