Skip to content

Expand table-returning functions into columns in a FROM clause - #3222

Open
reltuk wants to merge 1 commit into
mainfrom
aaron/sql-table-functions
Open

Expand table-returning functions into columns in a FROM clause#3222
reltuk wants to merge 1 commit into
mainfrom
aaron/sql-table-functions

Conversation

@reltuk

@reltuk reltuk commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

A function called in a FROM clause is already rewritten into a vitess.TableFuncExpr and resolved through GMS's TableFunctionWrapper, which asks sql.ExtendedTableFunction for the function's result columns. CompiledFunction answered that question with the function's OUT parameters only, so a function declared RETURNS TABLE(...) or RETURNS SETOF - which stores its result columns as the fields of a composite return type rather than as OUT parameters - reported a single column, and SELECT * FROM figures() returned one record value per row instead of one column per field.

OutParametersSchema now falls back to the fields of a composite return type, and Eval expands each record value back into separate columns, assignment casting each field to the result type the function declares. The SELECT-list shape is unchanged: EvalRowIter keeps returning one record value per row, so it now calls the underlying overload directly rather than going through Eval.

Also fixes a PL/pgSQL function declared RETURNS SETOF , whose RETURN QUERY results were buffered as one-field records and so failed to render in either a FROM clause or a SELECT list.

A function called in a FROM clause is already rewritten into a
vitess.TableFuncExpr and resolved through GMS's TableFunctionWrapper,
which asks sql.ExtendedTableFunction for the function's result columns.
CompiledFunction answered that question with the function's OUT
parameters only, so a function declared RETURNS TABLE(...) or
RETURNS SETOF <composite> - which stores its result columns as the
fields of a composite return type rather than as OUT parameters -
reported a single column, and `SELECT * FROM figures()` returned one
record value per row instead of one column per field.

OutParametersSchema now falls back to the fields of a composite return
type, and Eval expands each record value back into separate columns,
assignment casting each field to the result type the function declares.
The SELECT-list shape is unchanged: EvalRowIter keeps returning one
record value per row, so it now calls the underlying overload directly
rather than going through Eval.

Also fixes a PL/pgSQL function declared RETURNS SETOF <scalar>, whose
RETURN QUERY results were buffered as one-field records and so failed
to render in either a FROM clause or a SELECT list.
@reltuk
reltuk requested a review from Hydrocharged August 28, 2026 11:16
@github-actions

Copy link
Copy Markdown
Contributor
Main PR
Total 42090 42090
Successful 19245 19260
Failures 22845 22830
Partial Successes1 5469 5469
Main PR
Successful 45.7234% 45.7591%
Failures 54.2766% 54.2409%

${\color{lightgreen}Progressions (14)}$

join

QUERY: select * from mki8(1,2);
QUERY: select * from mki4(42);

plpgsql

QUERY: select * from ret_query2(8);

polymorphism

QUERY: select * from dfunc(1,2);
QUERY: select * from testpolym(37);

rangefuncs

QUERY: select * from rngfunc2 where f2 in (select f2 from rngfunct(rngfunc2.rngfuncid) z where z.rngfuncid = rngfunc2.rngfuncid) ORDER BY 1,2;
QUERY: select * from rngfunc2 where f2 in (select f2 from rngfunct(1) z where z.rngfuncid = rngfunc2.rngfuncid) ORDER BY 1,2;
QUERY: select * from rngfunc2 where f2 in (select f2 from rngfunct(rngfunc2.rngfuncid) z where z.rngfuncid = 1) ORDER BY 1,2;
QUERY: select rngfunct.rngfuncid, rngfunct.f2 from rngfunct(sin(pi()/2)::int) ORDER BY 1,2;
QUERY: SELECT * FROM getrngfunc5(1) AS t1;
QUERY: SELECT * FROM rngfunc();

subselect

QUERY: select count(*) from tenk1 t
where (exists(select 1 from tenk1 k where k.unique1 = t.unique2) or ten < 0);

window

QUERY: SELECT * FROM unbounded_syntax_test2a(2);
QUERY: SELECT * FROM unbounded_syntax_test2b(2);

Footnotes

  1. These are tests that we're marking as Successful, however they do not match the expected output in some way. This is due to small differences, such as different wording on the error messages, or the column names being incorrect while the data itself is correct.

@itoqa

itoqa Bot commented Aug 28, 2026

Copy link
Copy Markdown

Ito QA test results
Commit: 1b65a24: 13 test cases ran, 12 passed ✅, 1 additional finding ⚠️.

Summary

Coverage spans core database-function behavior, including scalar and multi-field results, field naming and types, filtering, joins, aliases, and query usage. It also exercises edge cases such as empty results, null values, declared type conversion, composite value preservation, and correlated table-function behavior.

