From 03900a23f20a349070af5125f821401a0df21dd8 Mon Sep 17 00:00:00 2001 From: youngjoonkim Date: Wed, 4 Feb 2026 17:42:32 +0900 Subject: [PATCH 1/2] feat: add postgresql named parameters support --- .../expression/ExpressionVisitor.java | 6 + .../expression/ExpressionVisitorAdapter.java | 5 + .../PostgresNamedFunctionParameter.java | 55 +++++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 6 + .../util/deparser/ExpressionDeParser.java | 10 ++ .../validator/ExpressionValidator.java | 7 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 19 ++++ .../PostgresNamedFunctionParameterTest.java | 106 ++++++++++++++++++ 8 files changed, 214 insertions(+) create mode 100644 src/main/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameter.java create mode 100644 src/test/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameterTest.java diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index e9b5f1b37..070592bc9 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -781,4 +781,10 @@ default void visit(Inverse inverse) { T visit(FromQuery fromQuery, S context); T visit(DateUnitExpression dateUnitExpression, S context); + + T visit(PostgresNamedFunctionParameter postgresNamedFunctionParameter, S context); + + default void visit(PostgresNamedFunctionParameter postgresNamedFunctionParameter) { + this.visit(postgresNamedFunctionParameter, null); + } } diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 88f92369b..39558d57a 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -743,6 +743,11 @@ public T visit(OracleNamedFunctionParameter oracleNamedFunctionParameter, S return oracleNamedFunctionParameter.getExpression().accept(this, context); } + @Override + public T visit(PostgresNamedFunctionParameter postgresNamedFunctionParameter, S context) { + return postgresNamedFunctionParameter.getExpression().accept(this, context); + } + @Override public T visit(GeometryDistance geometryDistance, S context) { return visitBinaryExpression(geometryDistance, context); diff --git a/src/main/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameter.java b/src/main/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameter.java new file mode 100644 index 000000000..573ad60f2 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameter.java @@ -0,0 +1,55 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +import java.util.Objects; + +/** + * @author Andreas Reichel + */ +public class PostgresNamedFunctionParameter extends ASTNodeAccessImpl implements Expression { + private final String name; + private final Expression expression; + + public PostgresNamedFunctionParameter(String name, Expression expression) { + this.name = Objects.requireNonNull(name, + "The NAME of the PostgresNamedFunctionParameter must not be null."); + this.expression = Objects.requireNonNull(expression, + "The EXPRESSION of the PostgresNamedFunctionParameter must not be null."); + } + + public String getName() { + return name; + } + + public Expression getExpression() { + return expression; + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append(name) + .append(" := ") + .append(expression); + + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index f61c02e70..a51da7f96 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -1758,6 +1758,12 @@ public Void visit(OracleNamedFunctionParameter oracleNamedFunctionParameter, return null; } + @Override + public Void visit(PostgresNamedFunctionParameter postgresNamedFunctionParameter, S context) { + postgresNamedFunctionParameter.getExpression().accept(this, context); + return null; + } + @Override public Void visit(RenameTableStatement renameTableStatement, S context) { for (Map.Entry e : renameTableStatement.getTableNames()) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index c97a28423..62a45feee 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -52,6 +52,7 @@ import net.sf.jsqlparser.expression.OracleHint; import net.sf.jsqlparser.expression.OracleNamedFunctionParameter; import net.sf.jsqlparser.expression.OverlapsCondition; +import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter; import net.sf.jsqlparser.expression.RangeExpression; import net.sf.jsqlparser.expression.RowConstructor; import net.sf.jsqlparser.expression.RowGetExpression; @@ -1835,4 +1836,13 @@ public StringBuilder visit(FromQuery fromQuery, S context) { public StringBuilder visit(DateUnitExpression dateUnitExpression, S context) { return builder.append(dateUnitExpression.toString()); } + + @Override + public StringBuilder visit(PostgresNamedFunctionParameter postgresNamedFunctionParameter, + S context) { + builder.append(postgresNamedFunctionParameter.getName()).append(" := "); + + postgresNamedFunctionParameter.getExpression().accept(this, context); + return builder; + } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java index 87f0205d8..34ae35247 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java @@ -51,6 +51,7 @@ import net.sf.jsqlparser.expression.OracleHint; import net.sf.jsqlparser.expression.OracleNamedFunctionParameter; import net.sf.jsqlparser.expression.OverlapsCondition; +import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter; import net.sf.jsqlparser.expression.RangeExpression; import net.sf.jsqlparser.expression.RowConstructor; import net.sf.jsqlparser.expression.RowGetExpression; @@ -1052,6 +1053,12 @@ public Void visit(OracleNamedFunctionParameter oracleNamedFunctionParameter, return null; } + @Override + public Void visit(PostgresNamedFunctionParameter postgresNamedFunctionParameter, S context) { + postgresNamedFunctionParameter.getExpression().accept(this, context); + return null; + } + @Override public Void visit(AllColumns allColumns, S context) { return null; diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 74d36e5a5..f5a1cbccb 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -515,6 +515,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */ | | | "> +| | | @@ -6224,6 +6225,8 @@ ExpressionList ComplexExpressionList(): ( LOOKAHEAD(2) expr=OracleNamedFunctionParameter() | + LOOKAHEAD(2) expr=PostgresNamedFunctionParameter() + | expr=Expression() ) { @@ -6235,6 +6238,8 @@ ExpressionList ComplexExpressionList(): ( LOOKAHEAD(2) expr=OracleNamedFunctionParameter() | + LOOKAHEAD(2) expr=PostgresNamedFunctionParameter() + | LOOKAHEAD(7) expr=LambdaExpression() | expr=Expression() @@ -6786,6 +6791,20 @@ OracleNamedFunctionParameter OracleNamedFunctionParameter() : { } } +PostgresNamedFunctionParameter PostgresNamedFunctionParameter() : { + Token token = null; + String name = null; + Expression expression; +} +{ + ( name=RelObjectNameExt2() | token= ) + + expression=Expression() + { + return new PostgresNamedFunctionParameter(name != null ? name : token.image, expression); + } +} + UserVariable UserVariable() : { Token tk; String varName; diff --git a/src/test/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameterTest.java b/src/test/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameterTest.java new file mode 100644 index 000000000..d49384dcc --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameterTest.java @@ -0,0 +1,106 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.test.TestUtils; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.validation.ValidationTestAsserts; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import net.sf.jsqlparser.util.validation.validator.ExpressionValidator; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * + * @author Andreas Reichel + */ +public class PostgresNamedFunctionParameterTest { + + /** + * This test will parse and deparse the statement and assures the functional coverage by + * JSQLParser. + * + * @throws JSQLParserException + */ + @Test + public void testExpression() throws JSQLParserException { + String sqlStr = + "select dy_api.test_func1('test_user', is_test := false) as col1"; + + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + sqlStr = "exec dbms_stats.gather_schema_stats(\n" + + " ownname := 'COMMON', \n" + + " estimate_percent := dbms_stats.auto_sample_size, \n" + + " method_opt := 'for all columns size auto', \n" + + " degree := DBMS_STATS.DEFAULT_DEGREE,\n" + + " cascade := DBMS_STATS.AUTO_CASCADE,\n" + + " options := 'GATHER AUTO'\n" + + " )"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + /** + * This test will trigger the method {@link ExpressionVisitorAdaptor#visit() Visit Method} in + * the ExpressionVisitorAdaptor needed for the Code Coverage. + * + * @throws JSQLParserException + */ + @Test + public void testExpressionVisitorAdaptor() throws JSQLParserException { + String sqlStr = + "select r.*, test.numeric_function ( p_1 := r.param1, p_2 := r.param2 ) as resultaat2"; + + CCJSqlParserUtil.parse(sqlStr).accept(new StatementVisitorAdapter()); + + // alternatively, for the Expression only + CCJSqlParserUtil.parseExpression("p_1 := r.param1").accept(new ExpressionVisitorAdapter(), + null); + } + + /** + * This test will trigger the method {@link TableNamesFinder#visit() Visit Method} in the + * TableNamesFinder needed for the Code Coverage. + * + * @throws JSQLParserException + */ + @Test + public void testTableNamesFinder() throws JSQLParserException { + String sqlStr = + "select r.*, test.numeric_function ( p_1 := r.param1, p_2 := r.param2 ) as resultaat2 from test_table"; + + Statement statement = CCJSqlParserUtil.parse(sqlStr); + List tables = new TablesNamesFinder<>().getTableList(statement); + assertEquals(1, tables.size()); + assertTrue(tables.contains("test_table")); + } + + /** + * This test will trigger the method {@link ExpressionValidator#visit() Visit Method} in the + * ExpressionValidator needed for the Code Coverage. + * + * @throws JSQLParserException + */ + @Test + public void testValidator() throws JSQLParserException { + String sqlStr = + "select r.*, test.numeric_function ( p_1 := r.param1, p_2 := r.param2 ) as resultaat2"; + + ValidationTestAsserts.validateNoErrors(sqlStr, 1, DatabaseType.POSTGRESQL); + } +} From ab33f169627082921cc8909b507adaca28227d93 Mon Sep 17 00:00:00 2001 From: youngjoonkim Date: Fri, 6 Feb 2026 10:01:50 +0900 Subject: [PATCH 2/2] modify: PostgresNamedFunctionParameterTest.java --- .../PostgresNamedFunctionParameterTest.java | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/test/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameterTest.java b/src/test/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameterTest.java index d49384dcc..c3e7af109 100644 --- a/src/test/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameterTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameterTest.java @@ -40,19 +40,9 @@ public class PostgresNamedFunctionParameterTest { @Test public void testExpression() throws JSQLParserException { String sqlStr = - "select dy_api.test_func1('test_user', is_test := false) as col1"; + "SELECT concat_lower_or_upper(a := 'Hello', uppercase := true, b := 'World')"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); - - sqlStr = "exec dbms_stats.gather_schema_stats(\n" - + " ownname := 'COMMON', \n" - + " estimate_percent := dbms_stats.auto_sample_size, \n" - + " method_opt := 'for all columns size auto', \n" - + " degree := DBMS_STATS.DEFAULT_DEGREE,\n" - + " cascade := DBMS_STATS.AUTO_CASCADE,\n" - + " options := 'GATHER AUTO'\n" - + " )"; - TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } /** @@ -64,12 +54,12 @@ public void testExpression() throws JSQLParserException { @Test public void testExpressionVisitorAdaptor() throws JSQLParserException { String sqlStr = - "select r.*, test.numeric_function ( p_1 := r.param1, p_2 := r.param2 ) as resultaat2"; + "SELECT concat_lower_or_upper(a := 'Hello', uppercase := true, b := 'World')"; CCJSqlParserUtil.parse(sqlStr).accept(new StatementVisitorAdapter()); // alternatively, for the Expression only - CCJSqlParserUtil.parseExpression("p_1 := r.param1").accept(new ExpressionVisitorAdapter(), + CCJSqlParserUtil.parseExpression("a := 'Hello'").accept(new ExpressionVisitorAdapter(), null); } @@ -82,7 +72,7 @@ public void testExpressionVisitorAdaptor() throws JSQLParserException { @Test public void testTableNamesFinder() throws JSQLParserException { String sqlStr = - "select r.*, test.numeric_function ( p_1 := r.param1, p_2 := r.param2 ) as resultaat2 from test_table"; + "SELECT concat_lower_or_upper(a := 'Hello', uppercase := true, b := 'World') FROM test_table"; Statement statement = CCJSqlParserUtil.parse(sqlStr); List tables = new TablesNamesFinder<>().getTableList(statement); @@ -99,7 +89,7 @@ public void testTableNamesFinder() throws JSQLParserException { @Test public void testValidator() throws JSQLParserException { String sqlStr = - "select r.*, test.numeric_function ( p_1 := r.param1, p_2 := r.param2 ) as resultaat2"; + "SELECT concat_lower_or_upper(a := 'Hello', uppercase := true, b := 'World') FROM test_table"; ValidationTestAsserts.validateNoErrors(sqlStr, 1, DatabaseType.POSTGRESQL); }