From ba8e3cbf5cd55045aa04ddb351219bed9fdc1957 Mon Sep 17 00:00:00 2001 From: wener Date: Wed, 4 Mar 2026 18:24:17 +0800 Subject: [PATCH] Fix composite SRF (RETURNS TABLE) to correctly handle array returns The composite case in call_srf_function had three bugs: 1. Used argv[0] (input argument) instead of ret (JS return value), causing all column values to be NULL. 2. Did not handle array-of-objects return for RETURNS TABLE/SETOF, only processing a single object. JS functions returning arrays like [{id:1, name:'a'}, {id:2, name:'b'}] would crash (segfault). 3. Missing NULL check on pfree(values) when pljs_jsvalue_to_datums returns NULL. Also fixed a minor memory leak: added JS_FreeValue for array elements in the non-composite scalar array case. Added regression tests for: - RETURNS TABLE with array of objects - RETURNS TABLE with single object return - RETURNS SETOF composite with array return --- expected/function.out | 46 +++++++++++++++++++++++++++++++++++++++++++ sql/function.sql | 31 +++++++++++++++++++++++++++++ src/pljs.c | 42 ++++++++++++++++++++++++++++++++------- 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/expected/function.out b/expected/function.out index 0ed6a64..ba5c59c 100644 --- a/expected/function.out +++ b/expected/function.out @@ -239,6 +239,52 @@ SELECT * FROM set_of_unnamed_records() AS x(a int, b int); 1 | 2 (1 row) +-- RETURNS TABLE with array of objects (composite SRF array return) +CREATE FUNCTION returns_table_array() RETURNS TABLE(id int, name text) AS +$$ + return [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 3, name: 'Charlie' } + ]; +$$ +LANGUAGE pljs; +SELECT * FROM returns_table_array(); + id | name +----+--------- + 1 | Alice + 2 | Bob + 3 | Charlie +(3 rows) + +-- RETURNS TABLE with single object +CREATE FUNCTION returns_table_single() RETURNS TABLE(x int, y text) AS +$$ + return { x: 42, y: 'hello' }; +$$ +LANGUAGE pljs; +SELECT * FROM returns_table_single(); + x | y +----+------- + 42 | hello +(1 row) + +-- RETURNS SETOF composite with array return +CREATE FUNCTION set_of_rec_array() RETURNS SETOF rec AS +$$ + return [ + { i: 10, t: 'ten' }, + { i: 20, t: 'twenty' } + ]; +$$ +LANGUAGE pljs; +SELECT * FROM set_of_rec_array(); + i | t +----+-------- + 10 | ten + 20 | twenty +(2 rows) + -- execute with an array of arguments CREATE FUNCTION execute_with_array() RETURNS VOID AS $$ diff --git a/sql/function.sql b/sql/function.sql index 517bb5b..f20d75b 100644 --- a/sql/function.sql +++ b/sql/function.sql @@ -148,6 +148,37 @@ SELECT * FROM set_of_unnamed_records() AS x(a int, c int); -- name counts and values match SELECT * FROM set_of_unnamed_records() AS x(a int, b int); +-- RETURNS TABLE with array of objects (composite SRF array return) +CREATE FUNCTION returns_table_array() RETURNS TABLE(id int, name text) AS +$$ + return [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 3, name: 'Charlie' } + ]; +$$ +LANGUAGE pljs; +SELECT * FROM returns_table_array(); + +-- RETURNS TABLE with single object +CREATE FUNCTION returns_table_single() RETURNS TABLE(x int, y text) AS +$$ + return { x: 42, y: 'hello' }; +$$ +LANGUAGE pljs; +SELECT * FROM returns_table_single(); + +-- RETURNS SETOF composite with array return +CREATE FUNCTION set_of_rec_array() RETURNS SETOF rec AS +$$ + return [ + { i: 10, t: 'ten' }, + { i: 20, t: 'twenty' } + ]; +$$ +LANGUAGE pljs; +SELECT * FROM set_of_rec_array(); + -- execute with an array of arguments CREATE FUNCTION execute_with_array() RETURNS VOID AS $$ diff --git a/src/pljs.c b/src/pljs.c index 7fc9413..35ecdfe 100644 --- a/src/pljs.c +++ b/src/pljs.c @@ -1183,15 +1183,42 @@ static Datum call_srf_function(FunctionCallInfo fcinfo, pljs_context *context, bool is_null = false; if (state->is_composite) { - bool *nulls = (bool *)palloc0(sizeof(bool) * state->tuple_desc->natts); + // Handle composite (RETURNS TABLE / RETURNS SETOF record): + // JS can return a single object or an array of objects. + if (JS_IsArray(context->ctx, ret)) { + // Array of objects: iterate and put each as a row. + for (uint32_t i = 0; i < pljs_js_array_length(ret, context->ctx); + i++) { + JSValue val = JS_GetPropertyUint32(context->ctx, ret, i); + bool *nulls = + (bool *)palloc0(sizeof(bool) * state->tuple_desc->natts); + + Datum *values = pljs_jsvalue_to_datums( + NULL, val, &nulls, state->tuple_desc, context->ctx); + + if (values != NULL) { + tuplestore_putvalues(state->tuple_store_state, + state->tuple_desc, values, nulls); + pfree(values); + } + pfree(nulls); + JS_FreeValue(context->ctx, val); + } + } else { + // Single object: put as one row. + 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); + Datum *values = pljs_jsvalue_to_datums( + NULL, ret, &nulls, state->tuple_desc, context->ctx); - pfree(nulls); - pfree(values); + if (values != NULL) { + tuplestore_putvalues(state->tuple_store_state, + state->tuple_desc, values, nulls); + pfree(values); + } + pfree(nulls); + } } else { if (JS_IsArray(context->ctx, ret)) { for (uint32_t i = 0; i < pljs_js_array_length(ret, context->ctx); @@ -1203,6 +1230,7 @@ static Datum call_srf_function(FunctionCallInfo fcinfo, pljs_context *context, context->ctx, NULL); tuplestore_putvalues(state->tuple_store_state, state->tuple_desc, &result, &is_null); + JS_FreeValue(context->ctx, val); } } else { if (!JS_IsUndefined(ret)) {