2689 lines
98 KiB
C
Executable File
2689 lines
98 KiB
C
Executable File
/*
|
|
* hemar.c
|
|
* Template parser implementation for Hemar
|
|
*/
|
|
#include "hemar.h"
|
|
#include "postgres.h"
|
|
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
|
|
#include "utils/elog.h"
|
|
#include "utils/memutils.h"
|
|
#include "mb/pg_wchar.h"
|
|
#include "fmgr.h"
|
|
#include "utils/builtins.h"
|
|
#include "lib/stringinfo.h"
|
|
#include "utils/jsonb.h"
|
|
#include "catalog/pg_type.h"
|
|
#include "executor/spi.h"
|
|
#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 direct_array_check(Datum jsonb_context, const char *path, bool *found);
|
|
static int get_jsonb_array_length(Datum jsonb_array);
|
|
static int direct_array_length(Datum jsonb_array);
|
|
static Datum get_jsonb_array_element(Datum jsonb_array, int index);
|
|
static Datum direct_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 void debug_jsonb_value(Datum jsonb_value, const char *label);
|
|
static bool is_jsonb_container_valid(JsonbContainer *container);
|
|
static JsonbValue *get_jsonb_value_by_path(Jsonb *jb, const char *path, bool *found);
|
|
|
|
/* 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)
|
|
{
|
|
while (*s && isspace((unsigned char)*s))
|
|
s++;
|
|
return s;
|
|
}
|
|
|
|
/* Initialize template value based on type */
|
|
static TemplateValue
|
|
init_template_value(TemplateNodeType type)
|
|
{
|
|
TemplateValue value;
|
|
|
|
switch (type)
|
|
{
|
|
case TEMPLATE_NODE_TEXT:
|
|
value.text.content = NULL;
|
|
break;
|
|
case TEMPLATE_NODE_INTERPOLATE:
|
|
value.interpolate.key = NULL;
|
|
break;
|
|
case TEMPLATE_NODE_SECTION:
|
|
value.section.iterator = NULL;
|
|
value.section.collection = NULL;
|
|
value.section.body = NULL;
|
|
break;
|
|
case TEMPLATE_NODE_EXECUTE:
|
|
value.execute.code = NULL;
|
|
break;
|
|
case TEMPLATE_NODE_INCLUDE:
|
|
value.include.key = NULL;
|
|
break;
|
|
default:
|
|
elog(ERROR, "Unknown template node type: %d", type);
|
|
/* This won't be reached due to elog(ERROR) */
|
|
memset(&value, 0, sizeof(value));
|
|
break;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
/* Initialize a new template node */
|
|
static TemplateNode *
|
|
init_template_node(MemoryContext context, TemplateNodeType type)
|
|
{
|
|
TemplateNode *node;
|
|
|
|
node = (TemplateNode *) MemoryContextAlloc(context, sizeof(TemplateNode));
|
|
node->next = NULL;
|
|
node->type = type;
|
|
node->value = (TemplateValue *) MemoryContextAlloc(context, sizeof(TemplateValue));
|
|
*node->value = init_template_value(type);
|
|
|
|
return node;
|
|
}
|
|
|
|
/* Error code to string conversion */
|
|
const char *
|
|
template_error_to_string(TemplateErrorCode code, TemplateConfig *config)
|
|
{
|
|
char *message = "";
|
|
switch (code)
|
|
{
|
|
case TEMPLATE_ERROR_NONE:
|
|
return "No error";
|
|
case TEMPLATE_ERROR_UNKNOWN_TAG:
|
|
return "Unknown tag";
|
|
case TEMPLATE_ERROR_NESTED_INTERPOLATION:
|
|
return "Nested interpolation";
|
|
case TEMPLATE_UNEXPECTED_OPEN_BRACES_AFFTER_SECTION_CONTROLE:
|
|
message = "Found `";
|
|
strcat(message, config->Syntax.Braces.open);
|
|
strcat(message, "` in `");
|
|
strcat(message, config->Syntax.Section.control);
|
|
strcat(message, "` in section block");
|
|
return message;
|
|
case TEMPLATE_UNEXPECTED_OPEN_BRACES_AFFTER_SECTION_SOURCE:
|
|
message = "Found `";
|
|
strcat(message, config->Syntax.Braces.open);
|
|
strcat(message, "` in `");
|
|
strcat(message, config->Syntax.Section.source);
|
|
strcat(message, "` in section block");
|
|
return message;
|
|
case TEMPLATE_ERROR_UNEXPECTED_INTERPOLATION_END:
|
|
return "Unexpected interpolation end";
|
|
case TEMPLATE_ERROR_NO_SOURSE_IN_SECTION:
|
|
message = "Not found `";
|
|
strcat(message, config->Syntax.Section.source);
|
|
strcat(message, "` keyword in section block");
|
|
return message;
|
|
case TEMPLATE_ERROR_NO_BEGIN_IN_SECTION:
|
|
message = "Not found `";
|
|
strcat(message, config->Syntax.Section.begin);
|
|
strcat(message, "` keyword in section block");
|
|
return message;
|
|
case TEMPLATE_ERROR_UNEXPECTED_SECTION_END:
|
|
return "Unexpected section end";
|
|
case TEMPLATE_ERROR_NESTED_INCLUDE:
|
|
return "Nested include";
|
|
case TEMPLATE_ERROR_NESTED_EXECUTE:
|
|
return "Nested execute";
|
|
case TEMPLATE_ERROR_INVALID_CONFIG:
|
|
return "Invalid config";
|
|
case TEMPLATE_ERROR_OUT_OF_MEMORY:
|
|
return "Out of memory";
|
|
case TEMPLATE_ERROR_UNEXPECTED_INCLUDE_END:
|
|
return "Unexpected include end";
|
|
case TEMPLATE_ERROR_UNEXPECTED_EXECUTE_END:
|
|
return "Unexpected execute end";
|
|
default:
|
|
return "Unknown error";
|
|
}
|
|
}
|
|
|
|
/* Default template configuration */
|
|
TemplateConfig
|
|
template_default_config(MemoryContext context)
|
|
{
|
|
TemplateConfig config;
|
|
|
|
config.Syntax.Braces.open = "{%";
|
|
config.Syntax.Braces.close = "%}";
|
|
config.Syntax.Section.control = "for ";
|
|
config.Syntax.Section.source = "in ";
|
|
config.Syntax.Section.begin = "do ";
|
|
config.Syntax.Interpolate.invoke = "";
|
|
config.Syntax.Include.invoke = "include ";
|
|
config.Syntax.Execute.invoke = "exec ";
|
|
config.Syntax.nesting = "->";
|
|
|
|
return config;
|
|
}
|
|
|
|
/* Validate template configuration */
|
|
bool
|
|
template_validate_config(const TemplateConfig *config, TemplateErrorCode *error_code)
|
|
{
|
|
if (!config)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_INVALID_CONFIG;
|
|
return false;
|
|
}
|
|
|
|
/* Check open brace */
|
|
if (!config->Syntax.Braces.open || strlen(config->Syntax.Braces.open) > TEMPLATE_MAX_PREFIX_LEN)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_INVALID_CONFIG;
|
|
return false;
|
|
}
|
|
|
|
/* Check close brace */
|
|
if (!config->Syntax.Braces.close || strlen(config->Syntax.Braces.close) > TEMPLATE_MAX_PREFIX_LEN)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_INVALID_CONFIG;
|
|
return false;
|
|
}
|
|
|
|
/* Check section control */
|
|
if (!config->Syntax.Section.control || strlen(config->Syntax.Section.control) > TEMPLATE_MAX_PREFIX_LEN)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_INVALID_CONFIG;
|
|
return false;
|
|
}
|
|
|
|
/* Check section source */
|
|
if (!config->Syntax.Section.source || strlen(config->Syntax.Section.source) > TEMPLATE_MAX_PREFIX_LEN)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_INVALID_CONFIG;
|
|
return false;
|
|
}
|
|
|
|
/* Check section begin */
|
|
if (!config->Syntax.Section.begin || strlen(config->Syntax.Section.begin) > TEMPLATE_MAX_PREFIX_LEN)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_INVALID_CONFIG;
|
|
return false;
|
|
}
|
|
|
|
/* Check interpolate invoke */
|
|
if (!config->Syntax.Interpolate.invoke || strlen(config->Syntax.Interpolate.invoke) > TEMPLATE_MAX_PREFIX_LEN)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_INVALID_CONFIG;
|
|
return false;
|
|
}
|
|
|
|
/* Check include invoke */
|
|
if (!config->Syntax.Include.invoke || strlen(config->Syntax.Include.invoke) > TEMPLATE_MAX_PREFIX_LEN)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_INVALID_CONFIG;
|
|
return false;
|
|
}
|
|
|
|
/* Check execute invoke */
|
|
if (!config->Syntax.Execute.invoke || strlen(config->Syntax.Execute.invoke) > TEMPLATE_MAX_PREFIX_LEN)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_INVALID_CONFIG;
|
|
return false;
|
|
}
|
|
|
|
/* Check nesting */
|
|
if (!config->Syntax.nesting || strlen(config->Syntax.nesting) > TEMPLATE_MAX_PREFIX_LEN)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_INVALID_CONFIG;
|
|
return false;
|
|
}
|
|
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_NONE;
|
|
return true;
|
|
}
|
|
|
|
/* Parse interpolation tag */
|
|
static TemplateNode *
|
|
template_parse_interpolation(MemoryContext context, const char **s_ptr,
|
|
const TemplateConfig *config, TemplateErrorCode *error_code)
|
|
{
|
|
const char **s = s_ptr;
|
|
const char *key_start;
|
|
size_t key_len;
|
|
TemplateNode *node;
|
|
|
|
node = init_template_node(context, TEMPLATE_NODE_INTERPOLATE);
|
|
|
|
/* Skip to the content */
|
|
*s += strlen(config->Syntax.Braces.open);
|
|
*s = skip_whitespace(*s);
|
|
*s += strlen(config->Syntax.Interpolate.invoke);
|
|
|
|
*s = skip_whitespace(*s);
|
|
key_start = *s;
|
|
|
|
while (**s != '\0')
|
|
{
|
|
if (isspace((unsigned char)**s) ||
|
|
strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
|
|
break;
|
|
|
|
if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_NESTED_INTERPOLATION;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
(*s)++;
|
|
}
|
|
|
|
key_len = *s - key_start;
|
|
node->value->interpolate.key = MemoryContextStrdup(context, pnstrdup(key_start, key_len));
|
|
elog(DEBUG1, "Parsing: %s", node->value->interpolate.key);
|
|
|
|
*s = skip_whitespace(*s);
|
|
|
|
/* Check for closing brace */
|
|
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) != 0)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_UNEXPECTED_INTERPOLATION_END;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
*s_ptr = *s + strlen(config->Syntax.Braces.close);
|
|
|
|
return node;
|
|
}
|
|
|
|
/* Parse section tag */
|
|
static TemplateNode *
|
|
template_parse_section(MemoryContext context, const char **s_ptr,
|
|
const TemplateConfig *config, TemplateErrorCode *error_code)
|
|
{
|
|
const char **s = s_ptr;
|
|
const char *iterator_start, *collection_start;
|
|
size_t iterator_len, collection_len;
|
|
TemplateNode *node, *body_node;
|
|
|
|
node = init_template_node(context, TEMPLATE_NODE_SECTION);
|
|
|
|
/* Skip to the content */
|
|
*s += strlen(config->Syntax.Braces.open);
|
|
*s = skip_whitespace(*s);
|
|
*s += strlen(config->Syntax.Section.control);
|
|
|
|
/* Find the iterator name */
|
|
*s = skip_whitespace(*s);
|
|
iterator_start = *s;
|
|
|
|
while (**s != '\0')
|
|
{
|
|
if (isspace((unsigned char)**s) ||
|
|
strncmp(*s, config->Syntax.Section.source, strlen(config->Syntax.Section.source)) == 0)
|
|
break;
|
|
|
|
if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_UNEXPECTED_OPEN_BRACES_AFFTER_SECTION_CONTROLE;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_NO_SOURSE_IN_SECTION;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
(*s)++;
|
|
}
|
|
|
|
iterator_len = *s - iterator_start;
|
|
node->value->section.iterator = MemoryContextStrdup(context, pnstrdup(iterator_start, iterator_len));
|
|
elog(DEBUG1, "Parsed section iterator: %s", node->value->section.iterator);
|
|
|
|
/* Find the collection name */
|
|
*s = skip_whitespace(*s);
|
|
|
|
if (strncmp(*s, config->Syntax.Section.source, strlen(config->Syntax.Section.source)) != 0)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_NO_SOURSE_IN_SECTION;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
*s += strlen(config->Syntax.Section.source);
|
|
*s = skip_whitespace(*s);
|
|
collection_start = *s;
|
|
|
|
while (**s != '\0')
|
|
{
|
|
if (isspace((unsigned char)**s) ||
|
|
strncmp(*s, config->Syntax.Section.begin, strlen(config->Syntax.Section.begin)) == 0)
|
|
break;
|
|
|
|
if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_UNEXPECTED_OPEN_BRACES_AFFTER_SECTION_SOURCE;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_NO_BEGIN_IN_SECTION;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
(*s)++;
|
|
}
|
|
|
|
collection_len = *s - collection_start;
|
|
node->value->section.collection = MemoryContextStrdup(context, pnstrdup(collection_start, collection_len));
|
|
elog(DEBUG1, "Parsed section collection: %s", node->value->section.collection);
|
|
|
|
/* Check for 'do' keyword */
|
|
*s = skip_whitespace(*s);
|
|
// TODO: why check begin second time, first in while
|
|
if (strncmp(*s, config->Syntax.Section.begin, strlen(config->Syntax.Section.begin)) != 0)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_UNEXPECTED_OPEN_BRACES_AFFTER_SECTION_SOURCE;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
*s += strlen(config->Syntax.Section.begin);
|
|
*s = skip_whitespace(*s);
|
|
|
|
/* Check if there's a closing brace right after 'do' */
|
|
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
|
|
{
|
|
/* Empty section body */
|
|
elog(DEBUG1, "Parsed empty section body");
|
|
*s_ptr = *s + strlen(config->Syntax.Braces.close);
|
|
node->value->section.body = NULL;
|
|
return node;
|
|
}
|
|
|
|
/* Parse the body as a normal template */
|
|
const char *body_start = *s;
|
|
const char *original_s = *s;
|
|
|
|
int inner_braces_opened_count = 0;
|
|
|
|
/* Find the end of the section */
|
|
while (**s) {
|
|
// s = {% a %} %}
|
|
elog(DEBUG1, "Step, braces opened: %d, s: %s", inner_braces_opened_count, *s);
|
|
if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0) {
|
|
elog(DEBUG1, "inner_braces_opened_count++");
|
|
inner_braces_opened_count++;
|
|
}
|
|
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0) {
|
|
if (inner_braces_opened_count > 0) {
|
|
elog(DEBUG1, "inner_braces_opened_count--");
|
|
inner_braces_opened_count--;
|
|
}
|
|
else {
|
|
elog(DEBUG1, "exit");
|
|
break;
|
|
}
|
|
}
|
|
(*s)++;
|
|
}
|
|
|
|
if (!**s)
|
|
{
|
|
/* Unexpected end of string before closing brace */
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_UNEXPECTED_SECTION_END;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
/* Extract the body content */
|
|
size_t body_len = *s - body_start;
|
|
char *body_content = pnstrdup(body_start, body_len);
|
|
|
|
elog(DEBUG1, "Section body content: %s", body_content);
|
|
|
|
/* Parse the body content as a template */
|
|
const char *body_ptr = body_content;
|
|
body_node = template_parse(context, &body_ptr, config, false, error_code);
|
|
|
|
if (!body_node)
|
|
{
|
|
elog(WARNING, "Failed to parse section body: %s", body_content);
|
|
pfree(body_content);
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
pfree(body_content);
|
|
node->value->section.body = body_node;
|
|
|
|
/* Set the pointer to after the closing brace */
|
|
*s_ptr = *s + strlen(config->Syntax.Braces.close);
|
|
|
|
return node;
|
|
}
|
|
|
|
/* Parse include tag */
|
|
static TemplateNode *
|
|
template_parse_include(MemoryContext context, const char **s_ptr,
|
|
const TemplateConfig *config, TemplateErrorCode *error_code)
|
|
{
|
|
const char **s = s_ptr;
|
|
const char *include_start;
|
|
size_t include_len;
|
|
TemplateNode *node;
|
|
|
|
node = init_template_node(context, TEMPLATE_NODE_INCLUDE);
|
|
|
|
/* Skip to the content */
|
|
*s += strlen(config->Syntax.Braces.open);
|
|
*s = skip_whitespace(*s);
|
|
*s += strlen(config->Syntax.Include.invoke);
|
|
|
|
*s = skip_whitespace(*s);
|
|
include_start = *s;
|
|
|
|
while (**s != '\0')
|
|
{
|
|
if (isspace((unsigned char)**s) ||
|
|
strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
|
|
break;
|
|
|
|
if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_NESTED_INCLUDE;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
(*s)++;
|
|
}
|
|
|
|
include_len = *s - include_start;
|
|
node->value->include.key = MemoryContextStrdup(context, pnstrdup(include_start, include_len));
|
|
|
|
*s = skip_whitespace(*s);
|
|
|
|
/* Check for closing brace */
|
|
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) != 0)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_UNEXPECTED_INCLUDE_END;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
*s_ptr = *s + strlen(config->Syntax.Braces.close);
|
|
|
|
return node;
|
|
}
|
|
|
|
/* Parse execute tag */
|
|
static TemplateNode *
|
|
template_parse_execute(MemoryContext context, const char **s_ptr,
|
|
const TemplateConfig *config, TemplateErrorCode *error_code)
|
|
{
|
|
const char **s = s_ptr;
|
|
const char *code_start;
|
|
size_t code_len;
|
|
TemplateNode *node;
|
|
|
|
node = init_template_node(context, TEMPLATE_NODE_EXECUTE);
|
|
|
|
/* Skip to the content */
|
|
*s += strlen(config->Syntax.Braces.open);
|
|
*s = skip_whitespace(*s);
|
|
*s += strlen(config->Syntax.Execute.invoke);
|
|
|
|
*s = skip_whitespace(*s);
|
|
code_start = *s;
|
|
|
|
while (**s != '\0')
|
|
{
|
|
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
|
|
break;
|
|
|
|
if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_NESTED_EXECUTE;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
(*s)++;
|
|
}
|
|
|
|
code_len = *s - code_start;
|
|
node->value->execute.code = MemoryContextStrdup(context, pnstrdup(code_start, code_len));
|
|
|
|
/* Check for closing brace */
|
|
if (strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) != 0)
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_UNEXPECTED_EXECUTE_END;
|
|
template_free_node(node);
|
|
return NULL;
|
|
}
|
|
|
|
*s_ptr = *s + strlen(config->Syntax.Braces.close);
|
|
|
|
return node;
|
|
}
|
|
|
|
/* Main template parser function */
|
|
TemplateNode *
|
|
template_parse(MemoryContext context, const char **s, const TemplateConfig *config,
|
|
bool inner_parse, TemplateErrorCode *error_code)
|
|
{
|
|
const char *start;
|
|
TemplateNode *root, *current, *tag_node;
|
|
bool current_node_filled = false;
|
|
const char *tag_prefix;
|
|
size_t text_len;
|
|
|
|
if (!template_validate_config(config, error_code))
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/* Check for empty template */
|
|
if (!s || !*s || !**s)
|
|
{
|
|
TemplateNode *empty_node = init_template_node(context, TEMPLATE_NODE_TEXT);
|
|
empty_node->value->text.content = MemoryContextStrdup(context, "");
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_NONE;
|
|
return empty_node;
|
|
}
|
|
|
|
start = *s;
|
|
root = init_template_node(context, TEMPLATE_NODE_TEXT);
|
|
current = root;
|
|
|
|
while (*s && **s != '\0')
|
|
{
|
|
/* Check for closing brace in inner parse */
|
|
if (inner_parse && strncmp(*s, config->Syntax.Braces.close, strlen(config->Syntax.Braces.close)) == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (strncmp(*s, config->Syntax.Braces.open, strlen(config->Syntax.Braces.open)) == 0)
|
|
{
|
|
/* Handle text before tag */
|
|
if (start != *s)
|
|
{
|
|
if (current_node_filled)
|
|
{
|
|
TemplateNode *new_node = init_template_node(context, TEMPLATE_NODE_TEXT);
|
|
current->next = new_node;
|
|
current = new_node;
|
|
}
|
|
else
|
|
{
|
|
current->type = TEMPLATE_NODE_TEXT;
|
|
*current->value = init_template_value(TEMPLATE_NODE_TEXT);
|
|
}
|
|
|
|
text_len = *s - start;
|
|
current->value->text.content = MemoryContextStrdup(context, pnstrdup(start, text_len));
|
|
current_node_filled = true;
|
|
}
|
|
|
|
/* Parse the tag */
|
|
tag_node = NULL;
|
|
tag_prefix = *s + strlen(config->Syntax.Braces.open);
|
|
tag_prefix = skip_whitespace(tag_prefix);
|
|
|
|
/* Determine tag type by prefix */
|
|
if (strncmp(tag_prefix, config->Syntax.Section.control, strlen(config->Syntax.Section.control)) == 0)
|
|
{
|
|
tag_node = template_parse_section(context, s, config, error_code);
|
|
}
|
|
else if (strncmp(tag_prefix, config->Syntax.Include.invoke, strlen(config->Syntax.Include.invoke)) == 0)
|
|
{
|
|
tag_node = template_parse_include(context, s, config, error_code);
|
|
}
|
|
else if (strncmp(tag_prefix, config->Syntax.Execute.invoke, strlen(config->Syntax.Execute.invoke)) == 0)
|
|
{
|
|
tag_node = template_parse_execute(context, s, config, error_code);
|
|
}
|
|
else if (strncmp(tag_prefix, config->Syntax.Interpolate.invoke, strlen(config->Syntax.Interpolate.invoke)) == 0)
|
|
{
|
|
tag_node = template_parse_interpolation(context, s, config, error_code);
|
|
}
|
|
else
|
|
{
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_UNKNOWN_TAG;
|
|
template_free_node(root);
|
|
return NULL;
|
|
}
|
|
|
|
if (!tag_node)
|
|
{
|
|
template_free_node(root);
|
|
return NULL;
|
|
}
|
|
|
|
if (current_node_filled)
|
|
{
|
|
current->next = tag_node;
|
|
current = tag_node;
|
|
}
|
|
else
|
|
{
|
|
*current = *tag_node;
|
|
pfree(tag_node);
|
|
}
|
|
|
|
current_node_filled = true;
|
|
start = *s;
|
|
}
|
|
else
|
|
{
|
|
(*s)++;
|
|
}
|
|
}
|
|
|
|
/* Handle remaining text */
|
|
if (start != *s)
|
|
{
|
|
if (current_node_filled)
|
|
{
|
|
TemplateNode *new_node = init_template_node(context, TEMPLATE_NODE_TEXT);
|
|
current->next = new_node;
|
|
current = new_node;
|
|
}
|
|
else
|
|
{
|
|
current->type = TEMPLATE_NODE_TEXT;
|
|
*current->value = init_template_value(TEMPLATE_NODE_TEXT);
|
|
}
|
|
|
|
text_len = *s - start;
|
|
current->value->text.content = MemoryContextStrdup(context, pnstrdup(start, text_len));
|
|
current_node_filled = true;
|
|
}
|
|
|
|
/* If no nodes were created, ensure we have at least an empty text node */
|
|
if (!current_node_filled)
|
|
{
|
|
current->type = TEMPLATE_NODE_TEXT;
|
|
*current->value = init_template_value(TEMPLATE_NODE_TEXT);
|
|
current->value->text.content = MemoryContextStrdup(context, "");
|
|
}
|
|
|
|
if (error_code)
|
|
*error_code = TEMPLATE_ERROR_NONE;
|
|
|
|
return root;
|
|
}
|
|
|
|
/* Free a template node and all its children */
|
|
void
|
|
template_free_node(TemplateNode *node)
|
|
{
|
|
TemplateNode *current, *next;
|
|
|
|
if (!node)
|
|
return;
|
|
|
|
current = node;
|
|
|
|
while (current)
|
|
{
|
|
next = current->next;
|
|
|
|
if (current->value)
|
|
{
|
|
switch (current->type)
|
|
{
|
|
case TEMPLATE_NODE_TEXT:
|
|
if (current->value->text.content)
|
|
pfree(current->value->text.content);
|
|
break;
|
|
|
|
case TEMPLATE_NODE_INTERPOLATE:
|
|
if (current->value->interpolate.key)
|
|
pfree(current->value->interpolate.key);
|
|
break;
|
|
|
|
case TEMPLATE_NODE_SECTION:
|
|
if (current->value->section.iterator)
|
|
pfree(current->value->section.iterator);
|
|
if (current->value->section.collection)
|
|
pfree(current->value->section.collection);
|
|
if (current->value->section.body)
|
|
template_free_node(current->value->section.body);
|
|
break;
|
|
|
|
case TEMPLATE_NODE_EXECUTE:
|
|
if (current->value->execute.code)
|
|
pfree(current->value->execute.code);
|
|
break;
|
|
|
|
case TEMPLATE_NODE_INCLUDE:
|
|
if (current->value->include.key)
|
|
pfree(current->value->include.key);
|
|
break;
|
|
|
|
default:
|
|
/* Should not happen */
|
|
break;
|
|
}
|
|
|
|
pfree(current->value);
|
|
}
|
|
|
|
pfree(current);
|
|
current = next;
|
|
}
|
|
}
|
|
|
|
/* 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");
|
|
|
|
/* Use PG_TRY/PG_CATCH to handle errors during the entire rendering process */
|
|
PG_TRY();
|
|
{
|
|
while (current)
|
|
{
|
|
switch (current->type)
|
|
{
|
|
case TEMPLATE_NODE_TEXT:
|
|
if (current->value->text.content)
|
|
{
|
|
elog(DEBUG1, "Rendering text node: %s", current->value->text.content);
|
|
appendStringInfoString(&result, current->value->text.content);
|
|
}
|
|
break;
|
|
|
|
case TEMPLATE_NODE_INTERPOLATE:
|
|
case TEMPLATE_NODE_SECTION:
|
|
case TEMPLATE_NODE_EXECUTE:
|
|
case TEMPLATE_NODE_INCLUDE:
|
|
PG_TRY();
|
|
{
|
|
if (current->type == TEMPLATE_NODE_INTERPOLATE)
|
|
{
|
|
/* Process interpolation node */
|
|
char *value = NULL;
|
|
bool found = false;
|
|
|
|
if (current->value->interpolate.key)
|
|
{
|
|
elog(DEBUG1, "Processing interpolation for key: %s", current->value->interpolate.key);
|
|
|
|
/* 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);
|
|
|
|
if (found && value)
|
|
{
|
|
elog(DEBUG1, "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, "Found array for key %s, converting to string", current->value->interpolate.key);
|
|
debug_jsonb_value(array_value, "Array for interpolation");
|
|
|
|
/* 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))
|
|
{
|
|
int array_length = get_jsonb_array_length(array_value);
|
|
int i;
|
|
Datum elem;
|
|
char *elem_str = NULL;
|
|
bool elem_found = false;
|
|
|
|
elog(DEBUG1, "Array length: %d", array_length);
|
|
|
|
for (i = 0; i < array_length; i++)
|
|
{
|
|
if (i > 0)
|
|
appendStringInfoString(&array_str, ", ");
|
|
|
|
elem = get_jsonb_array_element(array_value, i);
|
|
if (elem != (Datum) 0)
|
|
{
|
|
elem_found = false;
|
|
|
|
/* Try to get value from the element */
|
|
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, "null");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
appendStringInfoString(&array_str, "null");
|
|
}
|
|
}
|
|
}
|
|
|
|
appendStringInfoString(&array_str, "]");
|
|
appendStringInfoString(&result, array_str.data);
|
|
pfree(array_str.data);
|
|
}
|
|
else
|
|
{
|
|
elog(DEBUG1, "Key %s not found in context", current->value->interpolate.key);
|
|
/* Optionally append something to indicate missing key */
|
|
appendStringInfoString(&result, "");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (current->type == TEMPLATE_NODE_SECTION)
|
|
{
|
|
/* Handle sections (loops) */
|
|
char *collection_path = current->value->section.collection;
|
|
Datum array_value;
|
|
bool found = 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, "Processing section with collection path: %s", collection_path);
|
|
|
|
/* First try with the standard function */
|
|
array_value = get_jsonb_array(jsonb_context, collection_path, &found);
|
|
|
|
/* If not found, try direct check */
|
|
if (!found)
|
|
{
|
|
elog(DEBUG1, "Standard array check failed, trying direct check");
|
|
array_value = direct_array_check(jsonb_context, collection_path, &found);
|
|
}
|
|
|
|
if (found)
|
|
{
|
|
elog(DEBUG1, "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, "Invalid JSONB array container for path: %s", collection_path);
|
|
break;
|
|
}
|
|
|
|
/* First try with standard array length function */
|
|
array_length = get_jsonb_array_length(array_value);
|
|
elog(DEBUG1, "Standard array length for %s: %d", collection_path, array_length);
|
|
|
|
/* If that fails, try direct approach */
|
|
if (array_length <= 0)
|
|
{
|
|
array_length = direct_array_length(array_value);
|
|
elog(DEBUG1, "Direct array length for %s: %d", collection_path, array_length);
|
|
}
|
|
|
|
elog(DEBUG1, "Found array with length: %d", array_length);
|
|
debug_jsonb_value(array_value, "Section Array");
|
|
|
|
/* Handle empty arrays gracefully */
|
|
if (array_length <= 0)
|
|
{
|
|
elog(DEBUG1, "Array is empty, skipping section");
|
|
break;
|
|
}
|
|
|
|
/* If section body is empty, nothing to do */
|
|
if (current->value->section.body == NULL)
|
|
{
|
|
elog(DEBUG1, "Section body is empty, skipping");
|
|
break;
|
|
}
|
|
|
|
elog(DEBUG1, "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, "Section body structure: %s", section_info.data);
|
|
pfree(section_info.data);
|
|
}
|
|
|
|
for (i = 0; i < array_length; i++)
|
|
{
|
|
item_context = (Datum) 0;
|
|
item_error = false;
|
|
|
|
elog(DEBUG1, "Processing array element %d", i);
|
|
|
|
/* Safely get array element with error handling */
|
|
PG_TRY();
|
|
{
|
|
/* First try standard method */
|
|
item_context = get_jsonb_array_element(array_value, i);
|
|
|
|
/* If that fails, try direct approach */
|
|
if (item_context == (Datum) 0)
|
|
{
|
|
elog(DEBUG1, "Standard array element extraction failed, trying direct method");
|
|
item_context = direct_array_element(array_value, i);
|
|
}
|
|
|
|
/* 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, "Got valid array element %d", i);
|
|
debug_jsonb_value(item_context, "Array Element");
|
|
}
|
|
else
|
|
{
|
|
elog(WARNING, "Got invalid JSONB container for array element %d", i);
|
|
item_context = (Datum) 0; /* Reset to create empty object below */
|
|
}
|
|
}
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
elog(WARNING, "Error getting 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();
|
|
}
|
|
PG_END_TRY();
|
|
|
|
if (item_context == (Datum) 0)
|
|
{
|
|
elog(DEBUG1, "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));
|
|
}
|
|
|
|
/* Create context with iterator variable */
|
|
PG_TRY();
|
|
{
|
|
merged_context = create_iterator_context(jsonb_context, current->value->section.iterator, item_context);
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
elog(WARNING, "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, "Failed to create merged context for array element %d", 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, "Error rendering template section for array element %d", i);
|
|
*error = true;
|
|
return result.data;
|
|
}
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
elog(WARNING, "Exception during template rendering for array element %d", i);
|
|
/* Continue with next element */
|
|
FlushErrorState();
|
|
}
|
|
PG_END_TRY();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
elog(DEBUG1, "Collection not found: %s", collection_path);
|
|
}
|
|
}
|
|
}
|
|
else if (current->type == TEMPLATE_NODE_INCLUDE)
|
|
{
|
|
/* Handle includes */
|
|
char *template_key = current->value->include.key;
|
|
Datum include_data;
|
|
bool found = false;
|
|
|
|
if (template_key)
|
|
{
|
|
elog(DEBUG1, "Processing include with key: %s", template_key);
|
|
|
|
/* Find include template in context */
|
|
include_data = get_jsonb_include_template(jsonb_context, template_key, &found);
|
|
|
|
if (found)
|
|
{
|
|
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, "Error rendering included template: %s", template_key);
|
|
*error = true;
|
|
}
|
|
|
|
template_free_node(include_node);
|
|
}
|
|
else
|
|
{
|
|
elog(WARNING, "Error parsing included template: %s", template_key);
|
|
*error = true;
|
|
}
|
|
|
|
pfree(include_template);
|
|
}
|
|
else
|
|
{
|
|
elog(WARNING, "Included template is null: %s", template_key);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
elog(DEBUG1, "Include key not found: %s", template_key);
|
|
}
|
|
}
|
|
}
|
|
else if (current->type == TEMPLATE_NODE_EXECUTE)
|
|
{
|
|
/* Execute is not implemented in this version */
|
|
elog(DEBUG1, "Execute node type not implemented");
|
|
}
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
elog(WARNING, "Error processing template node of type %d", current->type);
|
|
FlushErrorState();
|
|
/* Continue with next node */
|
|
}
|
|
PG_END_TRY();
|
|
break;
|
|
|
|
default:
|
|
/* Unknown node type */
|
|
elog(WARNING, "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;
|
|
}
|
|
|
|
/* 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)
|
|
{
|
|
if (jbv_result->type == jbvString)
|
|
{
|
|
result = pnstrdup(jbv_result->val.string.val, jbv_result->val.string.len);
|
|
elog(DEBUG1, "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, "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, "Found boolean value for key %s: %s", path, result);
|
|
}
|
|
else if (jbv_result->type == jbvNull)
|
|
{
|
|
result = pstrdup("");
|
|
elog(DEBUG1, "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, "Found array value for key %s", path);
|
|
}
|
|
else if (token == WJB_BEGIN_OBJECT)
|
|
{
|
|
/* For objects, convert to "{Object}" placeholder */
|
|
result = pstrdup("{Object}");
|
|
elog(DEBUG1, "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, "Found complex value for key %s", path);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = pstrdup("[Invalid Binary]");
|
|
elog(DEBUG1, "Found invalid binary value for key %s", path);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
elog(DEBUG1, "Path %s not found in object", path);
|
|
}
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
elog(WARNING, "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)
|
|
{
|
|
Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_context);
|
|
JsonbValue *jbv_result;
|
|
JsonbIterator *it;
|
|
JsonbValue v;
|
|
JsonbIteratorToken token;
|
|
Datum result = (Datum) 0;
|
|
|
|
*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 new path traversal function */
|
|
bool path_found = false;
|
|
jbv_result = get_jsonb_value_by_path(jb, path, &path_found);
|
|
|
|
if (path_found && jbv_result)
|
|
{
|
|
elog(DEBUG1, "Found value for key %s (type=%d)", path, 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 = PointerGetDatum(JsonbValueToJsonb(jbv_result));
|
|
elog(DEBUG1, "Found array at path %s (result=%p)", path, DatumGetPointer(result));
|
|
debug_jsonb_value(result, "Array");
|
|
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 int
|
|
get_jsonb_array_length(Datum jsonb_array)
|
|
{
|
|
Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_array);
|
|
JsonbIterator *it;
|
|
JsonbValue v;
|
|
JsonbIteratorToken token;
|
|
int count = 0;
|
|
|
|
if (!jb)
|
|
{
|
|
elog(DEBUG1, "Null JSONB array passed to get_jsonb_array_length");
|
|
return 0;
|
|
}
|
|
|
|
/* 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_length");
|
|
return 0;
|
|
}
|
|
|
|
/* Use PG_TRY/PG_CATCH to handle 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, "JSONB value is not an array (token=%d)", token);
|
|
return 0;
|
|
}
|
|
|
|
elog(DEBUG1, "Counting array elements");
|
|
|
|
/* Count array elements */
|
|
while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE)
|
|
{
|
|
if (token == WJB_ELEM)
|
|
{
|
|
count++;
|
|
elog(DEBUG1, "Found array element %d", count);
|
|
}
|
|
}
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
elog(WARNING, "Exception while counting array length");
|
|
FlushErrorState();
|
|
return 0;
|
|
}
|
|
PG_END_TRY();
|
|
|
|
elog(DEBUG1, "Final array length: %d", count);
|
|
return count;
|
|
}
|
|
|
|
static int
|
|
direct_array_length(Datum jsonb_array)
|
|
{
|
|
Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_array);
|
|
int count = 0;
|
|
|
|
if (!jb)
|
|
{
|
|
elog(DEBUG1, "Null JSONB array passed to direct_array_length");
|
|
return 0;
|
|
}
|
|
|
|
/* Use direct approach to count array elements */
|
|
PG_TRY();
|
|
{
|
|
/* Convert to text and count array elements */
|
|
text *json_text = DatumGetTextP(DirectFunctionCall1(jsonb_out, PointerGetDatum(jb)));
|
|
|
|
if (json_text)
|
|
{
|
|
char *json_str = text_to_cstring(json_text);
|
|
elog(DEBUG1, "JSON array string: %s", json_str);
|
|
|
|
/* Skip whitespace */
|
|
while (*json_str && isspace((unsigned char)*json_str))
|
|
json_str++;
|
|
|
|
/* Check if it's an array */
|
|
if (*json_str == '[')
|
|
{
|
|
/* Skip the opening bracket */
|
|
json_str++;
|
|
|
|
/* Skip whitespace */
|
|
while (*json_str && isspace((unsigned char)*json_str))
|
|
json_str++;
|
|
|
|
/* Empty array check */
|
|
if (*json_str == ']')
|
|
{
|
|
elog(DEBUG1, "Empty array, length 0");
|
|
pfree(text_to_cstring(json_text));
|
|
return 0;
|
|
}
|
|
|
|
/* Count commas to determine array length */
|
|
count = 1; /* Start with 1 for the first element */
|
|
int in_string = 0;
|
|
int in_object = 0;
|
|
int in_array = 0;
|
|
|
|
while (*json_str && *json_str != ']')
|
|
{
|
|
if (!in_string)
|
|
{
|
|
if (*json_str == '"')
|
|
{
|
|
in_string = 1;
|
|
}
|
|
else if (*json_str == '{')
|
|
{
|
|
in_object++;
|
|
}
|
|
else if (*json_str == '}')
|
|
{
|
|
in_object--;
|
|
}
|
|
else if (*json_str == '[')
|
|
{
|
|
in_array++;
|
|
}
|
|
else if (*json_str == ']')
|
|
{
|
|
in_array--;
|
|
}
|
|
else if (*json_str == ',' && in_object == 0 && in_array == 0)
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
else if (*json_str == '"' && *(json_str-1) != '\\')
|
|
{
|
|
in_string = 0;
|
|
}
|
|
|
|
json_str++;
|
|
}
|
|
|
|
elog(DEBUG1, "Direct array length: %d", count);
|
|
}
|
|
else
|
|
{
|
|
elog(DEBUG1, "Not an array, length 0");
|
|
}
|
|
|
|
pfree(text_to_cstring(json_text));
|
|
}
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
elog(WARNING, "Exception in direct_array_length");
|
|
ErrorData *edata = CopyErrorData();
|
|
elog(WARNING, "Error message: %s", edata->message);
|
|
FreeErrorData(edata);
|
|
FlushErrorState();
|
|
return 0;
|
|
}
|
|
PG_END_TRY();
|
|
|
|
return count;
|
|
}
|
|
|
|
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;
|
|
|
|
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 = PointerGetDatum(JsonbValueToJsonb(&v));
|
|
debug_jsonb_value(result, "Array Element (Binary)");
|
|
}
|
|
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
|
|
direct_array_element(Datum jsonb_array, int index)
|
|
{
|
|
Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_array);
|
|
Datum result = (Datum) 0;
|
|
|
|
if (!jb || index < 0)
|
|
{
|
|
elog(DEBUG1, "Invalid array or index: array=%p, index=%d", jb, index);
|
|
return result;
|
|
}
|
|
|
|
/* Use direct approach to extract array element */
|
|
PG_TRY();
|
|
{
|
|
/* Convert entire array to JSON string */
|
|
text *json_text = DatumGetTextP(DirectFunctionCall1(jsonb_out, PointerGetDatum(jb)));
|
|
|
|
if (json_text)
|
|
{
|
|
char *json_str = text_to_cstring(json_text);
|
|
elog(DEBUG1, "JSON array for element extraction: %s", json_str);
|
|
|
|
/* Skip whitespace */
|
|
while (*json_str && isspace((unsigned char)*json_str))
|
|
json_str++;
|
|
|
|
/* Check if it's an array */
|
|
if (*json_str == '[')
|
|
{
|
|
/* Extract elements from JSON array */
|
|
int current_index = 0;
|
|
int nesting = 0;
|
|
char *element_start = NULL;
|
|
char *element_end = NULL;
|
|
int in_string = 0;
|
|
int object_nesting = 0;
|
|
int array_nesting = 0;
|
|
|
|
/* Skip opening bracket */
|
|
json_str++;
|
|
|
|
/* Skip leading whitespace */
|
|
while (*json_str && isspace((unsigned char)*json_str))
|
|
json_str++;
|
|
|
|
/* Empty array check */
|
|
if (*json_str == ']')
|
|
{
|
|
elog(DEBUG1, "Empty array");
|
|
pfree(text_to_cstring(json_text));
|
|
return result;
|
|
}
|
|
|
|
element_start = json_str;
|
|
|
|
/* Find the element at the specified index */
|
|
while (*json_str)
|
|
{
|
|
if (!in_string)
|
|
{
|
|
if (*json_str == '"')
|
|
{
|
|
in_string = 1;
|
|
}
|
|
else if (*json_str == '{')
|
|
{
|
|
object_nesting++;
|
|
}
|
|
else if (*json_str == '}')
|
|
{
|
|
object_nesting--;
|
|
}
|
|
else if (*json_str == '[')
|
|
{
|
|
array_nesting++;
|
|
}
|
|
else if (*json_str == ']')
|
|
{
|
|
if (array_nesting == 0)
|
|
{
|
|
/* End of array */
|
|
if (current_index == index)
|
|
{
|
|
element_end = json_str;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
elog(DEBUG1, "Index %d out of bounds (max: %d)", index, current_index);
|
|
break;
|
|
}
|
|
}
|
|
array_nesting--;
|
|
}
|
|
else if (*json_str == ',' && object_nesting == 0 && array_nesting == 0)
|
|
{
|
|
if (current_index == index)
|
|
{
|
|
element_end = json_str;
|
|
break;
|
|
}
|
|
|
|
current_index++;
|
|
element_start = json_str + 1;
|
|
|
|
/* Skip whitespace after comma */
|
|
while (*(element_start) && isspace((unsigned char)*(element_start)))
|
|
element_start++;
|
|
}
|
|
}
|
|
else if (*json_str == '"' && *(json_str-1) != '\\')
|
|
{
|
|
in_string = 0;
|
|
}
|
|
|
|
json_str++;
|
|
}
|
|
|
|
/* Check if we found the element */
|
|
if (element_start && element_end && current_index == index)
|
|
{
|
|
/* Create a string with just the array element */
|
|
size_t element_len = element_end - element_start;
|
|
char *element_str = pnstrdup(element_start, element_len);
|
|
|
|
elog(DEBUG1, "Extracted element %d: %s", index, element_str);
|
|
|
|
/* If it's a string, we need to wrap it in JSON object */
|
|
if (*element_str == '"')
|
|
{
|
|
StringInfoData object_str;
|
|
initStringInfo(&object_str);
|
|
appendStringInfo(&object_str, "{\"value\": %s}", element_str);
|
|
pfree(element_str);
|
|
element_str = object_str.data;
|
|
elog(DEBUG1, "Wrapped element in object: %s", element_str);
|
|
}
|
|
/* If it's a number, boolean, or null, we need to wrap it */
|
|
else if (*element_str != '{' && *element_str != '[')
|
|
{
|
|
StringInfoData object_str;
|
|
initStringInfo(&object_str);
|
|
appendStringInfo(&object_str, "{\"value\": %s}", element_str);
|
|
pfree(element_str);
|
|
element_str = object_str.data;
|
|
elog(DEBUG1, "Wrapped scalar element in object: %s", element_str);
|
|
}
|
|
|
|
/* Parse the element string as JSONB */
|
|
text *element_text = cstring_to_text(element_str);
|
|
Datum jsonb_element = DirectFunctionCall1(jsonb_in, PointerGetDatum(element_text));
|
|
|
|
pfree(element_str);
|
|
pfree(element_text);
|
|
|
|
if (jsonb_element)
|
|
{
|
|
result = jsonb_element;
|
|
elog(DEBUG1, "Successfully extracted array element %d", index);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
elog(DEBUG1, "Element at index %d not found", index);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
elog(DEBUG1, "Not an array");
|
|
}
|
|
|
|
pfree(text_to_cstring(json_text));
|
|
}
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
elog(WARNING, "Exception in direct_array_element for index %d", index);
|
|
ErrorData *edata = CopyErrorData();
|
|
elog(WARNING, "Error message: %s", edata->message);
|
|
elog(WARNING, "Error detail: %s", edata->detail ? edata->detail : "none");
|
|
FreeErrorData(edata);
|
|
FlushErrorState();
|
|
}
|
|
PG_END_TRY();
|
|
|
|
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, "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, "Invalid parent JSONB container in create_iterator_context");
|
|
return parent_context;
|
|
}
|
|
|
|
if (!is_jsonb_container_valid(&item_jb->root))
|
|
{
|
|
elog(WARNING, "Invalid item JSONB container in create_iterator_context");
|
|
return parent_context;
|
|
}
|
|
|
|
elog(DEBUG1, "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, "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, "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, "Found scalar value with 'value' key (type=%d)", v.type);
|
|
pushJsonbValue(&parse_state, WJB_VALUE, &v);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
elog(DEBUG1, "Item context is not an object (token=%d)", token);
|
|
}
|
|
|
|
/* If not a scalar, use the whole item as is */
|
|
if (!is_scalar)
|
|
{
|
|
elog(DEBUG1, "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, "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));
|
|
}
|
|
}
|
|
|
|
/* Debug function to print JSONB structure */
|
|
static void
|
|
debug_jsonb_value(Datum jsonb_value, const char *label)
|
|
{
|
|
Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_value);
|
|
JsonbIterator *it;
|
|
JsonbValue v;
|
|
JsonbIteratorToken token;
|
|
int depth = 0;
|
|
|
|
if (!jb)
|
|
{
|
|
elog(DEBUG1, "%s: NULL JSONB value", label);
|
|
return;
|
|
}
|
|
|
|
elog(DEBUG1, "%s: Starting JSONB debug", label);
|
|
|
|
it = JsonbIteratorInit((JsonbContainer *)&jb->root);
|
|
|
|
while ((token = JsonbIteratorNext(&it, &v, false)) != WJB_DONE)
|
|
{
|
|
switch (token)
|
|
{
|
|
case WJB_BEGIN_ARRAY:
|
|
elog(DEBUG1, "%s: %*sBegin Array", label, depth * 2, "");
|
|
depth++;
|
|
break;
|
|
|
|
case WJB_END_ARRAY:
|
|
depth--;
|
|
elog(DEBUG1, "%s: %*sEnd Array", label, depth * 2, "");
|
|
break;
|
|
|
|
case WJB_BEGIN_OBJECT:
|
|
elog(DEBUG1, "%s: %*sBegin Object", label, depth * 2, "");
|
|
depth++;
|
|
break;
|
|
|
|
case WJB_END_OBJECT:
|
|
depth--;
|
|
elog(DEBUG1, "%s: %*sEnd Object", label, depth * 2, "");
|
|
break;
|
|
|
|
case WJB_KEY:
|
|
elog(DEBUG1, "%s: %*sKey: %.*s", label, depth * 2, "",
|
|
v.val.string.len, v.val.string.val);
|
|
break;
|
|
|
|
case WJB_VALUE:
|
|
switch (v.type)
|
|
{
|
|
case jbvString:
|
|
elog(DEBUG1, "%s: %*sString Value: %.*s", label, depth * 2, "",
|
|
v.val.string.len, v.val.string.val);
|
|
break;
|
|
|
|
case jbvNumeric:
|
|
{
|
|
char *num_str = DatumGetCString(DirectFunctionCall1(numeric_out,
|
|
NumericGetDatum(v.val.numeric)));
|
|
elog(DEBUG1, "%s: %*sNumeric Value: %s", label, depth * 2, "", num_str);
|
|
pfree(num_str);
|
|
}
|
|
break;
|
|
|
|
case jbvBool:
|
|
elog(DEBUG1, "%s: %*sBoolean Value: %s", label, depth * 2, "",
|
|
v.val.boolean ? "true" : "false");
|
|
break;
|
|
|
|
case jbvNull:
|
|
elog(DEBUG1, "%s: %*sNull Value", label, depth * 2, "");
|
|
break;
|
|
|
|
case jbvBinary:
|
|
elog(DEBUG1, "%s: %*sBinary Value", label, depth * 2, "");
|
|
break;
|
|
|
|
default:
|
|
elog(DEBUG1, "%s: %*sUnknown Value Type: %d", label, depth * 2, "", v.type);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case WJB_ELEM:
|
|
elog(DEBUG1, "%s: %*sArray Element", label, depth * 2, "");
|
|
break;
|
|
|
|
default:
|
|
elog(DEBUG1, "%s: %*sUnknown Token: %d", label, depth * 2, "", token);
|
|
break;
|
|
}
|
|
}
|
|
|
|
elog(DEBUG1, "%s: Finished JSONB debug", label);
|
|
}
|
|
|
|
/* PostgreSQL function wrappers */
|
|
PG_MODULE_MAGIC;
|
|
|
|
PG_FUNCTION_INFO_V1(pg_template_parse);
|
|
Datum
|
|
pg_template_parse(PG_FUNCTION_ARGS)
|
|
{
|
|
text *template_text = PG_GETARG_TEXT_PP(0);
|
|
char *template_str = text_to_cstring(template_text);
|
|
const char *template_ptr = template_str;
|
|
MemoryContext old_context;
|
|
MemoryContext parse_context;
|
|
TemplateConfig config;
|
|
TemplateNode *root = NULL;
|
|
TemplateErrorCode error_code = TEMPLATE_ERROR_NONE;
|
|
text *result_text = NULL;
|
|
StringInfoData result;
|
|
|
|
/* Create a memory context for parsing */
|
|
parse_context = AllocSetContextCreate(CurrentMemoryContext,
|
|
"Template Parse Context",
|
|
ALLOCSET_DEFAULT_SIZES);
|
|
|
|
/* Switch to the new context for parsing */
|
|
old_context = MemoryContextSwitchTo(parse_context);
|
|
|
|
/* Initialize default config */
|
|
config = template_default_config(parse_context);
|
|
|
|
PG_TRY();
|
|
{
|
|
/* Parse the template */
|
|
root = template_parse(parse_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))));
|
|
}
|
|
|
|
/* Convert the parsed template to a string representation for debugging */
|
|
initStringInfo(&result);
|
|
appendStringInfo(&result, "Template parsed successfully. Structure:\n");
|
|
template_node_to_string(root, &result, 0);
|
|
|
|
/* Switch back to the original memory context */
|
|
MemoryContextSwitchTo(old_context);
|
|
|
|
/* Return the result */
|
|
result_text = cstring_to_text(result.data);
|
|
pfree(result.data);
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
/* Switch back to the original memory context for error handling */
|
|
MemoryContextSwitchTo(old_context);
|
|
|
|
/* Clean up */
|
|
if (template_str)
|
|
pfree(template_str);
|
|
|
|
/* Delete the parse context */
|
|
MemoryContextDelete(parse_context);
|
|
|
|
PG_RE_THROW();
|
|
}
|
|
PG_END_TRY();
|
|
|
|
/* Clean up */
|
|
MemoryContextDelete(parse_context);
|
|
pfree(template_str);
|
|
|
|
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: %s", template_str);
|
|
elog(DEBUG1, "Context JSONB: %p", context_jsonb);
|
|
debug_jsonb_value(PointerGetDatum(context_jsonb), "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");
|
|
|
|
/* 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)
|
|
{
|
|
TemplateNode *current = node;
|
|
int i;
|
|
|
|
while (current)
|
|
{
|
|
/* Add indentation */
|
|
for (i = 0; i < indent; i++)
|
|
appendStringInfoChar(result, ' ');
|
|
|
|
/* Add node type */
|
|
switch (current->type)
|
|
{
|
|
case TEMPLATE_NODE_TEXT:
|
|
appendStringInfo(result, "TEXT: \"%s\"\n",
|
|
current->value->text.content ? current->value->text.content : "");
|
|
break;
|
|
|
|
case TEMPLATE_NODE_INTERPOLATE:
|
|
appendStringInfo(result, "INTERPOLATE: \"%s\"\n",
|
|
current->value->interpolate.key ? current->value->interpolate.key : "");
|
|
break;
|
|
|
|
case TEMPLATE_NODE_SECTION:
|
|
appendStringInfo(result, "SECTION: iterator=\"%s\", collection=\"%s\"\n",
|
|
current->value->section.iterator ? current->value->section.iterator : "",
|
|
current->value->section.collection ? current->value->section.collection : "");
|
|
|
|
if (current->value->section.body)
|
|
{
|
|
template_node_to_string(current->value->section.body, result, indent + 2);
|
|
}
|
|
break;
|
|
|
|
case TEMPLATE_NODE_EXECUTE:
|
|
appendStringInfo(result, "EXECUTE: \"%s\"\n",
|
|
current->value->execute.code ? current->value->execute.code : "");
|
|
break;
|
|
|
|
case TEMPLATE_NODE_INCLUDE:
|
|
appendStringInfo(result, "INCLUDE: \"%s\"\n",
|
|
current->value->include.key ? current->value->include.key : "");
|
|
break;
|
|
|
|
default:
|
|
appendStringInfo(result, "UNKNOWN NODE TYPE: %d\n", current->type);
|
|
break;
|
|
}
|
|
|
|
current = current->next;
|
|
}
|
|
}
|
|
|
|
/* Direct array check function to handle arrays without using JsonbIterator */
|
|
static Datum
|
|
direct_array_check(Datum jsonb_context, const char *path, bool *found)
|
|
{
|
|
Jsonb *jb = (Jsonb *) DatumGetPointer(jsonb_context);
|
|
JsonbValue *jbv_result;
|
|
JsonbValue key;
|
|
Datum result = (Datum) 0;
|
|
|
|
*found = false;
|
|
|
|
if (!jb || !path)
|
|
return result;
|
|
|
|
/* Use direct approach without iterators */
|
|
PG_TRY();
|
|
{
|
|
key.type = jbvString;
|
|
key.val.string.val = (char *) path;
|
|
key.val.string.len = strlen(path);
|
|
|
|
jbv_result = findJsonbValueFromContainer(&jb->root, JB_FOBJECT, &key);
|
|
|
|
if (jbv_result)
|
|
{
|
|
elog(DEBUG1, "Direct check: Found value for key %s (type=%d)", path, jbv_result->type);
|
|
|
|
/* Method 1: Convert to text and check for array start */
|
|
text *json_text = NULL;
|
|
Jsonb *value_jsonb = JsonbValueToJsonb(jbv_result);
|
|
|
|
if (value_jsonb)
|
|
{
|
|
/* Convert to text and check if it starts with '[' */
|
|
json_text = DatumGetTextP(DirectFunctionCall1(jsonb_out, PointerGetDatum(value_jsonb)));
|
|
|
|
if (json_text)
|
|
{
|
|
char *json_str = text_to_cstring(json_text);
|
|
elog(DEBUG1, "JSON string representation: %s", json_str);
|
|
|
|
/* Skip whitespace */
|
|
while (*json_str && isspace((unsigned char)*json_str))
|
|
json_str++;
|
|
|
|
if (*json_str == '[')
|
|
{
|
|
elog(DEBUG1, "Direct check: Value is an array");
|
|
*found = true;
|
|
result = PointerGetDatum(value_jsonb);
|
|
}
|
|
else
|
|
{
|
|
elog(DEBUG1, "Direct check: Value is not an array");
|
|
}
|
|
|
|
pfree(json_str);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
elog(DEBUG1, "Direct check: Path %s not found in JSONB", path);
|
|
}
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
elog(WARNING, "Exception in direct_array_check for path %s", path);
|
|
ErrorData *edata = CopyErrorData();
|
|
elog(WARNING, "Error message: %s", edata->message);
|
|
FreeErrorData(edata);
|
|
FlushErrorState();
|
|
}
|
|
PG_END_TRY();
|
|
|
|
return result;
|
|
}
|
|
|
|
/* 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;
|
|
JsonbValue key;
|
|
JsonbContainer *container;
|
|
|
|
*found = false;
|
|
|
|
if (!jb || !path || !is_jsonb_container_valid(&jb->root))
|
|
{
|
|
elog(DEBUG1, "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)
|
|
{
|
|
/* Check if we're still working with an object (?) */
|
|
if (!(container->header & JB_FOBJECT))
|
|
{
|
|
elog(DEBUG1, "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, "Key '%s' not found in object", token);
|
|
pfree(path_copy);
|
|
return NULL;
|
|
}
|
|
|
|
/* If there are more path segments, we need to continue with the next container */
|
|
token = strtok_r(NULL, ".", &saveptr);
|
|
|
|
if (token != NULL)
|
|
{
|
|
/* We need to go deeper, so the current result must be a container */
|
|
if (result->type != jbvBinary)
|
|
{
|
|
elog(DEBUG1, "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, "Invalid JSONB container during path traversal");
|
|
pfree(path_copy);
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* If we got here, we found the value */
|
|
*found = true;
|
|
pfree(path_copy);
|
|
return result;
|
|
}
|
|
|