diff --git a/package/c/hemar/hemar--0.1.sql b/package/c/hemar/hemar--0.1.sql index a362488..b8f5818 100755 --- a/package/c/hemar/hemar--0.1.sql +++ b/package/c/hemar/hemar--0.1.sql @@ -2,31 +2,14 @@ \echo Use "CREATE EXTENSION hemar" to load this file. \quit CREATE SCHEMA hemar; - --- Define the template rendering functions using hectic library --- Expected usage: --- ```sql --- SELECT "hemar"."render"( --- "declare" := --- jsonb_build_object( --- 'name', 'test', --- 'config', jsonb_build_object( --- 'debug', true, --- 'limit', 100 --- ) --- ), --- "template" := $hemar$ --- {{ name }} {{ config.limit }} --- $hemar$ --- ); --- ``` -CREATE FUNCTION "hemar"."render"("declare" jsonb, "template" text) -RETURNS text -LANGUAGE C STRICT -AS 'hemar', 'pg_render'; - -- Parse function returns the structure of a template for debugging CREATE FUNCTION "hemar"."parse"("template" text) RETURNS text LANGUAGE C STRICT AS 'hemar', 'pg_template_parse'; + +-- JSONB path access function +CREATE FUNCTION "hemar"."jsonb_get_by_path"("json" jsonb, "path" text) +RETURNS jsonb +LANGUAGE C STRICT +AS 'hemar', 'pg_jsonb_get_by_path'; diff --git a/package/c/hemar/hemar.c b/package/c/hemar/hemar.c index a286d6a..a6a537a 100755 --- a/package/c/hemar/hemar.c +++ b/package/c/hemar/hemar.c @@ -20,14 +20,7 @@ #include "funcapi.h" /* Forward declarations */ -static char *get_jsonb_path_value(Datum jsonb_context, const char *path, bool *found); -static Datum get_jsonb_array(Datum jsonb_context, const char *path, bool *found); -static Datum get_jsonb_array_element(Datum jsonb_array, int index); -static Datum create_iterator_context(Datum parent_context, const char *iterator_name, Datum item_context); -static Datum get_jsonb_include_template(Datum jsonb_context, const char *key, bool *found); -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 const char * jbt_type_to_string(JsonbIteratorToken type) @@ -81,32 +74,6 @@ jbv_type_to_string(enum jbvType type) } } -/* Implementation of a simplified validity check for JsonbContainer */ -static bool -is_jsonb_container_valid(JsonbContainer *container) -{ - PG_TRY(); - { - container->header; - container->children; - } - PG_CATCH(); - { - elog(ERROR, "Invalid JSONB container"); - return false; - } - PG_END_TRY(); - - if (container == NULL) - return false; - - uint32 header = *(uint32 *)container; - if (header == 0) - return false; - - return true; -} - /* Helper function to skip whitespace */ static const char * skip_whitespace(const char *s) @@ -889,992 +856,6 @@ template_free_node(TemplateNode *node) } } -/* Render a template with the given context */ -static char * -template_render(MemoryContext context, TemplateNode *node, Datum jsonb_context, bool *error) -{ - StringInfoData result; - TemplateNode *current; - - if (!node || !context || error == NULL) - { - if (error) - *error = true; - return NULL; - } - - *error = false; - initStringInfo(&result); - current = node; - - elog(DEBUG1, "Starting template rendering"); - - PG_TRY(); - { - while (current) - { - switch (current->type) - { - case TEMPLATE_NODE_TEXT: - /* Process text node */ - elog(DEBUG1, ""); - elog(DEBUG1, "> TEXT"); - if (current->value->text.content) - { - elog(DEBUG1, "N*TEXT: Rendering text node: %s", current->value->text.content); - appendStringInfoString(&result, current->value->text.content); - } - break; - - case TEMPLATE_NODE_INTERPOLATE: - /* Process interpolation node */ - elog(DEBUG1, ""); - elog(DEBUG1, "> INTERPOLATE"); - - char *value = NULL; - bool found_interpolate = false; - - if (current->value->interpolate.key) - { - elog(DEBUG1, "N*INTR: Processing interpolation for key: %s", current->value->interpolate.key); - - /* First try to get as a direct path */ - /* Extract value from JSONB context */ - value = // ?? - - if (found_interpolate && value) - { - elog(DEBUG1, "N*INTR: Found value for key %s: %s", current->value->interpolate.key, value); - appendStringInfoString(&result, value); - pfree(value); - } - else - { - /* If not found as direct path, check if it's an array */ - Datum array_value; - bool array_found = false; - - array_value = get_jsonb_array(jsonb_context, current->value->interpolate.key, &array_found); - - if (array_found) - { - /* Convert array to string representation */ - elog(DEBUG1, "N*INTR: Found array for key %s, converting to string", current->value->interpolate.key); - - /* Create a string representation of the array */ - StringInfoData array_str; - initStringInfo(&array_str); - appendStringInfoString(&array_str, "["); - - Jsonb *array_jb = (Jsonb *) DatumGetPointer(array_value); - if (array_jb && is_jsonb_container_valid(&array_jb->root)) - { - /* Check if it's actually an array */ - JsonbContainer *jc = &array_jb->root; - if (!(jc->header & JB_FARRAY)) - { - elog(DEBUG1, "N*INTR: JSONB value is not an array"); - appendStringInfoString(&array_str, "[Not an array]"); - } - else - { - /* Iterate through array elements */ - JsonbIterator *it = JsonbIteratorInit(jc); - JsonbValue v; - JsonbIteratorToken token; - bool first_element = true; - - /* Skip the WJB_BEGIN_ARRAY token */ - token = JsonbIteratorNext(&it, &v, false); - - /* Process each array element */ - while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) - { - if (token != WJB_ELEM) - continue; - - if (!first_element) - appendStringInfoString(&array_str, ", "); - else - first_element = false; - - if (v.type == jbvString) - { - appendStringInfoChar(&array_str, '"'); - appendBinaryStringInfo(&array_str, v.val.string.val, v.val.string.len); - appendStringInfoChar(&array_str, '"'); - } - else if (v.type == jbvNumeric) - { - char *num_str = DatumGetCString(DirectFunctionCall1(numeric_out, NumericGetDatum(v.val.numeric))); - appendStringInfoString(&array_str, num_str); - pfree(num_str); - } - else if (v.type == jbvBool) - { - appendStringInfoString(&array_str, v.val.boolean ? "true" : "false"); - } - else if (v.type == jbvNull) - { - appendStringInfoString(&array_str, "null"); - } - else if (v.type == jbvBinary) - { - /* For complex values, convert to string */ - Datum elem = PointerGetDatum(JsonbValueToJsonb(&v)); - bool elem_found = false; - char *elem_str = get_jsonb_path_value(elem, "value", &elem_found); - - if (elem_found && elem_str) - { - appendStringInfoString(&array_str, elem_str); - pfree(elem_str); - } - else - { - appendStringInfoString(&array_str, "[Complex Value]"); - } - } - } - } - } - - appendStringInfoString(&array_str, "]"); - appendStringInfoString(&result, array_str.data); - pfree(array_str.data); - } - else - { - elog(DEBUG1, "N*INTR: Key %s not found in context", current->value->interpolate.key); - /* Optionally append something to indicate missing key */ - appendStringInfoString(&result, ""); - } - } - } - case TEMPLATE_NODE_SECTION: - elog(DEBUG1, ""); - elog(DEBUG1, "> SECTION"); - /* Handle sections (loops) */ - char *collection_path = current->value->section.collection; - Datum array_value; - bool found_section = false; - int array_length; - int i; - JsonbParseState *parse_state = NULL; - JsonbValue *empty_obj; - Datum item_context; - Datum merged_context; - char *item_result; - bool item_error = false; - - if (collection_path) - { - elog(DEBUG1, "N*SECT: Processing section with collection path: %s", collection_path); - - /* Use the improved get_jsonb_array function that handles nested paths */ - array_value = get_jsonb_array(jsonb_context, collection_path, &found_section); - - if (found_section) - { - elog(DEBUG1, "N*SECT: Found array for section: %s", collection_path); - - /* Make sure we have a valid array */ - Jsonb *array_jb = (Jsonb *) DatumGetPointer(array_value); - if (!array_jb || !is_jsonb_container_valid(&array_jb->root)) - { - elog(WARNING, "N*SECT: Invalid JSONB array container for path: %s", collection_path); - break; - } - - /* Check if it's actually an array */ - JsonbContainer *jc = &array_jb->root; - if (!(jc->header & JB_FARRAY)) - { - elog(DEBUG1, "N*SECT: JSONB value is not an array"); - break; - } - - /* If section body is empty, nothing to do */ - if (current->value->section.body == NULL) - { - elog(DEBUG1, "N*SECT: Section body is empty, skipping"); - break; - } - - elog(DEBUG1, "N*SECT: Rendering section body for each array element"); - - /* Log the section body structure for debugging */ - if (current->value->section.body) - { - StringInfoData section_info; - initStringInfo(§ion_info); - template_node_to_string(current->value->section.body, §ion_info, 0); - elog(DEBUG1, "N*SECT: Section body structure: %s", section_info.data); - pfree(section_info.data); - } - - /* Iterate through array elements */ - JsonbIterator *it = JsonbIteratorInit(jc); - JsonbValue v; - JsonbIteratorToken token; - int i = 0; - int nesting_level = 0; - bool in_element = false; - JsonbParseState *element_state = NULL; - JsonbValue *element_value = NULL; - - /* Skip the WJB_BEGIN_ARRAY token */ - token = JsonbIteratorNext(&it, &v, false); - elog(DEBUG1, "N*SECT: Iterator started, first token: %d", token); - - /* Process each array element */ - while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) - { - elog(DEBUG1, "N*SECT: Token: %d, Type: %s, Nesting: %d", - token, jbv_type_to_string(v.type), nesting_level); - - /* Handle array elements */ - if (token == WJB_ELEM) - { - item_context = (Datum) 0; - item_error = false; - - elog(DEBUG1, "N*SECT: Processing array element %d", i); - - /* Convert the JsonbValue to a Datum */ - PG_TRY(); - { - if (v.type == jbvBinary) - { - /* For binary values, just convert directly */ - item_context = PointerGetDatum(JsonbValueToJsonb(&v)); - } - else if (v.type == jbvNull) - { - /* Handle null values by creating an empty object */ - parse_state = NULL; - pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL); - empty_obj = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); - item_context = PointerGetDatum(JsonbValueToJsonb(empty_obj)); - } - else - { - /* For scalar values, create a proper JSON object */ - parse_state = NULL; - pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL); - - /* Add a dummy key "value" */ - JsonbValue key; - key.type = jbvString; - key.val.string.val = "value"; - key.val.string.len = 5; - - pushJsonbValue(&parse_state, WJB_KEY, &key); - - /* Add the value */ - pushJsonbValue(&parse_state, WJB_VALUE, &v); - - /* Finish the object */ - empty_obj = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); - - /* Convert to Jsonb */ - item_context = PointerGetDatum(JsonbValueToJsonb(empty_obj)); - } - - /* Process this element */ - process_array_element: - - /* Validate we got a valid item back */ - if (item_context != (Datum) 0) - { - Jsonb *item_jb = (Jsonb *) DatumGetPointer(item_context); - if (item_jb && is_jsonb_container_valid(&item_jb->root)) - { - elog(DEBUG1, "N*SECT: Got valid array element %d", i); - elog(DEBUG1, "N*SECT: Array Element: %s", JsonbToCString(NULL, &item_jb->root, VARSIZE_ANY_EXHDR(item_jb))); - - /* Create context with iterator variable */ - PG_TRY(); - { - merged_context = create_iterator_context(jsonb_context, current->value->section.iterator, item_context); - } - PG_CATCH(); - { - elog(WARNING, "N*SECT: Error creating merged context for array element %d", i); - /* Use parent context as fallback */ - merged_context = jsonb_context; - - /* Reset error state */ - FlushErrorState(); - } - PG_END_TRY(); - - if (merged_context == (Datum) 0) - { - elog(WARNING, "N*SECT: Failed to create merged context for array element %d", i); - i++; - continue; - } - - /* Render section body with new context */ - PG_TRY(); - { - item_result = template_render(context, current->value->section.body, merged_context, &item_error); - - if (!item_error && item_result) - { - appendStringInfoString(&result, item_result); - pfree(item_result); - } - else if (item_error) - { - elog(WARNING, "N*SECT: Error rendering template section for array element %d", i); - *error = true; - return result.data; - } - } - PG_CATCH(); - { - elog(WARNING, "N*SECT: Exception during template rendering for array element %d", i); - /* Continue with next element */ - FlushErrorState(); - } - PG_END_TRY(); - } - else - { - elog(WARNING, "N*SECT: Got invalid JSONB container for array element %d", i); - } - } - else - { - elog(DEBUG1, "N*SECT: Array element %d is null, creating empty object", i); - /* Create an empty object for null array elements */ - parse_state = NULL; - - pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL); - empty_obj = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); - item_context = PointerGetDatum(JsonbValueToJsonb(empty_obj)); - goto process_array_element; - } - } - PG_CATCH(); - { - elog(WARNING, "N*SECT: Error processing array element %d, creating empty object instead", i); - /* Create an empty object for problematic array elements */ - parse_state = NULL; - - pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL); - empty_obj = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); - item_context = PointerGetDatum(JsonbValueToJsonb(empty_obj)); - - /* Reset error state */ - FlushErrorState(); - goto process_array_element; - } - PG_END_TRY(); - - i++; - } - /* Handle complex objects within the array */ - else if (token == WJB_BEGIN_OBJECT || token == WJB_BEGIN_ARRAY) - { - if (nesting_level == 0) - { - /* Starting a new complex element */ - elog(DEBUG1, "N*SECT: Starting complex element %d", i); - element_state = NULL; - element_value = pushJsonbValue(&element_state, token, NULL); - in_element = true; - } - nesting_level++; - } - else if ((token == WJB_END_OBJECT || token == WJB_END_ARRAY) && in_element) - { - nesting_level--; - - if (nesting_level == 0) - { - /* Finished a complex element */ - elog(DEBUG1, "N*SECT: Finished complex element %d", i); - element_value = pushJsonbValue(&element_state, token, NULL); - in_element = false; - - /* Convert to Datum and process */ - item_context = PointerGetDatum(JsonbValueToJsonb(element_value)); - item_error = false; - - /* Process this complex element */ - goto process_array_element; - } - } - else if (in_element) - { - /* Add to the current element being built */ - pushJsonbValue(&element_state, token, &v); - } - } - } - else - { - elog(DEBUG1, "N*SECT: Collection not found: %s", collection_path); - } - } - case TEMPLATE_NODE_EXECUTE: - elog(DEBUG1, ""); - elog(DEBUG1, "> EXECUTE"); - /* Execute is not implemented in this version */ - elog(DEBUG1, "N*EXEC: Execute node type not implemented"); - case TEMPLATE_NODE_INCLUDE: - elog(DEBUG1, ""); - elog(DEBUG1, "> INCLUDE"); - /* Handle includes */ - char *template_key = current->value->include.key; - Datum include_data; - bool found_include = false; - - if (template_key) - { - elog(DEBUG1, "N*INCL: Processing include with key: %s", template_key); - - /* Find include template in context */ - include_data = get_jsonb_include_template(jsonb_context, template_key, &found_include); - - if (found_include) - { - char *include_template = NULL; - Datum include_context = (Datum) 0; - char *include_result; - bool include_error = false; - - /* Extract template and context */ - get_include_data(include_data, &include_template, &include_context); - - /* Parse and render included template */ - if (include_template) - { - TemplateConfig config = template_default_config(context); - TemplateErrorCode error_code; - const char *template_str = include_template; - TemplateNode *include_node = template_parse(context, &template_str, &config, false, &error_code); - - if (include_node && error_code == TEMPLATE_ERROR_NONE) - { - include_result = template_render(context, include_node, - include_context != (Datum) 0 ? include_context : jsonb_context, - &include_error); - - if (!include_error && include_result) - { - appendStringInfoString(&result, include_result); - pfree(include_result); - } - else - { - elog(WARNING, "N*INCL: Error rendering included template: %s", template_key); - *error = true; - } - - template_free_node(include_node); - } - else - { - elog(WARNING, "N*INCL: Error parsing included template: %s", template_key); - *error = true; - } - - pfree(include_template); - } - else - { - elog(WARNING, "N*INCL: Included template is null: %s", template_key); - } - } - else - { - elog(DEBUG1, "N*INCL: Include key not found: %s", template_key); - } - } - break; - default: - /* Unknown node type */ - elog(WARNING, "N*UNKN: Unknown node type: %d", current->type); - break; - } - - if (*error) - break; - - current = current->next; - } - } - PG_CATCH(); - { - elog(WARNING, "Unhandled exception during template rendering"); - FlushErrorState(); - *error = true; - return result.data; - } - PG_END_TRY(); - - elog(DEBUG1, "Template rendering completed successfully"); - return result.data; -} - -static Datum -get_jsonb_array(Datum jsonb_context, const char *path, bool *found) -{ - Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_context); - JsonbValue *jbv_result; - JsonbIterator *it; - JsonbValue v; - JsonbIteratorToken token; - Datum result = (Datum) 0; - Jsonb *result_jb; - - *found = false; - - if (!jb || !path) - { - elog(DEBUG1, "Null JSONB or path in get_jsonb_array"); - return result; - } - - /* Validate the container */ - if (!is_jsonb_container_valid(&jb->root)) - { - elog(WARNING, "Invalid JSONB container in get_jsonb_array"); - return result; - } - - elog(DEBUG1, "Looking for array at path: %s", path); - - /* Use PG_TRY/PG_CATCH to handle any errors during iteration */ - PG_TRY(); - { - /* Use the path traversal function */ - bool path_found = false; - jbv_result = // TODO: ?; - - if (path_found && jbv_result) - { - elog(DEBUG1, "Found value for key %s (%s)", path, jbv_type_to_string(jbv_result->type)); - - if (jbv_result->type == jbvBinary) - { - /* Get more details about the binary data */ - elog(DEBUG1, "Binary value found, trying to examine structure"); - - /* Validate the binary container before iterating */ - if (!is_jsonb_container_valid(jbv_result->val.binary.data)) - { - elog(WARNING, "Invalid binary JSONB container for key: %s", path); - return result; - } - - elog(DEBUG1, "Trying to initialize the iterator..."); - /* Try to initialize the iterator */ - PG_TRY(); - { - /* Log raw pointer for debugging */ - elog(DEBUG1, "Binary container address: %p", jbv_result->val.binary.data); - - /* Try to get the first 4 bytes of the binary data */ - uint32 header = *(uint32 *)jbv_result->val.binary.data; - elog(DEBUG1, "Binary container header: %u", header); - - /* Initialize the iterator with careful error handling */ - elog(DEBUG1, "Initializing iterator for binary container"); - it = JsonbIteratorInit(jbv_result->val.binary.data); - elog(DEBUG1, "Iterator initialized successfully"); - - elog(DEBUG1, "Getting first token"); - token = JsonbIteratorNext(&it, &v, false); - elog(DEBUG1, "First token retrieved: %d", token); - - if (token == WJB_BEGIN_ARRAY) - { - /* It's a valid array */ - *found = true; - result_jb = JsonbValueToJsonb(jbv_result); - result = PointerGetDatum(result_jb); - elog(DEBUG1, "Found array at path %s (result=%p)", path, DatumGetPointer(result)); - elog(DEBUG1, "Array: %s", JsonbToCString(NULL, &result_jb->root, VARSIZE_ANY_EXHDR(result_jb))); - return result; - } - else - { - elog(DEBUG1, "Path %s exists but is not an array (token type: %d)", path, token); - } - } - PG_CATCH(); - { - elog(WARNING, "Error initializing JSON iterator for path %s", path); - /* Get more details about the error */ - ErrorData *edata = CopyErrorData(); - elog(WARNING, "Error message: %s", edata->message); - elog(WARNING, "Error detail: %s", edata->detail ? edata->detail : "none"); - elog(WARNING, "Error hint: %s", edata->hint ? edata->hint : "none"); - elog(WARNING, "Error context: %s", edata->context ? edata->context : "none"); - FreeErrorData(edata); - FlushErrorState(); - } - PG_END_TRY(); - } - else - { - elog(DEBUG1, "Path %s exists but is not binary JSONB (type: %d)", path, jbv_result->type); - } - } - else - { - elog(DEBUG1, "Path %s not found in JSONB", path); - } - } - PG_CATCH(); - { - elog(WARNING, "Exception while processing array at path %s", path); - ErrorData *edata = CopyErrorData(); - elog(WARNING, "Error message: %s", edata->message); - FreeErrorData(edata); - FlushErrorState(); - } - PG_END_TRY(); - - return result; -} - -static Datum -get_jsonb_array_element(Datum jsonb_array, int index) -{ - Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_array); - JsonbIterator *it; - JsonbValue v; - JsonbIteratorToken token; - int current_index = 0; - Datum result = (Datum) 0; - JsonbParseState *parse_state = NULL; - JsonbValue *jbv_result; - JsonbValue key; - Jsonb *result_jb; - - if (!jb || index < 0) - { - elog(DEBUG1, "Invalid array or index: array=%p, index=%d", jb, index); - return result; - } - - /* Check if this is a valid Jsonb before proceeding */ - if (!is_jsonb_container_valid(&jb->root)) - { - elog(WARNING, "Invalid JSONB container in get_jsonb_array_element"); - return result; - } - - /* Use PG_TRY/PG_CATCH to handle any errors during iteration */ - PG_TRY(); - { - it = JsonbIteratorInit((JsonbContainer *)&jb->root); - - /* Skip the WJB_BEGIN_ARRAY token */ - token = JsonbIteratorNext(&it, &v, false); - - if (token != WJB_BEGIN_ARRAY) - { - elog(DEBUG1, "JSON value is not an array (token=%d)", token); - return result; - } - - /* Find the element at the specified index */ - while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) - { - if (token == WJB_ELEM) - { - if (current_index == index) - { - /* Found the element */ - elog(DEBUG1, "Found array element at index %d (type=%d)", index, v.type); - - if (v.type == jbvBinary) - { - /* For binary values, just convert directly */ - result_jb = JsonbValueToJsonb(&v); - result = PointerGetDatum(result_jb); - elog(DEBUG1, "Array Element (Binary): %s", JsonbToCString(NULL, &result_jb->root, VARSIZE_ANY_EXHDR(result_jb))); - } - else if (v.type == jbvNull) - { - /* Handle null values by creating an empty object */ - pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL); - jbv_result = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); - result = PointerGetDatum(JsonbValueToJsonb(jbv_result)); - elog(DEBUG1, "Converted null array element to empty object"); - } - else - { - /* For scalar values, we need to create a proper JSON value */ - pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL); - - /* Add a dummy key "value" */ - key.type = jbvString; - key.val.string.val = "value"; - key.val.string.len = 5; - - pushJsonbValue(&parse_state, WJB_KEY, &key); - - /* Add the value */ - pushJsonbValue(&parse_state, WJB_VALUE, &v); - - /* Finish the object */ - jbv_result = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); - - /* Convert to Jsonb */ - result = PointerGetDatum(JsonbValueToJsonb(jbv_result)); - elog(DEBUG1, "Wrapped scalar array element in object with 'value' key"); - } - break; - } - current_index++; - } - } - } - PG_CATCH(); - { - elog(WARNING, "Exception while processing array element at index %d", index); - FlushErrorState(); - return (Datum) 0; - } - PG_END_TRY(); - - if (result == (Datum) 0) - { - elog(DEBUG1, "Array element at index %d not found (array length = %d)", index, current_index); - } - - return result; -} - -static Datum -create_iterator_context(Datum parent_context, const char *iterator_name, Datum item_context) -{ - Jsonb *parent_jb = (Jsonb *) DatumGetPointer(parent_context); - Jsonb *item_jb = (Jsonb *) DatumGetPointer(item_context); - JsonbParseState *parse_state = NULL; - JsonbValue *result; - JsonbIterator *it; - JsonbValue v; - JsonbIteratorToken token; - JsonbValue key; - JsonbValue val; - bool is_scalar = false; - - if (!parent_jb || !item_jb || !iterator_name) - { - elog(DEBUG1, "CIC: Invalid parameters for create_iterator_context: parent=%p, item=%p, iterator=%s", - parent_jb, item_jb, iterator_name ? iterator_name : "(null)"); - return parent_context; - } - - /* Validate the containers */ - if (!is_jsonb_container_valid(&parent_jb->root)) - { - elog(WARNING, "CIC: Invalid parent JSONB container in create_iterator_context"); - return parent_context; - } - - if (!is_jsonb_container_valid(&item_jb->root)) - { - elog(WARNING, "CIC: Invalid item JSONB container in create_iterator_context"); - return parent_context; - } - - elog(DEBUG1, "CIC: Creating iterator context with iterator name: %s", iterator_name); - - /* Start with a copy of the parent context */ - pushJsonbValue(&parse_state, WJB_BEGIN_OBJECT, NULL); - - /* Copy all fields from parent context */ - it = JsonbIteratorInit((JsonbContainer *)&parent_jb->root); - - /* Skip the WJB_BEGIN_OBJECT token */ - token = JsonbIteratorNext(&it, &v, false); - - if (token != WJB_BEGIN_OBJECT) - { - elog(WARNING, "CIC: Parent context is not an object (token=%d)", token); - return parent_context; - } - - /* Copy all key-value pairs from parent */ - while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) - { - if (token == WJB_KEY) - { - /* Copy the key */ - key.type = jbvString; - key.val.string.val = pnstrdup(v.val.string.val, v.val.string.len); - key.val.string.len = v.val.string.len; - - pushJsonbValue(&parse_state, WJB_KEY, &key); - - /* Get and copy the value */ - token = JsonbIteratorNext(&it, &val, false); - pushJsonbValue(&parse_state, WJB_VALUE, &val); - - pfree(key.val.string.val); - } - } - - /* Add the iterator variable */ - key.type = jbvString; - key.val.string.val = (char *) iterator_name; - key.val.string.len = strlen(iterator_name); - - pushJsonbValue(&parse_state, WJB_KEY, &key); - - /* Check if the item is a scalar value wrapped in an object */ - it = JsonbIteratorInit((JsonbContainer *)&item_jb->root); - token = JsonbIteratorNext(&it, &v, false); - - if (token == WJB_BEGIN_OBJECT) - { - elog(DEBUG1, "CIC: Item context is an object"); - /* Check if it's our special scalar wrapper with "value" key */ - token = JsonbIteratorNext(&it, &v, false); - if (token == WJB_KEY && v.type == jbvString && - v.val.string.len == 5 && strncmp(v.val.string.val, "value", 5) == 0) - { - /* Get the scalar value */ - token = JsonbIteratorNext(&it, &v, false); - if (token == WJB_VALUE) - { - is_scalar = true; - elog(DEBUG1, "CIC: Found scalar value with 'value' key (type=%s)", jbv_type_to_string(v.type)); - pushJsonbValue(&parse_state, WJB_VALUE, &v); - } - } - } - else - { - elog(DEBUG1, "CIC: Item context is not an object (token=%d)", token); - } - - /* If not a scalar, use the whole item as is */ - if (!is_scalar) - { - elog(DEBUG1, "CIC: Using entire item as context value"); - it = JsonbIteratorInit((JsonbContainer *)&item_jb->root); - token = JsonbIteratorNext(&it, &v, false); - pushJsonbValue(&parse_state, WJB_VALUE, &v); - } - - /* Finalize the new context object */ - result = pushJsonbValue(&parse_state, WJB_END_OBJECT, NULL); - - /* Validate the result before returning */ - Jsonb *result_jb = JsonbValueToJsonb(result); - if (!result_jb || !is_jsonb_container_valid(&result_jb->root)) - { - elog(WARNING, "CIC: Created invalid JSONB container in create_iterator_context"); - return parent_context; - } - - return PointerGetDatum(result_jb); -} - -static Datum -get_jsonb_include_template(Datum jsonb_context, const char *key, bool *found) -{ - Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_context); - JsonbValue *jbv_result; - JsonbValue search_key; - Datum result = (Datum) 0; - char *include_key; - - *found = false; - - if (!jb || !key) - return result; - - /* Construct the include key */ - include_key = psprintf("include %s", key); - - /* Look for the include template in the context */ - search_key.type = jbvString; - search_key.val.string.val = include_key; - search_key.val.string.len = strlen(include_key); - - jbv_result = findJsonbValueFromContainer(&jb->root, JB_FOBJECT, &search_key); - - if (jbv_result && jbv_result->type == jbvBinary) - { - *found = true; - result = PointerGetDatum(JsonbValueToJsonb(jbv_result)); - } - - pfree(include_key); - return result; -} - -static void -get_include_data(Datum include_data, char **template_out, Datum *context_out) -{ - Jsonb *jb = (Jsonb *) DatumGetPointer(include_data); - JsonbIterator *it; - JsonbValue v; - JsonbIteratorToken token; - JsonbValue template_key, context_key; - JsonbValue *template_jbv, *context_jbv; - Jsonb *include_obj; - - *template_out = NULL; - *context_out = (Datum) 0; - - if (!jb) - return; - - /* Check if it's an array of include objects */ - it = JsonbIteratorInit((JsonbContainer *)&jb->root); - token = JsonbIteratorNext(&it, &v, false); - - if (token != WJB_BEGIN_ARRAY) - return; - - /* Get the first element (we only support one include for now) */ - token = JsonbIteratorNext(&it, &v, false); - - if (token != WJB_ELEM || v.type != jbvBinary) - return; - - /* Get the include object */ - include_obj = (Jsonb *) DatumGetPointer(JsonbValueToJsonb(&v)); - - /* Look for template and context keys */ - template_key.type = jbvString; - template_key.val.string.val = "template"; - template_key.val.string.len = strlen("template"); - - context_key.type = jbvString; - context_key.val.string.val = "context"; - context_key.val.string.len = strlen("context"); - - template_jbv = findJsonbValueFromContainer(&include_obj->root, JB_FOBJECT, &template_key); - - /* Extract template string */ - if (template_jbv && template_jbv->type == jbvString) - { - *template_out = pnstrdup(template_jbv->val.string.val, template_jbv->val.string.len); - } - - context_jbv = findJsonbValueFromContainer(&include_obj->root, JB_FOBJECT, &context_key); - - /* Extract context object */ - if (context_jbv && context_jbv->type == jbvBinary) - { - *context_out = PointerGetDatum(JsonbValueToJsonb(context_jbv)); - } -} - /* PostgreSQL function wrappers */ PG_MODULE_MAGIC; @@ -1952,98 +933,6 @@ pg_template_parse(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(result_text); } -PG_FUNCTION_INFO_V1(pg_render); -Datum -pg_render(PG_FUNCTION_ARGS) -{ - text *template_text = PG_GETARG_TEXT_PP(1); - Jsonb *context_jsonb = PG_GETARG_JSONB_P(0); - char *template_str = text_to_cstring(template_text); - const char *template_ptr = template_str; - MemoryContext old_context; - MemoryContext render_context; - TemplateConfig config; - TemplateNode *root = NULL; - TemplateErrorCode error_code = TEMPLATE_ERROR_NONE; - bool render_error = false; - char *rendered_result = NULL; - text *result_text = NULL; - - /* Log the template and context for debugging */ - elog(DEBUG1, " ==== Template ===="); - elog(DEBUG1, "%s", template_str); - elog(DEBUG1, " ==== End Template ===="); - elog(DEBUG1, " ==== Context ===="); - elog(DEBUG1, "%s", JsonbToCString(NULL, &context_jsonb->root, VARSIZE_ANY_EXHDR(context_jsonb))); - elog(DEBUG1, " ==== End Context ===="); - - /* Create a memory context for rendering */ - render_context = AllocSetContextCreate(CurrentMemoryContext, - "Template Render Context", - ALLOCSET_DEFAULT_SIZES); - - /* Switch to the new context for parsing and 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)))); - } - - elog(DEBUG1, "Template parsed successfully, starting render"); - elog(DEBUG1, "--------------------------------"); - - /* Render the template with the provided context */ - rendered_result = template_render(render_context, root, PointerGetDatum(context_jsonb), &render_error); - - /* Check for rendering errors */ - if (render_error || !rendered_result) - { - ereport(ERROR, - (errcode(ERRCODE_INTERNAL_ERROR), - errmsg("Template rendering error"))); - } - - elog(DEBUG1, "Template rendered successfully: %s", rendered_result); - - /* Convert the result to a text datum in the original memory context */ - MemoryContextSwitchTo(old_context); - result_text = cstring_to_text(rendered_result); - } - 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 convert template node to string for debugging */ static void template_node_to_string(TemplateNode *node, StringInfo result, int indent) @@ -2098,4 +987,539 @@ template_node_to_string(TemplateNode *node, StringInfo result, int indent) current = current->next; } +} + +/* Function declarations */ +PG_FUNCTION_INFO_V1(pg_jsonb_get_by_path); + +/* + * Parse a path string into segments for JSONB traversal + * Path format: field.nested_field[0].array_field[1][2] + * Returns an array of segments and the count of segments + */ +static char ** +parse_path_string(const char *path_str, int *num_segments) +{ + char **segments; + int max_segments = 32; /* Initial capacity */ + int segment_count = 0; + StringInfoData current_segment; + const char *p = path_str; + bool in_brackets = false; + + /* Allocate memory for segments */ + segments = (char **)palloc(sizeof(char *) * max_segments); + if (segments == NULL) + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"))); + + initStringInfo(¤t_segment); + + /* Parse the path string */ + while (*p) + { + if (*p == '.' && !in_brackets) + { + /* End of segment */ + if (current_segment.len > 0) + { + segments[segment_count++] = pstrdup(current_segment.data); + resetStringInfo(¤t_segment); + + /* Resize if needed */ + if (segment_count >= max_segments) + { + max_segments *= 2; + segments = (char **)repalloc(segments, sizeof(char *) * max_segments); + if (segments == NULL) + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"))); + } + } + } + else if (*p == '[') + { + /* Start of array index */ + if (in_brackets) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid path syntax: nested brackets"))); + + /* Save the current field name if any */ + if (current_segment.len > 0) + { + segments[segment_count++] = pstrdup(current_segment.data); + resetStringInfo(¤t_segment); + + /* Resize if needed */ + if (segment_count >= max_segments) + { + max_segments *= 2; + segments = (char **)repalloc(segments, sizeof(char *) * max_segments); + if (segments == NULL) + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"))); + } + } + + /* Add a special prefix to indicate this is an array index */ + appendStringInfoChar(¤t_segment, '['); + in_brackets = true; + } + else if (*p == ']') + { + /* End of array index */ + if (!in_brackets) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid path syntax: unmatched closing bracket"))); + + /* Save the array index */ + if (current_segment.len > 0) + { + /* We don't need to append the closing bracket, just save the segment */ + segments[segment_count++] = pstrdup(current_segment.data); + resetStringInfo(¤t_segment); + + /* Resize if needed */ + if (segment_count >= max_segments) + { + max_segments *= 2; + segments = (char **)repalloc(segments, sizeof(char *) * max_segments); + if (segments == NULL) + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"))); + } + } + else + { + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid path syntax: empty array index"))); + } + + in_brackets = false; + } + else + { + /* Regular character */ + appendStringInfoChar(¤t_segment, *p); + } + + p++; + } + + /* Handle the last segment if any */ + if (current_segment.len > 0) + { + if (in_brackets) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid path syntax: unclosed bracket"))); + + segments[segment_count++] = pstrdup(current_segment.data); + } + + pfree(current_segment.data); + *num_segments = segment_count; + + return segments; +} + +/* + * Get JSONB value by path + * Input: JSONB document and path string + * Output: JSONB value at the specified path or NULL if path is invalid + */ +Datum +pg_jsonb_get_by_path(PG_FUNCTION_ARGS) +{ + Jsonb *jb = PG_GETARG_JSONB_P(0); + text *path_text = PG_GETARG_TEXT_PP(1); + char *path_str = text_to_cstring(path_text); + + JsonbValue *result = NULL; + JsonbValue tmp_val; + JsonbIterator *it; + JsonbIteratorToken token; + JsonbValue v; + + char **segments; + int num_segments; + int current_segment = 0; + int array_index; + int current_index; + bool found; + int i; + + /* Empty path returns NULL */ + if (path_str == NULL || *path_str == '\0') + { + elog(WARNING, "path is empty, returning NULL"); + pfree(path_str); + PG_RETURN_NULL(); + } + + /* Parse the path string */ + segments = parse_path_string(path_str, &num_segments); + + if (num_segments == 0) + { + /* Free allocated memory before returning */ + elog(WARNING, "no segments in path, returning NULL"); + pfree(path_str); + pfree(segments); + PG_RETURN_NULL(); + } + + /* Start iterating through the JSONB document */ + it = JsonbIteratorInit(&jb->root); + token = JsonbIteratorNext(&it, &v, true); + + if (token == WJB_BEGIN_ARRAY) + { + /* If the root is an array, the first segment must be an array index */ + if (current_segment < num_segments && segments[current_segment][0] != '[') + { + /* First segment is not an array index, but root is an array */ + /* Free allocated memory before returning */ + elog(WARNING, "root is an array, but first segment '%s' is not an array index", segments[current_segment]); + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + PG_RETURN_NULL(); + } + } + else if (token == WJB_BEGIN_OBJECT) + { + /* If the root is an object, the first segment must be a field name */ + if (current_segment < num_segments && segments[current_segment][0] == '[') + { + /* First segment is an array index, but root is an object */ + /* Free allocated memory before returning */ + elog(WARNING, "root is an object, but first segment '%s' is an array index", segments[current_segment]); + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + PG_RETURN_NULL(); + } + } + else + { + /* Root is a scalar value, can't navigate further */ + /* Free allocated memory before returning */ + elog(WARNING, "root is a scalar value, can't navigate further"); + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + PG_RETURN_NULL(); + } + + /* Process each path segment */ + while (current_segment < num_segments) + { + char *segment = segments[current_segment]; + + if (segment[0] == '[') + { + /* Array index segment */ + if (token != WJB_BEGIN_ARRAY) + { + /* Not an array, can't use array index */ + /* Free allocated memory before returning */ + elog(WARNING, "segment '%s' is not an array, can't use array index", segment); + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + PG_RETURN_NULL(); + } + + /* Extract the array index */ + array_index = atoi(segment + 1); + if (array_index < 0) + { + /* Free allocated memory before returning */ + elog(WARNING, "invalid array index: %d", array_index); + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + PG_RETURN_NULL(); + } + + /* Navigate to the specified array element */ + current_index = 0; + found = false; + + /* Move to the first element */ + token = JsonbIteratorNext(&it, &v, true); + + /* Iterate through array elements */ + while (token != WJB_END_ARRAY) + { + if (current_index == array_index) + { + found = true; + + /* If this is the last segment, return the value */ + if (current_segment == num_segments - 1) + { + /* Create a standalone JsonbValue for the result */ + result = palloc(sizeof(JsonbValue)); + *result = v; + + /* Convert to a Jsonb container */ + if (result->type == jbvBinary) + { + tmp_val.type = jbvBinary; + tmp_val.val.binary.data = result->val.binary.data; + tmp_val.val.binary.len = result->val.binary.len; + } + else + { + tmp_val = *result; + } + + /* Free allocated memory before returning */ + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + + /* Return the result as a new Jsonb */ + PG_RETURN_JSONB_P(JsonbValueToJsonb(&tmp_val)); + } + + /* Not the last segment, continue traversing */ + if (v.type == jbvBinary) + { + /* Nested container, need to iterate into it */ + JsonbIterator *nested_it = JsonbIteratorInit((JsonbContainer *) v.val.binary.data); + it = nested_it; + + /* Get the container type (array or object) */ + token = JsonbIteratorNext(&it, &v, true); + + /* Move to the next segment */ + current_segment++; + /* Continue processing from the next segment */ + break; + } + else + { + /* Scalar value, can't navigate further */ + /* Free allocated memory before returning */ + elog(WARNING, "scalar value at segment '%s', can't navigate further", segment); + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + + PG_RETURN_NULL(); + } + } + + /* Skip this element */ + if (v.type == jbvBinary) + { + /* Skip over the entire container */ + JsonbIterator *nested_it = JsonbIteratorInit((JsonbContainer *) v.val.binary.data); + JsonbIteratorToken nested_token; + JsonbValue nested_v; + + /* Skip until we reach the end of the container */ + do + { + nested_token = JsonbIteratorNext(&nested_it, &nested_v, false); + } while (nested_token != WJB_DONE); + } + + current_index++; + token = JsonbIteratorNext(&it, &v, true); + } + + if (!found) + { + /* Array index out of bounds */ + /* Free allocated memory before returning */ + elog(WARNING, "array index %d out of bounds", array_index); + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + + PG_RETURN_NULL(); + } + + /* If we found the element and broke out of the loop to process the next segment, + we need to continue the outer loop */ + if (current_segment < num_segments) + continue; + } + else + { + /* Field name segment */ + if (token != WJB_BEGIN_OBJECT) + { + /* Not an object, can't use field name */ + /* Free allocated memory before returning */ + elog(WARNING, "segment '%s' is not an object, can't use field name", segment); + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + + PG_RETURN_NULL(); + } + + /* Navigate to the specified field */ + found = false; + + token = JsonbIteratorNext(&it, &v, true); + while (token != WJB_END_OBJECT) + { + /* We should be at a key */ + if (token != WJB_KEY) + { + /* Free allocated memory before returning */ + elog(WARNING, "unexpected token: expected WJB_KEY"); + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("unexpected JSONB iterator token"))); + } + + /* Check if this is the field we're looking for */ + if (v.type == jbvString && + strlen(segment) == v.val.string.len && + strncmp(segment, v.val.string.val, v.val.string.len) == 0) + { + found = true; + + /* Get the value */ + token = JsonbIteratorNext(&it, &v, true); + + /* If this is the last segment, return the value */ + if (current_segment == num_segments - 1) + { + /* Create a standalone JsonbValue for the result */ + result = palloc(sizeof(JsonbValue)); + *result = v; + + /* Convert to a Jsonb container */ + if (result->type == jbvBinary) + { + tmp_val.type = jbvBinary; + tmp_val.val.binary.data = result->val.binary.data; + tmp_val.val.binary.len = result->val.binary.len; + } + else + { + tmp_val = *result; + } + + /* Free allocated memory before returning */ + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + + /* Return the result as a new Jsonb */ + PG_RETURN_JSONB_P(JsonbValueToJsonb(&tmp_val)); + } + + /* Not the last segment, continue traversing */ + if (v.type == jbvBinary) + { + /* Nested container, need to iterate into it */ + JsonbIterator *nested_it = JsonbIteratorInit((JsonbContainer *) v.val.binary.data); + it = nested_it; + + /* Get the container type (array or object) */ + token = JsonbIteratorNext(&it, &v, true); + + /* Move to the next segment */ + current_segment++; + /* Continue processing from the next segment */ + break; + } + else + { + /* Scalar value, can't navigate further */ + /* Free allocated memory before returning */ + elog(WARNING, "scalar value at segment '%s', can't navigate further", segment); + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + + PG_RETURN_NULL(); + } + } + else + { + /* Skip this field's value */ + token = JsonbIteratorNext(&it, &v, true); + + if (v.type == jbvBinary) + { + /* Skip over the entire container */ + JsonbIterator *nested_it = JsonbIteratorInit((JsonbContainer *) v.val.binary.data); + JsonbIteratorToken nested_token; + JsonbValue nested_v; + + /* Skip until we reach the end of the container */ + do + { + nested_token = JsonbIteratorNext(&nested_it, &nested_v, false); + } while (nested_token != WJB_DONE); + } + } + + token = JsonbIteratorNext(&it, &v, true); + } + + if (!found) + { + /* Field not found */ + /* Free allocated memory before returning */ + elog(WARNING, "field not found, returning NULL"); + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + + PG_RETURN_NULL(); + } + + /* If we found the field and broke out of the loop to process the next segment, + we need to continue the outer loop */ + if (current_segment < num_segments) + continue; + } + + current_segment++; + } + + /* We should have returned by now if we found the value */ + /* Free allocated memory before returning */ + elog(WARNING, "unexpected end of function, returning NULL"); + for (i = 0; i < num_segments; i++) + pfree(segments[i]); + pfree(segments); + pfree(path_str); + + PG_RETURN_NULL(); } \ No newline at end of file diff --git a/package/c/hemar/hemar.h b/package/c/hemar/hemar.h index 0b9fe23..ff7f10f 100755 --- a/package/c/hemar/hemar.h +++ b/package/c/hemar/hemar.h @@ -1,115 +1,115 @@ -/* - * hemar.h - * Template parser for Hemar - */ -#ifndef HEMAR_TEMPLATE_H -#define HEMAR_TEMPLATE_H - -#include "postgres.h" -#include "utils/memutils.h" - -/* Maximum length for template syntax elements */ -#define TEMPLATE_MAX_PREFIX_LEN 32 - -/* Template error codes */ -typedef enum { - TEMPLATE_ERROR_NONE = 0, - TEMPLATE_ERROR_UNKNOWN_TAG, - TEMPLATE_UNEXPECTED_OPEN_BRACES_AFFTER_SECTION_CONTROLE, - TEMPLATE_UNEXPECTED_OPEN_BRACES_AFFTER_SECTION_SOURCE, - TEMPLATE_ERROR_UNEXPECTED_INTERPOLATION_END, - TEMPLATE_ERROR_NO_SOURSE_IN_SECTION, - TEMPLATE_ERROR_NESTED_INTERPOLATION, - TEMPLATE_ERROR_UNEXPECTED_SECTION_END, - TEMPLATE_ERROR_NO_BEGIN_IN_SECTION, - TEMPLATE_ERROR_NESTED_INCLUDE, - TEMPLATE_ERROR_NESTED_EXECUTE, - TEMPLATE_ERROR_INVALID_CONFIG, - TEMPLATE_ERROR_OUT_OF_MEMORY, - TEMPLATE_ERROR_UNEXPECTED_INCLUDE_END, - TEMPLATE_ERROR_UNEXPECTED_EXECUTE_END -} TemplateErrorCode; - -/* Template node types */ -typedef enum { - TEMPLATE_NODE_TEXT, - TEMPLATE_NODE_INTERPOLATE, - TEMPLATE_NODE_SECTION, - TEMPLATE_NODE_EXECUTE, - TEMPLATE_NODE_INCLUDE -} TemplateNodeType; - -/* Template configuration structure */ -typedef struct { - struct { - struct { - const char *open; /* Default: "{%" */ - const char *close; /* Default: "%}" */ - } Braces; - struct { - const char *control; /* default: "for " */ - const char *source; /* default: "in " */ - const char *begin; /* default: "do " */ - } Section; - struct { - const char *invoke; /* default: "" */ - } Interpolate; - struct { - const char *invoke; /* default: "include " */ - } Include; - struct { - const char *invoke; /* default: "exec " */ - } Execute; - const char *nesting; /* default: "->" */ - } Syntax; -} TemplateConfig; - -/* Forward declaration */ -typedef struct TemplateNode TemplateNode; - -/* Template value structures */ -typedef struct { - char *iterator; - char *collection; - TemplateNode *body; -} TemplateSectionValue; - -typedef struct { - char *key; -} TemplateInterpolateValue; - -typedef struct { - char *code; -} TemplateExecuteValue; - -typedef struct { - char *key; -} TemplateIncludeValue; - -typedef struct { - char *content; -} TemplateTextValue; - -typedef union { - TemplateSectionValue section; - TemplateInterpolateValue interpolate; - TemplateExecuteValue execute; - TemplateIncludeValue include; - TemplateTextValue text; -} TemplateValue; - -/* Template node structure */ -struct TemplateNode { - TemplateNodeType type; - TemplateValue *value; - TemplateNode *next; -}; - -/* Function declarations */ -TemplateConfig template_default_config(MemoryContext context); -bool template_validate_config(const TemplateConfig *config, TemplateErrorCode *error_code); -TemplateNode *template_parse(MemoryContext context, const char **s, const TemplateConfig *config, bool inner_parse, TemplateErrorCode *error_code); -void template_free_node(TemplateNode *node); -const char *template_error_to_string(TemplateErrorCode code, TemplateConfig *config); - -#endif /* HEMAR_TEMPLATE_H */ +/* + * hemar.h + * Template parser for Hemar + */ +#ifndef HEMAR_TEMPLATE_H +#define HEMAR_TEMPLATE_H + +#include "postgres.h" +#include "utils/memutils.h" + +/* Maximum length for template syntax elements */ +#define TEMPLATE_MAX_PREFIX_LEN 32 + +/* Template error codes */ +typedef enum { + TEMPLATE_ERROR_NONE = 0, + TEMPLATE_ERROR_UNKNOWN_TAG, + TEMPLATE_UNEXPECTED_OPEN_BRACES_AFFTER_SECTION_CONTROLE, + TEMPLATE_UNEXPECTED_OPEN_BRACES_AFFTER_SECTION_SOURCE, + TEMPLATE_ERROR_UNEXPECTED_INTERPOLATION_END, + TEMPLATE_ERROR_NO_SOURSE_IN_SECTION, + TEMPLATE_ERROR_NESTED_INTERPOLATION, + TEMPLATE_ERROR_UNEXPECTED_SECTION_END, + TEMPLATE_ERROR_NO_BEGIN_IN_SECTION, + TEMPLATE_ERROR_NESTED_INCLUDE, + TEMPLATE_ERROR_NESTED_EXECUTE, + TEMPLATE_ERROR_INVALID_CONFIG, + TEMPLATE_ERROR_OUT_OF_MEMORY, + TEMPLATE_ERROR_UNEXPECTED_INCLUDE_END, + TEMPLATE_ERROR_UNEXPECTED_EXECUTE_END +} TemplateErrorCode; + +/* Template node types */ +typedef enum { + TEMPLATE_NODE_TEXT, + TEMPLATE_NODE_INTERPOLATE, + TEMPLATE_NODE_SECTION, + TEMPLATE_NODE_EXECUTE, + TEMPLATE_NODE_INCLUDE +} TemplateNodeType; + +/* Template configuration structure */ +typedef struct { + struct { + struct { + const char *open; /* Default: "{%" */ + const char *close; /* Default: "%}" */ + } Braces; + struct { + const char *control; /* default: "for " */ + const char *source; /* default: "in " */ + const char *begin; /* default: "do " */ + } Section; + struct { + const char *invoke; /* default: "" */ + } Interpolate; + struct { + const char *invoke; /* default: "include " */ + } Include; + struct { + const char *invoke; /* default: "exec " */ + } Execute; + const char *nesting; /* default: "->" */ + } Syntax; +} TemplateConfig; + +/* Forward declaration */ +typedef struct TemplateNode TemplateNode; + +/* Template value structures */ +typedef struct { + char *iterator; + char *collection; + TemplateNode *body; +} TemplateSectionValue; + +typedef struct { + char *key; +} TemplateInterpolateValue; + +typedef struct { + char *code; +} TemplateExecuteValue; + +typedef struct { + char *key; +} TemplateIncludeValue; + +typedef struct { + char *content; +} TemplateTextValue; + +typedef union { + TemplateSectionValue section; + TemplateInterpolateValue interpolate; + TemplateExecuteValue execute; + TemplateIncludeValue include; + TemplateTextValue text; +} TemplateValue; + +/* Template node structure */ +struct TemplateNode { + TemplateNodeType type; + TemplateValue *value; + TemplateNode *next; +}; + +/* Function declarations */ +TemplateConfig template_default_config(MemoryContext context); +bool template_validate_config(const TemplateConfig *config, TemplateErrorCode *error_code); +TemplateNode *template_parse(MemoryContext context, const char **s, const TemplateConfig *config, bool inner_parse, TemplateErrorCode *error_code); +void template_free_node(TemplateNode *node); +const char *template_error_to_string(TemplateErrorCode code, TemplateConfig *config); + +#endif /* HEMAR_TEMPLATE_H */ \ 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 2e91254..10560e4 100755 --- a/package/c/hemar/test/test_jsonb_path.sql +++ b/package/c/hemar/test/test_jsonb_path.sql @@ -1,333 +1,267 @@ --- Test file for hemar.jsonb_get_by_path function +-- 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; - + current_path text; 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'); + current_path := 'name'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Simple field access (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Simple field access (string): % | PASSED: % (expected: "John Doe")', - total_tests, result, passed; + RAISE WARNING 'Test %: Simple field access (path="%"): % | PASSED: % (expected: "John Doe")', + total_tests, current_path, result, passed; END IF; total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, 'age'); + current_path := 'age'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Numeric field access (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Numeric field access: % | PASSED: % (expected: 30)', - total_tests, result, passed; + RAISE WARNING 'Test %: Numeric field access (path="%"): % | PASSED: % (expected: 30)', + total_tests, current_path, result, passed; END IF; total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, 'is_active'); - + current_path := 'is_active'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Boolean field access (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Boolean field access: % | PASSED: % (expected: true)', - total_tests, result, passed; + RAISE WARNING 'Test %: Boolean field access (path="%"): % | PASSED: % (expected: true)', + total_tests, current_path, result, passed; END IF; -- Test nested field access total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, 'address.city'); + current_path := 'address.city'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Nested object field access (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Nested object field access: % | PASSED: % (expected: "New York")', - - total_tests, result, passed; + RAISE WARNING 'Test %: Nested object field access (path="%"): % | PASSED: % (expected: "New York")', + total_tests, current_path, result, passed; END IF; -- Test array access total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, 'tags[1]'); + current_path := 'tags[1]'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Simple array access (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Simple array access: % | PASSED: % (expected: "postgresql")', - total_tests, result, passed; + RAISE WARNING 'Test %: Simple array access (path="%"): % | PASSED: % (expected: "postgresql")', + total_tests, current_path, result, passed; END IF; total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, 'contacts[0].type'); - + current_path := 'contacts[0].type'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Object in array access (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Object in array access: % | PASSED: % (expected: "email")', - total_tests, result, passed; + RAISE WARNING 'Test %: Object in array access (path="%"): % | PASSED: % (expected: "email")', + total_tests, current_path, result, passed; END IF; total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, 'skills[1][0]'); + current_path := 'skills[1][0]'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Nested array access (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Nested array access: % | PASSED: % (expected: "Python")', - total_tests, result, passed; + RAISE WARNING 'Test %: Nested array access (path="%"): % | PASSED: % (expected: "Python")', + total_tests, current_path, result, passed; END IF; total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, 'contacts[1].value'); + current_path := 'contacts[1].value'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Complex path with multiple array indices (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Complex path with multiple array indices: % | PASSED: % (expected: "555-1234")', - total_tests, result, passed; + RAISE WARNING 'Test %: Complex path with multiple array indices (path="%"): % | PASSED: % (expected: "555-1234")', + total_tests, current_path, result, passed; END IF; -- Test object and array returns total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, 'address'); + current_path := 'address'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Path to object (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Path to object: % | PASSED: % (expected type: object, got: %)', - - total_tests, result, passed, jsonb_typeof(result); + RAISE WARNING 'Test %: Path to object (path="%"): % | PASSED: % (expected type: object, got: %)', + total_tests, current_path, result, passed, jsonb_typeof(result); END IF; total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, 'contacts'); + current_path := 'contacts'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Path to array (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Path to array: % | PASSED: % (expected type: array, got: %)', - total_tests, result, passed, jsonb_typeof(result); + RAISE WARNING 'Test %: Path to array (path="%"): % | PASSED: % (expected type: array, got: %)', + total_tests, current_path, 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'); + current_path := 'unknown_field'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Non-existent field (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Non-existent field: % | PASSED: % (expected: NULL)', - - total_tests, result, passed; + RAISE WARNING 'Test %: Non-existent field (path="%"): % | PASSED: % (expected: NULL)', + total_tests, current_path, result, passed; END IF; total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, 'address.country'); + current_path := 'address.country'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; - + RAISE NOTICE 'Test %: Non-existent nested field (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - - RAISE WARNING 'Test %: Non-existent nested field: % | PASSED: % (expected: NULL)', - - total_tests, result, passed; + RAISE WARNING 'Test %: Non-existent nested field (path="%"): % | PASSED: % (expected: NULL)', + total_tests, current_path, result, passed; END IF; total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, 'tags[10]'); + current_path := 'tags[10]'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Array index out of bounds (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Array index out of bounds: % | PASSED: % (expected: NULL)', - total_tests, result, passed; + RAISE WARNING 'Test %: Array index out of bounds (path="%"): % | PASSED: % (expected: NULL)', + total_tests, current_path, result, passed; END IF; -- Test edge cases total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, ''); - + current_path := ''; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Empty path (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Empty path: % | PASSED: % (expected: NULL)', - total_tests, result, passed; + RAISE WARNING 'Test %: Empty path (path="%"): % | PASSED: % (expected: NULL)', + total_tests, current_path, result, passed; END IF; total_tests := total_tests + 1; - result := hemar.jsonb_get_by_path(test_json, 'skills[0][1]'); - + current_path := 'skills[0][1]'; + result := hemar.jsonb_get_by_path(test_json, current_path); 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; + RAISE NOTICE 'Test %: Multiple array indices (path="%"): % | PASSED: %', + total_tests, current_path, result, passed; ELSE - RAISE WARNING 'Test %: Multiple array indices: % | PASSED: % (expected: 5)', - total_tests, result, passed; + RAISE WARNING 'Test %: Multiple array indices (path="%"): % | PASSED: % (expected: 5)', + total_tests, current_path, result, passed; END IF; -- Print summary - RAISE NOTICE '------------------------------------'; IF passed_tests = total_tests THEN + RAISE NOTICE '------------------------------------'; RAISE NOTICE 'SUMMARY: % of % tests passed (100%%)', passed_tests, total_tests; + RAISE NOTICE '------------------------------------'; ELSE + RAISE WARNING '------------------------------------'; RAISE WARNING 'SUMMARY: % of % tests passed (%)', passed_tests, total_tests, round((passed_tests::numeric / total_tests::numeric) * 100, 2) || '%'; + RAISE WARNING '------------------------------------'; 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 $$; \ No newline at end of file +END $$;