Safe to merge — the only failure is a medium-severity limitation in correlated table-function queries and is explicitly unrelated to this PR, with no regressions or newly introduced failures attributable to the change. It is a flag for later rather than a merge blocker.

Tests run by Ito

View full run

Result Severity Type Description
General The join returned the matching square row with the base table and function fields in the right places. The sides filter and column aliases worked as expected.
General The table function exposed its two fields under the requested aliases, kept their text and integer types, and returned only the square row when filtered for four or more sides.
General The function returned integer values 3 and 4 when called directly and from a table query. Filtering for values greater than 3 returned 4 in both forms.
Columns The function returned separate shape and sides columns with the expected triangle and square rows.
Parameters The function returned separate doubled and label columns with the expected values and types.
Record Selecting the table-returning function directly kept each result as one composite value, with triangle/3 and square/4 returned in two rows.
Rev The empty table function returned zero rows when used in both supported query forms.
Rev The returned table kept the missing value as SQL NULL while preserving the value 42 in the other column. The same result also stayed one correctly formatted record when called in the SELECT list.
Rev A table function returned the declared integer and text types even though its body used bigint and varchar values. Filtering, arithmetic, and text expressions also returned the expected results.
Rev A SQL function returned the values 3 and 4 as separate integer rows in both FROM and SELECT queries.
Rev A function returning named records produced shape, sides, and is_regular in the declared order with the expected values and types. Renaming the fields with an alias list kept the same positions and values.
Scalar The function returned integer rows 3 and 4 in both SELECT-list and FROM queries.
⚠️ Medium severity Rev The correlated LATERAL query was rejected by the server. No per-input matches or expanded composite fields were returned.
Additional Findings Details

These findings are unrelated to the current changes but were observed during testing.

🟡 Correlated table function fails with outer-row input
  • Severity: Medium Medium severity
  • Description: The correlated LATERAL query was rejected by the server. No per-input matches or expanded composite fields were returned.
  • Impact: Applications that use a composite table function with a correlated LATERAL join cannot run the query or get results for each input row. Users must avoid this query pattern or change the query to work around the limitation.
  • Steps to Reproduce:
    1. Create a composite type with an integer key and a text field.
    2. Create an input table with several integer keys and a composite SETOF function that filters its rows using an integer argument.
    3. Run a query that joins the input table to LATERAL rev4_filter(i.id), ordering by the input key and returned field.
    4. Observe that the server returns unsupported syntax: values row(rev4_filter(i.id)) instead of producing expanded composite rows for each input row.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The PR implementation in server/functions/framework/compiled_function.go:188-216 adds OutParametersSchema support for composite return types by deriving one sql.Column per CompositeAttrs entry. Lines 219-235 unwrap a []pgtypes.RecordValue into a flat sql.Row for the FROM result. This supports the result schema and value conversion after a table function has been evaluated, but it does not implement correlated evaluation or a LATERAL-specific plan path. The execution path at lines 413-425 calls evalRaw for the current row and only wraps the resulting iterator with recordExpandingRowIter; evalRaw at lines 449-514 evaluates arguments against the supplied row, but no code in this implementation handles the VALUES-based representation produced for the correlated table-function call. The prior local SQL execution reached the valid correlated query and failed with unsupported syntax: values row(rev4_filter(i.id)), which is consistent with the absent planner/execution support rather than a bad fixture. Existing SQL function coverage at testing/go/create_function_sql_test.go:477-551 exercises standalone FROM calls, joins, and SELECT-list calls, but contains no correlated LATERAL case. The smallest practical fix is to add the missing correlated table-function planning/evaluation handling at the query/table-function boundary, preserving the outer row when evaluating rev4_filter(i.id), then pass each returned record through the existing composite schema expansion. The composite schema and Unwrap changes alone cannot fix this failure.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

@coffeegoddd

Copy link
Copy Markdown
Contributor

@reltuk DOLT

read_tests from_latency to_latency percent_change
covering_index_scan_postgres 2.43 2.43 0.0
groupby_scan_postgres 75.82 77.19 1.81
index_join_postgres 2.26 2.26 0.0
index_join_scan_postgres 1.61 1.61 0.0
index_scan_postgres 484.44 493.24 1.82
oltp_point_select 0.37 0.37 0.0
oltp_read_only 6.55 6.55 0.0
select_random_points 0.72 0.72 0.0
select_random_ranges 1.03 1.04 0.97
table_scan_postgres 484.44 493.24 1.82
types_table_scan_postgres 1235.62 1235.62 0.0
write_tests from_latency to_latency percent_change
oltp_delete_insert_postgres 6.67 6.67 0.0
oltp_insert 3.36 3.36 0.0
oltp_read_write 13.46 13.46 0.0
oltp_update_index 3.62 3.55 -1.93
oltp_update_non_index 3.3 3.3 0.0
oltp_write_only 7.04 7.04 0.0
types_delete_insert_postgres 7.17 7.17 0.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants