feat: hel: render without execute
This commit is contained in:
12
flake.nix
12
flake.nix
@@ -437,8 +437,8 @@
|
||||
} {
|
||||
pname = "postgrect";
|
||||
version = "0.1";
|
||||
src = ./package/c/postgreact;
|
||||
nativeBuildInputs = with prev; [pkg-config];
|
||||
src = ./package/c/hel;
|
||||
nativeBuildInputs = (with prev; [pkg-config]) ++ [ self.packages.${prev.system}.c-hectic ];
|
||||
preInstall = ''mkdir $out'';
|
||||
};
|
||||
in {
|
||||
@@ -447,25 +447,25 @@
|
||||
http = buildHttpExt "17";
|
||||
pg_smtp_client = buildSmtpExt "17";
|
||||
plhaskell = buildPlHaskellExt "17";
|
||||
postgreact = buildHelloExt "17";
|
||||
hel = buildHelloExt "17";
|
||||
};};
|
||||
postgresql_16 = prev.postgresql_16 // {pkgs = prev.postgresql_16.pkgs // {
|
||||
http = buildHttpExt "16";
|
||||
pg_smtp_client = buildSmtpExt "16";
|
||||
plhaskell = buildPlHaskellExt "16";
|
||||
postgreact = buildHelloExt "16";
|
||||
hel = buildHelloExt "16";
|
||||
};};
|
||||
postgresql_15 = prev.postgresql_15 // {pkgs = prev.postgresql_15.pkgs // {
|
||||
http = buildHttpExt "15";
|
||||
pg_smtp_client = buildSmtpExt "15";
|
||||
plhaskell = buildPlHaskellExt "15";
|
||||
postgreact = buildHelloExt "15";
|
||||
hel = buildHelloExt "15";
|
||||
};};
|
||||
postgresql_14 = prev.postgresql_14 // {pkgs = prev.postgresql_14.pkgs // {
|
||||
http = buildHttpExt "14";
|
||||
pg_smtp_client = buildSmtpExt "14";
|
||||
plhaskell = buildPlHaskellExt "14";
|
||||
postgreact = buildHelloExt "14";
|
||||
hel = buildHelloExt "14";
|
||||
};};
|
||||
writers = let
|
||||
writeC =
|
||||
|
||||
@@ -603,11 +603,11 @@ struct Json {
|
||||
|
||||
RESULT(Json, Json);
|
||||
|
||||
#define json_parse(arena, s) json_parse__(__FILE__, __func__, __LINE__, arena, s)
|
||||
Json *json_parse__(const char* file, const char* func, int line, Arena *arena, const char **s);
|
||||
#define json_parse(arena, s) json_parse__(__FILE__, __func__, __LINE__, arena, s)
|
||||
|
||||
#define json_to_string(arena, item) json_to_string__(__FILE__, __func__, __LINE__, arena, item)
|
||||
char *json_to_string__(const char* file, const char* func, int line, Arena *arena, const Json * const item);
|
||||
#define json_to_string(arena, item) json_to_string__(__FILE__, __func__, __LINE__, arena, item)
|
||||
|
||||
#define json_to_string_with_opts(arena, item, raw) json_to_string_with_opts__(__FILE__, __func__, __LINE__, arena, item, raw)
|
||||
char *json_to_string_with_opts__(const char* file, const char* func, int line, Arena *arena, const Json * const item, JsonRawOpt raw);
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
" struct TemplateSectionValue section = {\n" \
|
||||
" iterator = %p \"item\",\n" \
|
||||
" collection = %p \"items\",\n" \
|
||||
" body = {\n" \
|
||||
" struct TemplateNode body = {\n" \
|
||||
" enum type = TEXT 0 ,\n" \
|
||||
" union TemplateValue value = {\n" \
|
||||
" struct TemplateTextValue text = {\n" \
|
||||
|
||||
13
package/c/hel/Makefile
Normal file
13
package/c/hel/Makefile
Normal file
@@ -0,0 +1,13 @@
|
||||
MODULE_big = hel
|
||||
OBJS = hel.o
|
||||
EXTENSION = hel
|
||||
DATA = hel--0.1.sql
|
||||
|
||||
PG_CONFIG = pg_config
|
||||
|
||||
PGXS := $(shell $(PG_CONFIG) --pgxs)
|
||||
|
||||
# Add hectic library path
|
||||
CFLAGS += -I -lhectic
|
||||
|
||||
include $(PGXS)
|
||||
26
package/c/hel/hel--0.1.sql
Executable file
26
package/c/hel/hel--0.1.sql
Executable file
@@ -0,0 +1,26 @@
|
||||
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
|
||||
\echo Use "CREATE EXTENSION hel" to load this file. \quit
|
||||
|
||||
CREATE SCHEMA hel;
|
||||
|
||||
-- Define the parse_text_with_hectic function that uses hectic library
|
||||
-- Expected usage:
|
||||
-- ```sql
|
||||
-- SELECT "hel"."render"(
|
||||
-- "declare" :=
|
||||
-- jsonb_build_object(
|
||||
-- 'name', 'test',
|
||||
-- 'config', jsonb_build_object(
|
||||
-- 'debug', true,
|
||||
-- 'limit', 100
|
||||
-- )
|
||||
-- ),
|
||||
-- "template" := $hel$
|
||||
-- {{ name }} {{ config.limit }}
|
||||
-- $hel$
|
||||
-- );
|
||||
-- ```
|
||||
CREATE FUNCTION "hel"."render"("declare" json, "template" text)
|
||||
RETURNS text
|
||||
AS 'hel', 'render'
|
||||
LANGUAGE C STRICT;
|
||||
307
package/c/hel/hel.c
Executable file
307
package/c/hel/hel.c
Executable file
@@ -0,0 +1,307 @@
|
||||
#include "postgres.h"
|
||||
#include "fmgr.h"
|
||||
#include "utils/builtins.h"
|
||||
#include "utils/json.h"
|
||||
#include "hectic.h"
|
||||
#include <string.h>
|
||||
|
||||
#ifdef PG_MODULE_MAGIC
|
||||
PG_MODULE_MAGIC;
|
||||
#endif
|
||||
|
||||
/* Helper function to get a JSON value by key path */
|
||||
static Json *json_get_by_path(Arena *arena, const Json *context, const char *key_path) {
|
||||
if (!context || !key_path || !*key_path) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *path_copy = arena_strdup(arena, key_path);
|
||||
char *token = strtok(path_copy, ".");
|
||||
Json *current = (Json*)context;
|
||||
|
||||
while (token && current) {
|
||||
current = json_get_object_item(current, token);
|
||||
token = strtok(NULL, ".");
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/* Convert JSON value to string */
|
||||
static char *json_value_to_string(Arena *arena, const Json *json) {
|
||||
if (!json) {
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (json->type) {
|
||||
case JSON_STRING:
|
||||
return json->value.string;
|
||||
case JSON_NUMBER: {
|
||||
char *buf = arena_alloc(arena, 64);
|
||||
snprintf(buf, 64, "%.6g", json->value.number);
|
||||
return buf;
|
||||
}
|
||||
case JSON_BOOL:
|
||||
return json->value.boolean ? "true" : "false";
|
||||
case JSON_NULL:
|
||||
return "";
|
||||
case JSON_ARRAY:
|
||||
case JSON_OBJECT:
|
||||
return json_to_string(arena, json);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/* Forward declaration for recursive function */
|
||||
static char *render_template_node(Arena *arena, const TemplateNode *node, const Json *context);
|
||||
|
||||
/* Render a text node */
|
||||
static char *render_text_node(Arena *arena, const TemplateNode *node) {
|
||||
if (!node || node->type != TEMPLATE_NODE_TEXT) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return node->value.text.content;
|
||||
}
|
||||
|
||||
/* Render an interpolation node */
|
||||
static char *render_interpolation_node(Arena *arena, const TemplateNode *node, const Json *context) {
|
||||
if (!node || node->type != TEMPLATE_NODE_INTERPOLATE || !context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const char *key = node->value.interpolate.key;
|
||||
Json *value = json_get_by_path(arena, context, key);
|
||||
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return json_value_to_string(arena, value);
|
||||
}
|
||||
|
||||
/* Render a section node (for loop) */
|
||||
static char *render_section_node(Arena *arena, const TemplateNode *node, const Json *context) {
|
||||
if (!node || node->type != TEMPLATE_NODE_SECTION || !context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const char *collection_key = node->value.section.collection;
|
||||
const char *iterator_name = node->value.section.iterator;
|
||||
TemplateNode *body = node->value.section.body;
|
||||
|
||||
Json *collection = json_get_by_path(arena, context, collection_key);
|
||||
|
||||
if (!collection || collection->type != JSON_ARRAY) {
|
||||
return "";
|
||||
}
|
||||
|
||||
size_t buffer_size = 1024;
|
||||
char *buffer = arena_alloc(arena, buffer_size);
|
||||
size_t buffer_pos = 0;
|
||||
|
||||
Json *item = collection->value.child;
|
||||
while (item) {
|
||||
const char *empty_json = "{}";
|
||||
Json *iter_context = json_parse(arena, &empty_json);
|
||||
if (!iter_context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Json *item_json = arena_alloc(arena, sizeof(Json));
|
||||
memcpy(item_json, item, sizeof(Json));
|
||||
item_json->key = arena_strdup(arena, iterator_name);
|
||||
item_json->next = NULL;
|
||||
|
||||
char *rendered_body = render_template_node(arena, body, iter_context);
|
||||
|
||||
size_t rendered_len = strlen(rendered_body);
|
||||
if (buffer_pos + rendered_len + 1 > buffer_size) {
|
||||
buffer_size = (buffer_pos + rendered_len + 1) * 2;
|
||||
buffer = arena_realloc(arena, buffer, buffer_size / 2, buffer_size);
|
||||
}
|
||||
|
||||
strcpy(buffer + buffer_pos, rendered_body);
|
||||
buffer_pos += rendered_len;
|
||||
|
||||
item = item->next;
|
||||
}
|
||||
|
||||
buffer[buffer_pos] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/* Render an include node */
|
||||
static char *render_include_node(Arena *arena, const TemplateNode *node, const Json *context) {
|
||||
if (!node || node->type != TEMPLATE_NODE_INCLUDE || !context) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const char *include_key = node->value.include.key;
|
||||
Json *include_value = json_get_by_path(arena, context, include_key);
|
||||
|
||||
if (!include_value || include_value->type != JSON_ARRAY) {
|
||||
return "";
|
||||
}
|
||||
|
||||
char *buffer = arena_alloc(arena, 1024);
|
||||
size_t buffer_pos = 0;
|
||||
|
||||
Json *include_item = include_value->value.child;
|
||||
while (include_item) {
|
||||
if (include_item->type == JSON_OBJECT) {
|
||||
Json *template_json = json_get_object_item(include_item, "template");
|
||||
Json *content_json = json_get_object_item(include_item, "content");
|
||||
Json *context_json = json_get_object_item(include_item, "context");
|
||||
|
||||
if (template_json && template_json->type == JSON_STRING) {
|
||||
const char *template_str = template_json->value.string;
|
||||
const Json *include_context = context_json ? context_json : context;
|
||||
|
||||
TemplateConfig config = template_default_config();
|
||||
TemplateResult template_result = template_parse(arena, &template_str, &config);
|
||||
|
||||
if (!IS_RESULT_ERROR(template_result)) {
|
||||
TemplateNode template_node = RESULT_SOME_VALUE(template_result);
|
||||
|
||||
char *rendered = render_template_node(arena, &template_node, include_context);
|
||||
|
||||
buffer_pos += sprintf(buffer + buffer_pos, "%s", rendered);
|
||||
}
|
||||
} else if (content_json && content_json->type == JSON_STRING) {
|
||||
buffer_pos += sprintf(buffer + buffer_pos, "%s", content_json->value.string);
|
||||
}
|
||||
}
|
||||
|
||||
include_item = include_item->next;
|
||||
}
|
||||
|
||||
buffer[buffer_pos] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/* Render a template node tree recursively */
|
||||
static char *render_template_node(Arena *arena, const TemplateNode *node, const Json *context) {
|
||||
if (!node) {
|
||||
return "";
|
||||
}
|
||||
|
||||
size_t buffer_size = 4096;
|
||||
char *output = arena_alloc(arena, buffer_size);
|
||||
size_t output_pos = 0;
|
||||
|
||||
const TemplateNode *current = node;
|
||||
while (current) {
|
||||
char *rendered = NULL;
|
||||
|
||||
switch (current->type) {
|
||||
case TEMPLATE_NODE_TEXT:
|
||||
rendered = render_text_node(arena, current);
|
||||
break;
|
||||
|
||||
case TEMPLATE_NODE_INTERPOLATE:
|
||||
rendered = render_interpolation_node(arena, current, context);
|
||||
break;
|
||||
|
||||
case TEMPLATE_NODE_SECTION:
|
||||
rendered = render_section_node(arena, current, context);
|
||||
break;
|
||||
|
||||
case TEMPLATE_NODE_INCLUDE:
|
||||
rendered = render_include_node(arena, current, context);
|
||||
break;
|
||||
|
||||
case TEMPLATE_NODE_EXECUTE:
|
||||
todo;
|
||||
rendered = "";
|
||||
break;
|
||||
|
||||
default:
|
||||
rendered = "";
|
||||
break;
|
||||
}
|
||||
|
||||
size_t rendered_len = strlen(rendered);
|
||||
if (output_pos + rendered_len + 1 > buffer_size) {
|
||||
buffer_size = (output_pos + rendered_len + 1) * 2;
|
||||
output = arena_realloc(arena, output, buffer_size / 2, buffer_size);
|
||||
}
|
||||
|
||||
strcpy(output + output_pos, rendered);
|
||||
output_pos += rendered_len;
|
||||
|
||||
if (current->children) {
|
||||
char *children_rendered = render_template_node(arena, current->children, context);
|
||||
size_t children_len = strlen(children_rendered);
|
||||
|
||||
if (output_pos + children_len + 1 > buffer_size) {
|
||||
buffer_size = (output_pos + children_len + 1) * 2;
|
||||
output = arena_realloc(arena, output, buffer_size / 2, buffer_size);
|
||||
}
|
||||
|
||||
strcpy(output + output_pos, children_rendered);
|
||||
output_pos += children_len;
|
||||
}
|
||||
|
||||
current = current->next;
|
||||
}
|
||||
|
||||
output[output_pos] = '\0';
|
||||
return output;
|
||||
}
|
||||
|
||||
/* Define the function render */
|
||||
PG_FUNCTION_INFO_V1(render);
|
||||
|
||||
/*
|
||||
* Function to render templates using hectic library with JSON context
|
||||
* Arguments:
|
||||
* 1. declare - JSON context for rendering
|
||||
* 2. template - The template text to render
|
||||
*/
|
||||
Datum render(PG_FUNCTION_ARGS)
|
||||
{
|
||||
text *context_text = PG_GETARG_TEXT_PP(0);
|
||||
text *template_text = PG_GETARG_TEXT_PP(1);
|
||||
|
||||
/* Convert input text to C string */
|
||||
char *template_str = text_to_cstring(template_text);
|
||||
char *context_str = text_to_cstring(context_text);
|
||||
|
||||
/* Initialize arena for memory management */
|
||||
Arena arena = arena_init(MEM_MiB);
|
||||
|
||||
/* Parse the JSON context */
|
||||
const char *json_ptr = context_str;
|
||||
Json *context = json_parse(&arena, &json_ptr);
|
||||
|
||||
if (!context) {
|
||||
arena_free(&arena);
|
||||
ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("Invalid JSON context")));
|
||||
}
|
||||
|
||||
/* Parse the template text */
|
||||
const char *template_ptr = template_str;
|
||||
TemplateConfig config = template_default_config();
|
||||
TemplateResult template_result = template_parse(&arena, &template_ptr, &config);
|
||||
|
||||
if (IS_RESULT_ERROR(template_result)) {
|
||||
arena_free(&arena);
|
||||
ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("Failed to parse template: %s",
|
||||
RESULT_ERROR_MESSAGE(template_result))));
|
||||
}
|
||||
|
||||
/* Render the template */
|
||||
TemplateNode root_node = RESULT_SOME_VALUE(template_result);
|
||||
char *result_str = render_template_node(&arena, &root_node, context);
|
||||
|
||||
/* Prepare return value */
|
||||
text *result = cstring_to_text(result_str);
|
||||
|
||||
arena_free(&arena);
|
||||
|
||||
PG_RETURN_TEXT_P(result);
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
comment = 'My first extension'
|
||||
default_version = '0.1'
|
||||
module_pathname = '$libdir/postgreact'
|
||||
module_pathname = '$libdir/hel'
|
||||
@@ -87,11 +87,11 @@ case "$MODE" in
|
||||
mkdir -p target
|
||||
echo "# Building PostgreSQL extension"
|
||||
# shellcheck disable=SC2086
|
||||
gcc $CFLAGS $OPTFLAGS -I$PG_INCLUDE -shared -o target/postgreact.so postgreact.c
|
||||
gcc $CFLAGS $OPTFLAGS -I$PG_INCLUDE -shared -o target/hel.so hel.c
|
||||
|
||||
# Copy extension files to target directory
|
||||
cp postgreact.control target/
|
||||
cp postgreact--0.1.sql target/
|
||||
cp hel.control target/
|
||||
cp hel--0.1.sql target/
|
||||
|
||||
echo "Build complete. Files available in target/ directory."
|
||||
;;
|
||||
0
package/c/hel/test/01-test.c
Executable file
0
package/c/hel/test/01-test.c
Executable file
@@ -1,10 +0,0 @@
|
||||
MODULE_big = postgreact
|
||||
OBJS = postgreact.o
|
||||
EXTENSION = postgreact
|
||||
|
||||
DATA = $(wildcard *.sql)
|
||||
|
||||
PG_CONFIG = pg_config
|
||||
|
||||
PGXS := $(shell $(PG_CONFIG) --pgxs)
|
||||
include $(PGXS)
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
postgresql,
|
||||
...
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
pname = "postgreact";
|
||||
version = "0.1";
|
||||
|
||||
src = ./.;
|
||||
|
||||
buildInputs = [
|
||||
postgresql
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
mkdir -p target
|
||||
sh ./make.sh build
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/lib/postgresql $out/share/postgresql/extension
|
||||
|
||||
# Install compiled library
|
||||
install -m 755 -D target/postgreact.so $out/lib/postgresql/postgreact.so
|
||||
|
||||
# Install control and SQL files
|
||||
install -m 644 -D postgreact.control $out/share/postgresql/extension/postgreact.control
|
||||
install -m 644 -D postgreact--0.1.sql $out/share/postgresql/extension/postgreact--0.1.sql
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "PostgreSQL extension for reactive functions";
|
||||
homepage = "https://github.com/yukkop/util.nix";
|
||||
license = licenses.mit;
|
||||
platforms = postgresql.meta.platforms;
|
||||
maintainers = with maintainers; [ ];
|
||||
};
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
|
||||
\echo Use "CREATE EXTENSION postgreact" to load this file. \quit
|
||||
|
||||
-- Define the hello function that uses our C implementation
|
||||
CREATE FUNCTION hello()
|
||||
RETURNS text
|
||||
AS 'postgreact', 'hello'
|
||||
LANGUAGE C STRICT;
|
||||
@@ -1,16 +0,0 @@
|
||||
#include "postgres.h"
|
||||
#include "fmgr.h"
|
||||
#include "utils/builtins.h" /* for text_to_cstring and cstring_to_text */
|
||||
|
||||
#ifdef PG_MODULE_MAGIC
|
||||
PG_MODULE_MAGIC;
|
||||
#endif
|
||||
|
||||
/* Define the function hello */
|
||||
PG_FUNCTION_INFO_V1(hello);
|
||||
|
||||
/* Implement the function */
|
||||
Datum hello(PG_FUNCTION_ARGS)
|
||||
{
|
||||
PG_RETURN_TEXT_P(cstring_to_text("Hello, world!"));
|
||||
}
|
||||
Reference in New Issue
Block a user