Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/main/java/net/sf/jsqlparser/expression/Function.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import net.sf.jsqlparser.statement.select.Limit;
import net.sf.jsqlparser.statement.select.OrderByElement;

import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
import net.sf.jsqlparser.statement.select.PlainSelect;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -46,6 +49,7 @@ public class Function extends ASTNodeAccessImpl implements Expression {
private KeepExpression keep = null;
private String onOverflowTruncate = null;
private String extraKeyword = null;
private List<ColumnDefinition> resultColumnDefinitions;

/**
* Generic keyword arguments captured inside function parentheses, e.g.
Expand All @@ -55,6 +59,23 @@ public class Function extends ASTNodeAccessImpl implements Expression {
*/
private List<KeywordArgument> keywordArguments = null;

/** Column definitions supplied for a record-returning function inside ROWS FROM. */
public List<ColumnDefinition> getResultColumnDefinitions() {
return resultColumnDefinitions;
}

public void setResultColumnDefinitions(List<ColumnDefinition> resultColumnDefinitions) {
this.resultColumnDefinitions = resultColumnDefinitions;
}

public StringBuilder appendResultColumnDefinitionsTo(StringBuilder builder) {
if (resultColumnDefinitions != null) {
builder.append(" AS ")
.append(PlainSelect.getStringList(resultColumnDefinitions, true, true));
}
return builder;
}

public Function() {}

public Function(String name, Expression... parameters) {
Expand Down Expand Up @@ -446,7 +467,8 @@ public String toString() {
ans = "{fn " + ans + "}";
}

return ans;
return resultColumnDefinitions == null ? ans
: appendResultColumnDefinitionsTo(new StringBuilder(ans)).toString();
}

public Function withAttribute(Expression attribute) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package net.sf.jsqlparser.statement.select;

import java.util.List;
import java.util.function.Consumer;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
Expand Down Expand Up @@ -228,13 +229,24 @@ public FromItem setSampleClause(SampleClause sampleClause) {
}

public StringBuilder appendTo(StringBuilder builder) {
return appendTo(builder, builder::append);
}

public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionPrinter) {
if (prefix != null) {
builder.append(prefix).append(" ");
}
if (rowsFromFunctions != null) {
builder.append("ROWS FROM ").append(rowsFromFunctions);
builder.append("ROWS FROM (");
for (int i = 0; i < rowsFromFunctions.size(); i++) {
if (i > 0) {
builder.append(", ");
}
expressionPrinter.accept(rowsFromFunctions.get(i));
}
builder.append(")");
} else {
builder.append(function);
expressionPrinter.accept(function);
}

if (withClause != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,7 @@ public <S> StringBuilder visit(Function function, S context) {
if (function.isEscaped()) {
builder.append("}");
}
return builder;
return function.appendResultColumnDefinitionsTo(builder);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,8 @@ public <S> StringBuilder visit(TableStatement tableStatement, S context) {

@Override
public <S> StringBuilder visit(TableFunction tableFunction, S context) {
tableFunction.appendTo(builder);
tableFunction.appendTo(builder,
expression -> expression.accept(expressionVisitor, context));
return builder;
}

Expand Down
39 changes: 37 additions & 2 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -13529,17 +13529,52 @@ ParenthesedExpressionList<Function> RowsFromFunctionList():
}
{
"("
function = Function() { functions.add(function); }
function = RowsFromFunction() { functions.add(function); }
(
","
function = Function() { functions.add(function); }
function = RowsFromFunction() { functions.add(function); }
)*
")"
{
return functions;
}
}

/** A function and its optional PostgreSQL record column definition list. */
Function RowsFromFunction():
{
Function function;
List<ColumnDefinition> columns = new ArrayList<ColumnDefinition>();
ColumnDefinition column;
}
{
function=Function()
[ <K_AS> "("
column=RowsFromColumnDefinition() { columns.add(column); }
( "," column=RowsFromColumnDefinition() { columns.add(column); } )*
")" { function.setResultColumnDefinitions(columns); }
]
{ return function; }
}

ColumnDefinition RowsFromColumnDefinition():
{
String name;
String collation;
String part;
ColDataType type;
ColumnDefinition column;
}
{
name=RelObjectName() type=ColDataType()
{ column = new ColumnDefinition(name, type); }
[ <K_COLLATE> collation=RelObjectName()
( "." part=RelObjectName() { collation += "." + part; } )*
{ column.addColumnOptions(ColumnOption.collate(collation)); }
]
{ return column; }
}

List<Index.ColumnParams> ColumnNamesWithParamsList() : {
List<Index.ColumnParams> colNames = new ArrayList<Index.ColumnParams>();
String columnName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement.select;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import java.util.Set;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.create.table.ColDataType;
import net.sf.jsqlparser.statement.create.table.ColumnDefinition;
import net.sf.jsqlparser.util.TablesNamesFinder;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import net.sf.jsqlparser.util.deparser.SelectDeParser;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;

class RowsFromColumnDefinitionTest {

// Accepted/rejected cases are data records, not executable SQL batch scripts.
// Both outcomes were checked against PostgreSQL 18.6.
@ParameterizedTest
@CsvFileSource(resources = "rows-from-cases.tsv", delimiter = '\t')
void preservesValidDefinitionsAndRejectsMalformedLists(boolean valid, String sql)
throws JSQLParserException {
if (!valid) {
assertThrows(JSQLParserException.class, () -> parse(sql));
return;
}
PlainSelect select = parse(sql);
StringBuilder visitor = new StringBuilder();
select.accept(new StatementDeParser(visitor));
for (String rendered : List.of(select.toString(), visitor.toString())) {
PlainSelect reparsed = parse(rendered);
assertEquals(select.toString(), reparsed.toString());
assertEquals(select.getFromItem().getClass(), reparsed.getFromItem().getClass());
}
}

@Test
void eachFunctionOwnsItsDefinitionsIndependentlyOfTheOuterAlias()
throws JSQLParserException {
PlainSelect select = parse("SELECT * FROM ROWS FROM ("
+ "json_to_record('{}') AS (amount numeric(10, 2), label text COLLATE \"C\"), "
+ "generate_series(1, 2), json_to_record('{}') AS (items integer[])) "
+ "WITH ORDINALITY AS r(total, name, n, data, ord)");
TableFunction table = (TableFunction) select.getFromItem();
assertTrue(table.isRowsFrom());
assertEquals(3, table.getRowsFromFunctions().size());
assertEquals("ORDINALITY", table.getWithClause());
assertEquals(5, table.getAlias().getAliasColumns().size());
List<ColumnDefinition> first = table.getFunctions().get(0).getResultColumnDefinitions();
assertEquals("amount", first.get(0).getColumnName());
assertEquals(10, first.get(0).getColDataType().getPrecision());
assertEquals(2, first.get(0).getColDataType().getScale());
assertEquals("\"C\"", first.get(1).getColumnOptions().get(0).getCollation());
assertNull(table.getFunctions().get(1).getResultColumnDefinitions());
assertEquals(1, table.getFunctions().get(2).getResultColumnDefinitions().get(0)
.getColDataType().getArrayData().size());

first.get(0).setColumnName("changed");
first.get(0).setColDataType(new ColDataType("bigint"));
TableFunction reparsed = (TableFunction) parse(select.toString()).getFromItem();
assertEquals("changed", reparsed.getFunctions().get(0).getResultColumnDefinitions()
.get(0).getColumnName());
assertEquals("bigint", reparsed.getFunctions().get(0).getResultColumnDefinitions()
.get(0).getColDataType().getDataType());
}

@Test
void legacyFunctionListEditsKeepDefinitionsAttachedToTheirFunction()
throws JSQLParserException {
TableFunction table = (TableFunction) parse("SELECT * FROM ROWS FROM ("
+ "json_to_record('{}') AS (a integer), generate_series(1, 2))").getFromItem();
Function record = table.getRowsFromFunctions().remove(0);
table.getRowsFromFunctions().add(record);
TableFunction reparsed = (TableFunction) parse("SELECT * FROM " + table).getFromItem();
assertNull(reparsed.getFunctions().get(0).getResultColumnDefinitions());
assertNotNull(reparsed.getFunctions().get(1).getResultColumnDefinitions());
}

@Test
void tableFunctionRendererVisitsFunctionArguments() throws JSQLParserException {
PlainSelect select = parse("SELECT * FROM ROWS FROM (json_to_record('{}') AS (a integer), "
+ "generate_series(1, 2)) WITH ORDINALITY AS r(a, n, ord)");
String original = select.toString();
StringBuilder sql = new StringBuilder();
ExpressionDeParser expressions = new ExpressionDeParser() {
@Override
public <S> StringBuilder visit(LongValue value, S context) {
return getBuilder().append(value.getValue() + 10);
}
};
select.accept(new StatementDeParser(expressions, new SelectDeParser(), sql));
assertTrue(sql.toString().contains("generate_series(11, 12)"));
assertTrue(sql.toString().contains("AS (a integer)"));
assertEquals(original, select.toString());
assertEquals(sql.toString(), parse(sql.toString()).toString());
}

@Test
void tableDiscoveryStillVisitsSubqueriesInRecordFunctionArguments()
throws JSQLParserException {
String sql = "SELECT * FROM ROWS FROM ("
+ "json_to_record((SELECT payload FROM events)) AS (a integer))";
assertEquals(Set.of("events"), TablesNamesFinder.findTables(sql));
}

private static PlainSelect parse(String sql) throws JSQLParserException {
return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
true SELECT * FROM ROWS FROM (json_to_recordset('[{"a":1}]') AS (a integer)) WITH ORDINALITY
true SELECT * FROM ROWS FROM (json_to_recordset('[{"a":1,"b":"x"}]') AS (a integer, b text)) AS r
true SELECT * FROM ROWS FROM (json_to_recordset('[{"a":1}]') AS (a integer), generate_series(1, 2)) WITH ORDINALITY AS r(a, n, ord)
true SELECT * FROM ROWS FROM (generate_series(1, 2), jsonb_to_recordset('[{"a":1.25}]'::jsonb) AS (a numeric(10, 2))) AS r(n, amount)
true SELECT * FROM ROWS FROM (json_to_record('{"a":"x"}') AS (a text COLLATE "C"))
true SELECT * FROM ROWS FROM (json_to_record('{"a":"x"}') AS (a text COLLATE pg_catalog."C"))
true SELECT * FROM ROWS FROM (json_to_record('{"a":[1,2]}') AS (a integer[]))
true SELECT * FROM ROWS FROM (json_to_record('{"a":"2026-01-01T12:00:00Z"}') AS (a timestamp(6) with time zone))
true SELECT * FROM ROWS FROM (json_to_record('{"odd name":1}') AS ("odd name" integer)) AS r("renamed")
true SELECT r.* FROM (VALUES ('[{"a":1}]'::json)) AS src(payload) CROSS JOIN LATERAL ROWS FROM (json_to_recordset(src.payload) AS (a integer)) WITH ORDINALITY AS r
true SELECT * FROM ROWS FROM (generate_series(1, 2), generate_series(1, 1)) AS r(a, b)
true SELECT * FROM ROWS FROM (json_to_record('{"a":1}') AS (a integer), json_to_record('{"b":2}') AS (b integer))
false SELECT * FROM ROWS FROM (json_to_record('{}') AS ())
false SELECT * FROM ROWS FROM (json_to_record('{}') AS (a))
false SELECT * FROM ROWS FROM (json_to_record('{}') AS (a integer,))
false SELECT * FROM ROWS FROM (json_to_record('{}') AS (a integer NOT NULL))
false SELECT * FROM ROWS FROM (json_to_record('{}') AS (a integer DEFAULT 1))
false SELECT * FROM ROWS FROM (json_to_record('{}') AS r(a integer))
false SELECT * FROM ROWS FROM (json_to_record('{}') AS (a integer) WITH ORDINALITY)
false SELECT * FROM ROWS FROM ()
Loading