diff --git a/.gitignore b/.gitignore index d0ccc93..ade5ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,6 @@ regression.out .depend docs/html docs/latex +docs/xml pljs--*.sql compile_commands.json diff --git a/META.json b/META.json index 37ccdd1..fa2660a 100644 --- a/META.json +++ b/META.json @@ -2,7 +2,7 @@ "name": "pljs", "abstract": "PLJS trusted procedural language", "description": "PLJS is a QuickJS-based Javascript Language Extension for \"modern\" PostgreSQL. It is compact, lightweight, and decently fast.", - "version": "1.0.3", + "version": "1.0.4", "maintainer": ["Jerry Sievert "], "license": { "PostgreSQL": "http://www.postgresql.org/about/licence" @@ -17,9 +17,9 @@ }, "provides": { "pljs": { - "file": "pljs--1.0.3.sql", + "file": "pljs--1.0.4.sql", "docfile": "README.md", - "version": "1.0.3", + "version": "1.0.4", "abstract": "PLJS trusted procedural language" } }, diff --git a/Makefile b/Makefile index 10c198b..3be5661 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: lintcheck format cleansql docs clean test all -PLJS_VERSION = 1.0.3 +PLJS_VERSION = 1.0.4 PG_CONFIG ?= pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) @@ -21,9 +21,6 @@ SHLIB_LINK = -Ldeps/quickjs -lquickjs ifeq ($(DEBUG), 1) PG_CFLAGS += -g SHLIB_LINK += -g -else -PG_CFLAGS += -O3 -SHLIB_LINK += -O3 endif ifeq ($(DEBUG_MEMORY), 1) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index db64e04..3cf5b3b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -28,3 +28,17 @@ Released _August 20, 2025_. Released _August 20, 2025_. - PGXN release went out with an incorrect control file + +### 1.0.4 + +Released _January 11, 2026_. + +- Up memory default limit to 512MB +- Remove unnecessary include from modules.c +- Remove extra running of GC after each execution +- Better handling of SRF's, including freeing resources +- Increased test timeouts to accommodate slower CI machines +- Alter memory limit minimum to 64MB to facilitate testing +- Alter some tests for s390x testing +- Clean up parameter orders for function calls +- Fixed potential memory leak in `pljs_jsvalue_to_datums` diff --git a/expected/array_spread.out b/expected/array_spread.out index ff38959..e71ed70 100644 --- a/expected/array_spread.out +++ b/expected/array_spread.out @@ -1,14 +1,6 @@ --- ten seconds should be enough to show this doesn't destroy memory -set statement_timeout = '5s'; -set pljs.memory_limit = '256'; -do $$ Object.prototype [Symbol.iterator] = function() { return { next:() => this } }; -[...({})]; -$$ language pljs; -ERROR: execution error -DETAIL: InternalError: out of memory - at (:2:1) - at (:3:3) - +-- we set the timeout to as high as we can, since some platforms may have slower CI machines +set pljs.statement_timeout = '65536s'; +set pljs.memory_limit = '64'; do $$ Object.prototype [Symbol.iterator] = function() { return { next:() => this } }; [...({})]; $$ language pljs; diff --git a/expected/bytea.out b/expected/bytea.out index a11003a..217eacc 100644 --- a/expected/bytea.out +++ b/expected/bytea.out @@ -64,10 +64,10 @@ AS $$ return arr; $$; -SELECT filled_int16array_bytea() = '\x0100020003000400'::bytea; - ?column? ----------- - t +SELECT filled_int16array_bytea(); + filled_int16array_bytea +------------------------- + \x0100020003000400 (1 row) CREATE FUNCTION filled_int32array_bytea() RETURNS bytea @@ -81,10 +81,10 @@ AS $$ return arr; $$; -SELECT filled_int32array_bytea() = '\x01000000020000000300000004000000'::bytea; - ?column? ----------- - t +SELECT filled_int32array_bytea(); + filled_int32array_bytea +------------------------------------ + \x01000000020000000300000004000000 (1 row) DO $$ diff --git a/expected/bytea_1.out b/expected/bytea_1.out new file mode 100644 index 0000000..768cc82 --- /dev/null +++ b/expected/bytea_1.out @@ -0,0 +1,101 @@ +CREATE FUNCTION valid_arraybuffer_bytea(len integer) RETURNS bytea +LANGUAGE pljs IMMUTABLE STRICT +AS $$ + arr = new ArrayBuffer(len); + for(i = 0; i < len; i++) { + arr[i] = i; + } + + return arr; +$$; +SELECT length(valid_arraybuffer_bytea(20)); + length +-------- + 20 +(1 row) + +CREATE FUNCTION valid_int8array_bytea(len integer) RETURNS bytea +LANGUAGE pljs IMMUTABLE STRICT +AS $$ + return new Int8Array(len); +$$; +SELECT length(valid_int8array_bytea(20)); + length +-------- + 20 +(1 row) + +CREATE FUNCTION valid_int16array_bytea(len integer) RETURNS bytea +LANGUAGE pljs IMMUTABLE STRICT +AS $$ + return new Int16Array(len); +$$; +SELECT length(valid_int16array_bytea(20)); + length +-------- + 40 +(1 row) + +CREATE FUNCTION filled_int8array_bytea() RETURNS bytea +LANGUAGE pljs IMMUTABLE STRICT +AS $$ + arr = new Int8Array(4); + arr[0] = 1; + arr[1] = 2; + arr[2] = 3; + arr[3] = 4; + + return arr; +$$; +SELECT filled_int8array_bytea() = '\x01020304'::bytea; + ?column? +---------- + t +(1 row) + +CREATE FUNCTION filled_int16array_bytea() RETURNS bytea +LANGUAGE pljs IMMUTABLE STRICT +AS $$ + arr = new Int16Array(4); + arr[0] = 1; + arr[1] = 2; + arr[2] = 3; + arr[3] = 4; + + return arr; +$$; +SELECT filled_int16array_bytea(); + filled_int16array_bytea +------------------------- + \x0001000200030004 +(1 row) + +CREATE FUNCTION filled_int32array_bytea() RETURNS bytea +LANGUAGE pljs IMMUTABLE STRICT +AS $$ + arr = new Int32Array(4); + arr[0] = 1; + arr[1] = 2; + arr[2] = 3; + arr[3] = 4; + + return arr; +$$; +SELECT filled_int32array_bytea(); + filled_int32array_bytea +------------------------------------ + \x00000001000000020000000300000004 +(1 row) + +DO $$ + const test = 'string test'; + const res = pljs.execute(`select $1::bytea`, [test]); + const result = res[0].bytea; + + if (result === test) { + pljs.elog(INFO, 'OK'); + } else { + pljs.elog(WARNING, 'FAIL'); + } +$$ language pljs; +INFO: OK diff --git a/expected/memory_limits.out b/expected/memory_limits.out index 0776fc6..9c18662 100644 --- a/expected/memory_limits.out +++ b/expected/memory_limits.out @@ -1,3 +1,4 @@ +set pljs.memory_limit = 64; do $$ const limit = pljs.execute(`select setting from pg_settings where name = $1`, ['pljs.memory_limit'])[0].setting; const a = new ArrayBuffer(limit*1024*1024/2); diff --git a/pljs.control b/pljs.control index 787dd07..1a25a5a 100644 --- a/pljs.control +++ b/pljs.control @@ -1,6 +1,6 @@ comment = 'PL/JS trusted procedural language' -default_version = '1.0.3' +default_version = '1.0.4' module_pathname = '$libdir/pljs' relocatable = false schema = pg_catalog diff --git a/sql/array_spread.sql b/sql/array_spread.sql index 930f268..2f47434 100644 --- a/sql/array_spread.sql +++ b/sql/array_spread.sql @@ -1,11 +1,8 @@ --- ten seconds should be enough to show this doesn't destroy memory -set statement_timeout = '5s'; -set pljs.memory_limit = '256'; +-- we set the timeout to as high as we can, since some platforms may have slower CI machines +set pljs.statement_timeout = '65536s'; +set pljs.memory_limit = '64'; do $$ Object.prototype [Symbol.iterator] = function() { return { next:() => this } }; [...({})]; $$ language pljs; -do $$ Object.prototype [Symbol.iterator] = function() { return { next:() => this } }; -[...({})]; -$$ language pljs; \ No newline at end of file diff --git a/sql/bytea.sql b/sql/bytea.sql index feded42..a5dee0a 100644 --- a/sql/bytea.sql +++ b/sql/bytea.sql @@ -53,7 +53,7 @@ AS $$ return arr; $$; -SELECT filled_int16array_bytea() = '\x0100020003000400'::bytea; +SELECT filled_int16array_bytea(); CREATE FUNCTION filled_int32array_bytea() RETURNS bytea LANGUAGE pljs IMMUTABLE STRICT @@ -67,7 +67,7 @@ AS $$ return arr; $$; -SELECT filled_int32array_bytea() = '\x01000000020000000300000004000000'::bytea; +SELECT filled_int32array_bytea(); DO $$ const test = 'string test'; diff --git a/sql/memory_limits.sql b/sql/memory_limits.sql index da0e8f6..c873860 100644 --- a/sql/memory_limits.sql +++ b/sql/memory_limits.sql @@ -1,3 +1,5 @@ +set pljs.memory_limit = 64; + do $$ const limit = pljs.execute(`select setting from pg_settings where name = $1`, ['pljs.memory_limit'])[0].setting; const a = new ArrayBuffer(limit*1024*1024/2); diff --git a/src/functions.c b/src/functions.c index 09ff9c5..2475d53 100644 --- a/src/functions.c +++ b/src/functions.c @@ -362,8 +362,8 @@ static int pljs_execute_params(const char *sql, JSValue params, JSValue param = JS_GetPropertyUint32(ctx, params, i); bool is_null; - values[i] = pljs_jsvalue_to_datum(param, parstate.param_types[i], ctx, NULL, - &is_null); + values[i] = pljs_jsvalue_to_datum(parstate.param_types[i], param, &is_null, + ctx, NULL); JS_FreeValue(ctx, param); } @@ -441,8 +441,8 @@ static JSValue pljs_plan_execute(JSContext *ctx, JSValueConst this_val, bool is_null; values[i] = pljs_jsvalue_to_datum( - param, plan->parstate ? plan->parstate->param_types[i] : 0, ctx, NULL, - &is_null); + plan->parstate ? plan->parstate->param_types[i] : 0, param, &is_null, + ctx, NULL); JS_FreeValue(ctx, param); } @@ -739,8 +739,8 @@ static JSValue pljs_plan_cursor(JSContext *ctx, JSValueConst this_val, int argc, bool is_null; values[i] = pljs_jsvalue_to_datum( - param, plan->parstate ? plan->parstate->param_types[i] : 0, ctx, NULL, - &is_null); + plan->parstate ? plan->parstate->param_types[i] : 0, param, &is_null, + ctx, NULL); } PG_TRY(); @@ -1069,14 +1069,20 @@ static JSValue pljs_return_next(JSContext *ctx, JSValueConst this_val, int argc, return js_throw("field name / property name mismatch", ctx); } - bool is_null = false; - pljs_jsvalue_to_record(argv[0], NULL, ctx, &is_null, retstate->tuple_desc, - retstate->tuple_store_state); + bool *nulls = (bool *)palloc0(sizeof(bool) * retstate->tuple_desc->natts); + Datum *values = pljs_jsvalue_to_datums(NULL, argv[0], &nulls, + retstate->tuple_desc, ctx); + + tuplestore_putvalues(retstate->tuple_store_state, retstate->tuple_desc, + values, nulls); + + pfree(nulls); + pfree(values); } else { bool is_null = false; - Datum result = pljs_jsvalue_to_datum( - argv[0], TupleDescAttr(retstate->tuple_desc, 0)->atttypid, ctx, NULL, - &is_null); + Datum result = + pljs_jsvalue_to_datum(TupleDescAttr(retstate->tuple_desc, 0)->atttypid, + argv[0], &is_null, ctx, NULL); tuplestore_putvalues(retstate->tuple_store_state, retstate->tuple_desc, &result, &is_null); } @@ -1372,8 +1378,8 @@ static JSValue pljs_window_get_func_arg_in_partition(JSContext *ctx, return JS_UNDEFINED; } - return pljs_datum_to_jsvalue(res, storage->function->argtypes[argno], ctx, - false); + return pljs_datum_to_jsvalue(storage->function->argtypes[argno], res, isnull, + true, ctx); } static JSValue pljs_window_get_func_arg_in_frame(JSContext *ctx, @@ -1418,8 +1424,8 @@ static JSValue pljs_window_get_func_arg_in_frame(JSContext *ctx, if (isout) { return JS_UNDEFINED; } - return pljs_datum_to_jsvalue(res, storage->function->argtypes[argno], ctx, - false); + return pljs_datum_to_jsvalue(storage->function->argtypes[argno], res, isnull, + true, ctx); } static JSValue pljs_window_get_func_arg_current(JSContext *ctx, @@ -1450,8 +1456,8 @@ static JSValue pljs_window_get_func_arg_current(JSContext *ctx, } PG_END_TRY(); - return pljs_datum_to_jsvalue(res, storage->function->argtypes[argno], ctx, - false); + return pljs_datum_to_jsvalue(storage->function->argtypes[argno], res, isnull, + true, ctx); } static JSValue pljs_window_object_to_string(JSContext *ctx, diff --git a/src/pljs.c b/src/pljs.c index 1526d7d..7fc9413 100644 --- a/src/pljs.c +++ b/src/pljs.c @@ -104,8 +104,8 @@ void pljs_guc_init(void) { DefineCustomIntVariable("pljs.memory_limit", gettext_noop("Runtime limit in MBytes"), - gettext_noop("The default value is 256 MB"), - (int *)&configuration.memory_limit, 256, 256, 3096, + gettext_noop("The default value is 512 MB"), + (int *)&configuration.memory_limit, 512, 64, 3096, PGC_SUSET, 0, NULL, NULL, NULL); DefineCustomStringVariable( @@ -427,13 +427,11 @@ static JSValueConst *convert_arguments_to_javascript(FunctionCallInfo fcinfo, if (WindowObjectIsValid(window_obj)) { for (int i = 0; i < nargs; i++) { - bool isnull; - Datum arg = WinGetFuncArgCurrent(window_obj, i, &isnull); - if (isnull) { - argv[i] = JS_NULL; - } else { - argv[i] = pljs_datum_to_jsvalue(arg, argtypes[i], context->ctx, true); - } + bool is_null; + Datum arg = WinGetFuncArgCurrent(window_obj, i, &is_null); + // Window functions: expand_composite=false (skip composite expansion) + argv[i] = + pljs_datum_to_jsvalue(argtypes[i], arg, is_null, false, context->ctx); } } else { for (int i = 0; i < nargs; i++) { @@ -453,12 +451,10 @@ static JSValueConst *convert_arguments_to_javascript(FunctionCallInfo fcinfo, if (fcinfo && IsPolymorphicType(argtype)) { argtype = get_fn_expr_argtype(fcinfo->flinfo, i); } - if (fcinfo->args[inargs].isnull == 1) { - argv[inargs] = JS_NULL; - } else { - argv[inargs] = pljs_datum_to_jsvalue(fcinfo->args[inargs].value, - argtype, context->ctx, false); - } + bool is_null = (fcinfo->args[inargs].isnull == 1); + // Regular functions: expand_composite=true (expand composite types) + argv[inargs] = pljs_datum_to_jsvalue(argtype, fcinfo->args[inargs].value, + is_null, true, context->ctx); inargs++; } @@ -986,8 +982,7 @@ static Datum call_trigger(FunctionCallInfo fcinfo, pljs_context *context) { pljs_type type; pljs_type_fill(&type, context->function->rettype); - Datum d = - pljs_jsvalue_to_record(ret, &type, context->ctx, NULL, tupdesc, NULL); + Datum d = pljs_jsvalue_to_record(&type, ret, NULL, tupdesc, context->ctx); HeapTupleHeader header = DatumGetHeapTupleHeader(d); @@ -1043,7 +1038,7 @@ static Datum call_function(FunctionCallInfo fcinfo, pljs_context *context, JSValue ret = JS_Call(context->ctx, context->js_function, JS_UNDEFINED, context->function->inargs, argv); - JS_RunGC(rt); + SPI_finish(); if (JS_IsException(ret)) { @@ -1056,7 +1051,7 @@ static Datum call_function(FunctionCallInfo fcinfo, pljs_context *context, /* Shuts up the compiler, since ereports of ERROR stop execution. */ return (Datum)0; } else { - Datum datum; + Datum datum = 0; if (rettype == RECORDOID) { TupleDesc tupdesc; @@ -1065,12 +1060,11 @@ static Datum call_function(FunctionCallInfo fcinfo, pljs_context *context, pljs_type type; pljs_type_fill(&type, rettype); - datum = - pljs_jsvalue_to_record(ret, &type, context->ctx, NULL, tupdesc, NULL); + datum = pljs_jsvalue_to_record(&type, ret, NULL, tupdesc, context->ctx); } else { bool is_null; datum = - pljs_jsvalue_to_datum(ret, rettype, context->ctx, fcinfo, &is_null); + pljs_jsvalue_to_datum(rettype, ret, &is_null, context->ctx, fcinfo); } JS_FreeValue(context->ctx, ret); @@ -1183,14 +1177,21 @@ static Datum call_srf_function(FunctionCallInfo fcinfo, pljs_context *context, return (Datum)0; } else { // Check to see if we have any values to append - if (!JS_IsUndefined(ret) || !JS_IsNull(ret)) { + if (!JS_IsUndefined(ret) && !JS_IsNull(ret)) { MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory); - bool is_null; + bool is_null = false; if (state->is_composite) { - pljs_jsvalue_to_record(ret, NULL, context->ctx, &is_null, - state->tuple_desc, state->tuple_store_state); + bool *nulls = (bool *)palloc0(sizeof(bool) * state->tuple_desc->natts); + + Datum *values = pljs_jsvalue_to_datums(NULL, argv[0], &nulls, + state->tuple_desc, context->ctx); + tuplestore_putvalues(state->tuple_store_state, state->tuple_desc, + values, nulls); + + pfree(nulls); + pfree(values); } else { if (JS_IsArray(context->ctx, ret)) { for (uint32_t i = 0; i < pljs_js_array_length(ret, context->ctx); @@ -1198,16 +1199,16 @@ static Datum call_srf_function(FunctionCallInfo fcinfo, pljs_context *context, JSValue val = JS_GetPropertyUint32(context->ctx, ret, i); Datum result = pljs_jsvalue_to_datum( - val, TupleDescAttr(state->tuple_desc, 0)->atttypid, - context->ctx, NULL, &is_null); + TupleDescAttr(state->tuple_desc, 0)->atttypid, val, &is_null, + context->ctx, NULL); tuplestore_putvalues(state->tuple_store_state, state->tuple_desc, &result, &is_null); } } else { if (!JS_IsUndefined(ret)) { Datum result = pljs_jsvalue_to_datum( - ret, TupleDescAttr(state->tuple_desc, 0)->atttypid, - context->ctx, NULL, &is_null); + TupleDescAttr(state->tuple_desc, 0)->atttypid, ret, &is_null, + context->ctx, NULL); tuplestore_putvalues(state->tuple_store_state, state->tuple_desc, &result, &is_null); diff --git a/src/pljs.h b/src/pljs.h index 477a7f8..e6a8f28 100644 --- a/src/pljs.h +++ b/src/pljs.h @@ -181,20 +181,22 @@ void pljs_cache_reset(void); // type.c // To Javascript -JSValue pljs_datum_to_jsvalue(Datum arg, Oid type, JSContext *ctx, - bool skip_composite); -JSValue pljs_datum_to_array(Datum arg, pljs_type *type, JSContext *ctx); -JSValue pljs_datum_to_object(Datum arg, pljs_type *type, JSContext *ctx); +JSValue pljs_datum_to_jsvalue(Oid argtype, Datum arg, bool is_null, + bool expand_composite, JSContext *ctx); +JSValue pljs_datum_to_array(pljs_type *type, Datum arg, JSContext *ctx); +JSValue pljs_datum_to_object(pljs_type *type, Datum arg, JSContext *ctx); JSValue pljs_tuple_to_jsvalue(TupleDesc, HeapTuple, JSContext *ctx); JSValue pljs_spi_result_to_jsvalue(int, JSContext *); -// To Datum -Datum pljs_jsvalue_to_array(JSValue, pljs_type *, JSContext *, +// To Postgres +Datum pljs_jsvalue_to_array(pljs_type *, JSValue, JSContext *, FunctionCallInfo); -Datum pljs_jsvalue_to_datum(JSValue, Oid, JSContext *, FunctionCallInfo, - bool *); -Datum pljs_jsvalue_to_record(JSValue val, pljs_type *type, JSContext *ctx, - bool *is_null, TupleDesc, Tuplestorestate *); +Datum pljs_jsvalue_to_datum(Oid rettype, JSValue val, bool *is_null, + JSContext *ctx, FunctionCallInfo fcinfo); +Datum pljs_jsvalue_to_record(pljs_type *type, JSValue val, bool *is_null, + TupleDesc tupdesc, JSContext *ctx); +Datum *pljs_jsvalue_to_datums(pljs_type *type, JSValue val, bool **is_null, + TupleDesc tupdesc, JSContext *ctx); // Utility uint32_t pljs_js_array_length(JSValue, JSContext *); diff --git a/src/types.c b/src/types.c index 807ccbb..358196c 100644 --- a/src/types.c +++ b/src/types.c @@ -20,6 +20,19 @@ #include +/* + * Error handling helper macros for consistent error patterns. + */ +#define PLJS_THROW_IF_NULL(ptr, msg, ctx) \ + do { \ + if ((ptr) == NULL) { \ + return js_throw((msg), (ctx)); \ + } \ + } while (0) + +#define PLJS_THROW_TYPE_ERROR(expected, ctx) \ + js_throw("expected " expected " type", (ctx)) + // Helper functions that should really exist as part of quickjs. static JSClassID JS_CLASS_OBJECT = 1; static JSClassID JS_CLASS_DATE = 10; @@ -72,7 +85,7 @@ static Jsonb *convert_object(JSValue object, JSContext *ctx); * @param @c double Javascript epoch * @returns #Datum of type `DATEADT` */ -static Datum epoch_to_date(double epoch) { +static Datum pljs_convert_epoch_to_date(double epoch) { epoch -= (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * 86400000.0; #ifdef HAVE_INT64_TIMESTAMP @@ -89,7 +102,7 @@ static Datum epoch_to_date(double epoch) { * @param @c double Javascript epoch * @returns #Datum of a timestamptz */ -static Datum epoch_to_timestamptz(double epoch) { +static Datum pljs_convert_epoch_to_timestamptz(double epoch) { epoch -= (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * 86400000.0; #ifdef HAVE_INT64_TIMESTAMP @@ -105,7 +118,7 @@ static Datum epoch_to_timestamptz(double epoch) { * @param #Datum of type `DateADT` * @returns @c double Javascript epoch */ -static double date_to_epoch(DateADT date) { +static double pljs_convert_date_to_epoch(DateADT date) { double epoch; #ifdef HAVE_INT64_TIMESTAMP @@ -123,7 +136,7 @@ static double date_to_epoch(DateADT date) { * @param #Datum of type `TimestampTz` * @returns @c double Javascript epoch */ -static double timestamptz_to_epoch(TimestampTz tm) { +static double pljs_convert_timestamptz_to_epoch(TimestampTz tm) { double epoch; #ifdef HAVE_INT64_TIMESTAMP @@ -145,7 +158,7 @@ static double timestamptz_to_epoch(TimestampTz tm) { * @param what #text - string to duplicate * @returns @c char * copy of the text field */ -static char *dup_pgtext(text *what) { +static char *pljs_util_dup_pgtext(text *what) { size_t len = VARSIZE(what) - VARHDRSZ; char *dup = palloc(len + 1); @@ -158,7 +171,7 @@ static char *dup_pgtext(text *what) { /** * @brief Converts an SPI status into static text. */ -static const char *spi_status_string(int status) { +static const char *pljs_util_spi_status_string(int status) { static char private_buf[1024]; if (status > 0) @@ -245,18 +258,43 @@ void pljs_type_fill(pljs_type *type, Oid typid) { } } +/** + * @brief Helper to get or lookup a TupleDesc with consistent ownership + * semantics. + * + * If a TupleDesc is provided, it is used directly and needs_release is set to + * false. If NULL is provided, a TupleDesc is looked up from the type OID and + * needs_release is set to true, indicating the caller must call + * ReleaseTupleDesc when done. + * + * @param typid #Oid - the type OID to look up if provided is NULL + * @param provided #TupleDesc - optional pre-existing TupleDesc to use + * @param needs_release @c bool* - output indicating if caller must release + * @returns #TupleDesc the tuple descriptor to use + */ +static TupleDesc pljs_get_tupdesc(Oid typid, TupleDesc provided, + bool *needs_release) { + if (provided != NULL) { + *needs_release = false; + return provided; + } + + *needs_release = true; + return lookup_rowtype_tupdesc(typid, -1); +} + /** * @brief Converts a #Datum for a Javascript object. * * Takes a #Datum and converts it to a Javascript object. If there is * an error, throws a Javascript exception. * - * @param arg #Datum - value to convert * @param type #pljs_type - type information of the #Datum + * @param arg #Datum - value to convert * @param ctx #JSContext - Javascript context to execute in * @returns #JSValue of the object or thrown exception in case of error */ -JSValue pljs_datum_to_object(Datum arg, pljs_type *type, JSContext *ctx) { +JSValue pljs_datum_to_object(pljs_type *type, Datum arg, JSContext *ctx) { if (arg == 0) { return JS_UNDEFINED; } @@ -306,14 +344,10 @@ JSValue pljs_datum_to_object(Datum arg, pljs_type *type, JSContext *ctx) { datum = heap_getattr(&tuple, i + 1, tupdesc, &isnull); - if (isnull) { - JS_SetPropertyStr(ctx, obj, colname, JS_NULL); - } else { - JS_SetPropertyStr( - ctx, obj, colname, - pljs_datum_to_jsvalue(datum, TupleDescAttr(tupdesc, i)->atttypid, - ctx, false)); - } + JS_SetPropertyStr( + ctx, obj, colname, + pljs_datum_to_jsvalue(TupleDescAttr(tupdesc, i)->atttypid, datum, + isnull, true, ctx)); } ReleaseTupleDesc(tupdesc); @@ -328,12 +362,12 @@ JSValue pljs_datum_to_object(Datum arg, pljs_type *type, JSContext *ctx) { * Takes a Postgres #Datum and type and converts it into a Javascript * array. All properties are set, including array length. * + * @param type #pljs_type - type information for the array * @param arg #Datum - Postgres array to convert - * @oaram type #pljs_type - type information for the array * @param ctx #JSContext - Javascript context to execute in * @returns #JSValue of the array */ -JSValue pljs_datum_to_array(Datum arg, pljs_type *type, JSContext *ctx) { +JSValue pljs_datum_to_array(pljs_type *type, Datum arg, JSContext *ctx) { JSValue array = JS_NewArray(ctx); Datum *values; bool *nulls; @@ -344,8 +378,7 @@ JSValue pljs_datum_to_array(Datum arg, pljs_type *type, JSContext *ctx) { for (int i = 0; i < nelems; i++) { JSValue value = - nulls[i] ? JS_NULL - : pljs_datum_to_jsvalue(values[i], type->typid, ctx, false); + pljs_datum_to_jsvalue(type->typid, values[i], nulls[i], true, ctx); JS_SetPropertyUint32(ctx, array, i, value); } @@ -360,7 +393,7 @@ JSValue pljs_datum_to_array(Datum arg, pljs_type *type, JSContext *ctx) { } /** - * @brief Default type conversion from @Datum to @JSValue. + * @brief Fallback type conversion from @Datum to @JSValue. * * If a type is unknown, instead of returning `undefined` or `NULL`, do a * `cstring` or `varlena`, or value based conversion to an `String` or `Int32` @@ -376,8 +409,8 @@ JSValue pljs_datum_to_array(Datum arg, pljs_type *type, JSContext *ctx) { * @param ctx #JSContext - Javascript context * @returns #JSValue conversion of the Datum */ -static JSValue pljs_datum_to_jsvalue_default(Datum arg, pljs_type type, - JSContext *ctx) { +static JSValue pljs_datum_to_jsvalue_fallback(Datum arg, pljs_type type, + JSContext *ctx) { JSValue ret = JS_UNDEFINED; if (type.byval) { @@ -402,18 +435,23 @@ static JSValue pljs_datum_to_jsvalue_default(Datum arg, pljs_type type, * * Takes a Postgres #Datum and type and converts it into a Javascript * value. If the type is an array or is composite, then call out to - * the correct functions. If `skip_composite` is true, then the value - * is directly converted, even if it is composite. There is currently - * only one case for this: conversion from a window function. + * the correct functions. * - * @param arg #Datum - Postgres array to convert - * @oaram type #pljs_type - type information for the type + * @param argtype #Oid - type information for the type + * @param arg #Datum - Postgres value to convert + * @param is_null @c bool - whether the datum is null + * @param expand_composite @c bool - whether to expand composite types to + * objects * @param ctx #JSContext - Javascript context to execute in - * @param skip_composite @c bool - whether to skip the composite check - * @returns #JSValue of the value, or JS_NULL if unable to convert + * @returns #JSValue of the value, or JS_NULL if null */ -JSValue pljs_datum_to_jsvalue(Datum arg, Oid argtype, JSContext *ctx, - bool skip_composite) { +JSValue pljs_datum_to_jsvalue(Oid argtype, Datum arg, bool is_null, + bool expand_composite, JSContext *ctx) { + // Handle null case explicitly + if (is_null) { + return JS_NULL; + } + JSValue return_result; char *str; @@ -421,11 +459,11 @@ JSValue pljs_datum_to_jsvalue(Datum arg, Oid argtype, JSContext *ctx, pljs_type_fill(&type, argtype); if (type.category == TYPCATEGORY_ARRAY) { - return pljs_datum_to_array(arg, &type, ctx); + return pljs_datum_to_array(&type, arg, ctx); } - if (!skip_composite && type.is_composite) { - return pljs_datum_to_object(arg, &type, ctx); + if (expand_composite && type.is_composite) { + return pljs_datum_to_object(&type, arg, ctx); } switch (type.typid) { @@ -467,7 +505,7 @@ JSValue pljs_datum_to_jsvalue(Datum arg, Oid argtype, JSContext *ctx, case BPCHAROID: case XMLOID: // Get a copy of the string. - str = dup_pgtext(DatumGetTextP(arg)); + str = pljs_util_dup_pgtext(DatumGetTextP(arg)); return_result = JS_NewString(ctx, str); @@ -481,7 +519,7 @@ JSValue pljs_datum_to_jsvalue(Datum arg, Oid argtype, JSContext *ctx, case JSONOID: // Get a copy of the string. - str = dup_pgtext(DatumGetTextP(arg)); + str = pljs_util_dup_pgtext(DatumGetTextP(arg)); return_result = JS_ParseJSON(ctx, str, strlen(str), NULL); @@ -528,16 +566,17 @@ JSValue pljs_datum_to_jsvalue(Datum arg, Oid argtype, JSContext *ctx, } case DATEOID: - return_result = JS_NewDate(ctx, date_to_epoch(DatumGetDateADT(arg))); + return_result = + JS_NewDate(ctx, pljs_convert_date_to_epoch(DatumGetDateADT(arg))); break; case TIMESTAMPOID: case TIMESTAMPTZOID: - return_result = - JS_NewDate(ctx, timestamptz_to_epoch(DatumGetTimestampTz(arg))); + return_result = JS_NewDate( + ctx, pljs_convert_timestamptz_to_epoch(DatumGetTimestampTz(arg))); break; default: - return_result = pljs_datum_to_jsvalue_default(arg, type, ctx); + return_result = pljs_datum_to_jsvalue_fallback(arg, type, ctx); } return return_result; @@ -549,13 +588,13 @@ JSValue pljs_datum_to_jsvalue(Datum arg, Oid argtype, JSContext *ctx, * Takes a Javascript #JSValue of an array and type and converts * it into a Postgres array. * + * @param type #pljs_type - type information for the array * @param val #JSValue - Javascript array to convert - * @oaram type #pljs_type - type information for the array * @param ctx #JSContext - Javascript context to execute in * @param fcinfo #FunctionCallInfo - needed to conversion back to a #Datum * @returns #Datum of the array */ -Datum pljs_jsvalue_to_array(JSValue val, pljs_type *type, JSContext *ctx, +Datum pljs_jsvalue_to_array(pljs_type *type, JSValue val, JSContext *ctx, FunctionCallInfo fcinfo) { ArrayType *result; Datum *values; @@ -579,7 +618,7 @@ Datum pljs_jsvalue_to_array(JSValue val, pljs_type *type, JSContext *ctx, nulls[i] = true; } else { values[i] = - pljs_jsvalue_to_datum(elem, type->typid, ctx, fcinfo, &nulls[i]); + pljs_jsvalue_to_datum(type->typid, elem, &nulls[i], ctx, fcinfo); } } @@ -644,88 +683,136 @@ bool pljs_jsvalue_object_contains_all_column_names(JSValue val, JSContext *ctx, return true; } +/** + * @brief Converts a composite Javascript object into an array of Datums. + * + * Takes a Javascript object and converts it into an array of Datums, setting + * the null flag for each Datum if it is null. Note that this function assumes + * that `is_null` is allocated and initialized to `0` (`false`) for each + * element. + * + * @param type #pljs_type - type information for the record (used if tupdesc is + * NULL) + * @param val #JSValue - the Javascript object to convert + * @param is_null @c bool** - pointer to array of null flags for each element + * @param tupdesc #TupleDesc - can be `NULL`, will be looked up from type if so + * @param ctx #JSContext - Javascript context to execute in + * @returns Array of #Datum of the Javascript object, or NULL if val is + * null/undefined + */ +Datum *pljs_jsvalue_to_datums(pljs_type *type, JSValue val, bool **is_null, + TupleDesc tupdesc, JSContext *ctx) { + // Check for null/undefined BEFORE any allocations to avoid memory leaks + if (JS_IsNull(val) || JS_IsUndefined(val)) { + return NULL; + } + + // Get the tuple descriptor, looking it up if not provided + bool cleanup_tupdesc; + tupdesc = pljs_get_tupdesc(type ? type->typid : InvalidOid, tupdesc, + &cleanup_tupdesc); + + // Allocate the values array now that we have the tuple descriptor + Datum *values = (Datum *)palloc(sizeof(Datum) * tupdesc->natts); + + for (int16 c = 0; c < tupdesc->natts; c++) { + // If this is a dropped column, we can skip it, and set the null flag to + // true. + if (TupleDescAttr(tupdesc, c)->attisdropped) { + (*is_null)[c] = true; + continue; + } + + // Retrieve the column name of each attribute that we are expecting, we + // only care about named tuples. + char *colname = NameStr(TupleDescAttr(tupdesc, c)->attname); + + JSValue o = JS_GetPropertyStr(ctx, val, colname); + + if (JS_IsNull(o) || JS_IsUndefined(o)) { + (*is_null)[c] = true; + continue; + } + + // Set the value of each Datum, or set the `is_null` flag if it is + // considered `NULL`. + values[c] = pljs_jsvalue_to_datum(TupleDescAttr(tupdesc, c)->atttypid, o, + &(*is_null)[c], ctx, NULL); + } + + if (cleanup_tupdesc) { + ReleaseTupleDesc(tupdesc); + } + + return values; +} + /** * @brief Converts a Javascript object into a Postgres record. * * Takes a Javascript object and converts it into a Postgres * record (composite Postgres type). * + * @param type #pljs_type - type information for the record * @param val #JSValue - the Javascript object to convert - * @oaram type #pljs_type - type information for the record - * @param ctx #JSContext - Javascript context to execute in * @param is_null @c bool - pointer to fill of whether the record is null * @param tupdesc #TupleDesc - can be `NULL` - * @param tupstore #Tuplestorestate + * @param ctx #JSContext - Javascript context to execute in * @returns #Datum of the Postgres record */ -Datum pljs_jsvalue_to_record(JSValue val, pljs_type *type, JSContext *ctx, - bool *is_null, TupleDesc tupdesc, - Tuplestorestate *tupstore) { +Datum pljs_jsvalue_to_record(pljs_type *type, JSValue val, bool *is_null, + TupleDesc tupdesc, JSContext *ctx) { Datum result = 0; + // If the value is null or undefined, we can simply set the record to null + // and return a `NULL` Datum. if (JS_IsNull(val) || JS_IsUndefined(val)) { *is_null = true; return (Datum)0; } - bool cleanup_tupdesc = false; - PG_TRY(); - { - if (tupdesc == NULL) { - Oid rettype = type->typid; - - tupdesc = lookup_rowtype_tupdesc(rettype, -1); - cleanup_tupdesc = true; - } - } - PG_CATCH(); - { - PG_RE_THROW(); - } - PG_END_TRY(); + // Get the tuple descriptor, looking it up if not provided + bool cleanup_tupdesc; + tupdesc = pljs_get_tupdesc(type->typid, tupdesc, &cleanup_tupdesc); - if (tupdesc != NULL) { - Datum *values = (Datum *)palloc(sizeof(Datum) * tupdesc->natts); - bool *nulls = (bool *)palloc(sizeof(bool) * tupdesc->natts); + Datum *values = (Datum *)palloc0(sizeof(Datum) * tupdesc->natts); + bool *nulls = (bool *)palloc0(sizeof(bool) * tupdesc->natts); - memset(nulls, 0, sizeof(bool) * tupdesc->natts); + for (int16 c = 0; c < tupdesc->natts; c++) { + if (TupleDescAttr(tupdesc, c)->attisdropped) { + nulls[c] = true; + continue; + } - for (int16 c = 0; c < tupdesc->natts; c++) { - if (TupleDescAttr(tupdesc, c)->attisdropped) { - nulls[c] = true; - continue; - } + char *colname = NameStr(TupleDescAttr(tupdesc, c)->attname); - char *colname = NameStr(TupleDescAttr(tupdesc, c)->attname); + JSValue o = JS_GetPropertyStr(ctx, val, colname); - JSValue o = JS_GetPropertyStr(ctx, val, colname); + if (JS_IsNull(o) || JS_IsUndefined(o)) { + nulls[c] = true; + continue; + } - if (JS_IsNull(o) || JS_IsUndefined(o)) { - nulls[c] = true; - continue; - } + values[c] = pljs_jsvalue_to_datum(TupleDescAttr(tupdesc, c)->atttypid, o, + &nulls[c], ctx, NULL); + } - values[c] = pljs_jsvalue_to_datum(o, TupleDescAttr(tupdesc, c)->atttypid, - ctx, NULL, &nulls[c]); - } + // Form a Tuple from the values and nulls using the tuple descriptor + // as the template for the tuple. + result = HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)); - if (tupstore != NULL) { - result = (Datum)0; - tuplestore_putvalues(tupstore, tupdesc, values, nulls); - } else { - result = HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls)); - } + pfree(nulls); + pfree(values); - if (cleanup_tupdesc) { - ReleaseTupleDesc(tupdesc); - } + if (cleanup_tupdesc) { + ReleaseTupleDesc(tupdesc); } return result; } /** - * @brief Default type conversion from @JSValue to @Datum. + * @brief Fallback type conversion from @JSValue to @Datum. * * If a type is unknown, instead of returning `NULL`, do a * `cstring` or `varlena`, or value based conversion to an `Datum`. @@ -735,22 +822,22 @@ Datum pljs_jsvalue_to_record(JSValue val, pljs_type *type, JSContext *ctx, * a `varlena` will be used and the size set appropriately. * * @param value #JSValue - Javascript to convert - * @param isnull + * @param is_null @c bool - pointer to fill of whether the value is null * @param type #pljs_type - type of the datum * @param ctx #JSContext - Javascript context * @returns #Datum conversion of the JSValue */ -static Datum jsvalue_to_datum_default(JSValue value, bool *isnull, - pljs_type type, JSContext *ctx) { +static Datum pljs_jsvalue_to_datum_fallback(JSValue value, bool *is_null, + pljs_type type, JSContext *ctx) { Datum ret = 0; // Set whether the Datum is `NULL` or not. JSValue is_set_null_value = JS_GetPropertyStr(ctx, value, "is_null"); - *isnull = JS_ToBool(ctx, is_set_null_value); + *is_null = JS_ToBool(ctx, is_set_null_value); // If the value's property of `null` is set to `true`, we return an empty // Datum. - if (*isnull) { + if (*is_null) { return (Datum)0; } @@ -799,22 +886,26 @@ static Datum jsvalue_to_datum_default(JSValue value, bool *isnull, * checking whether it is an array or record and converting it * properly. * + * @param rettype #Oid - type information for the record * @param val #JSValue - the Javascript object to convert - * @oaram type #pljs_type - type information for the record + * @param is_null @c bool* - pointer to fill with whether the result is null * @param ctx #JSContext - Javascript context to execute in - * @param fcinfo #FunctionCallInfo - * @param is_null @c bool - pointer to fill of whether the record is null + * @param fcinfo #FunctionCallInfo - optional, can be NULL * @returns #Datum of the Postgres value */ -Datum pljs_jsvalue_to_datum(JSValue val, Oid rettype, JSContext *ctx, - FunctionCallInfo fcinfo, bool *isnull) { +Datum pljs_jsvalue_to_datum(Oid rettype, JSValue val, bool *is_null, + JSContext *ctx, FunctionCallInfo fcinfo) { + // Initialize is_null to false + if (is_null) { + *is_null = false; + } pljs_type type; pljs_type_fill(&type, rettype); if (type.typid != JSONOID && type.typid != JSONBOID && JS_IsArray(ctx, val)) { - return pljs_jsvalue_to_array(val, &type, ctx, fcinfo); + return pljs_jsvalue_to_array(&type, val, ctx, fcinfo); } if (type.category == TYPCATEGORY_ARRAY && !JS_IsArray(ctx, val)) { @@ -822,15 +913,15 @@ Datum pljs_jsvalue_to_datum(JSValue val, Oid rettype, JSContext *ctx, } if (type.is_composite) { - return pljs_jsvalue_to_record(val, &type, ctx, isnull, NULL, NULL); + return pljs_jsvalue_to_record(&type, val, is_null, NULL, ctx); } if (JS_IsNull(val) || JS_IsUndefined(val)) { if (fcinfo) { PG_RETURN_NULL(); } else { - if (isnull) { - *isnull = true; + if (is_null) { + *is_null = true; } PG_RETURN_NULL(); @@ -1107,7 +1198,7 @@ Datum pljs_jsvalue_to_datum(JSValue val, Oid rettype, JSContext *ctx, if (Is_Date(val)) { double in; JS_ToFloat64(ctx, &in, val); - return epoch_to_date(in); + return pljs_convert_epoch_to_date(in); } break; case TIMESTAMPOID: @@ -1115,12 +1206,12 @@ Datum pljs_jsvalue_to_datum(JSValue val, Oid rettype, JSContext *ctx, if (Is_Date(val)) { double in; JS_ToFloat64(ctx, &in, val); - return epoch_to_timestamptz(in); + return pljs_convert_epoch_to_timestamptz(in); } break; default: - return jsvalue_to_datum_default(val, isnull, type, ctx); + return pljs_jsvalue_to_datum_fallback(val, is_null, type, ctx); } // shut up, compiler @@ -1175,13 +1266,9 @@ JSValue pljs_tuple_to_jsvalue(TupleDesc tupledesc, HeapTuple heap_tuple, char *name = NameStr(tuple_attrs->attname); - if (isnull) { - JS_SetPropertyStr(ctx, obj, name, JS_NULL); - } else { - JS_SetPropertyStr( - ctx, obj, name, - pljs_datum_to_jsvalue(datum, tuple_attrs->atttypid, ctx, false)); - } + JS_SetPropertyStr( + ctx, obj, name, + pljs_datum_to_jsvalue(tuple_attrs->atttypid, datum, isnull, true, ctx)); } return obj; @@ -1198,7 +1285,7 @@ JSValue pljs_spi_result_to_jsvalue(int status, JSContext *ctx) { JSValue result; if (status < 0) { - return js_throw(spi_status_string(status), ctx); + return js_throw(pljs_util_spi_status_string(status), ctx); } switch (status) {