Skip to content

Commit e5b50ce

Browse files
committed
feat: support PostgreSQL PREPARE parameter types
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 2fc6e3e commit e5b50ce

4 files changed

Lines changed: 142 additions & 3 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/PrepareStatement.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@
99
*/
1010
package net.sf.jsqlparser.statement;
1111

12+
import java.util.List;
13+
import java.util.function.Consumer;
14+
import net.sf.jsqlparser.statement.create.table.ColDataType;
15+
import net.sf.jsqlparser.statement.select.PlainSelect;
16+
1217
/**
13-
* {@code PREPARE name AS statement}, which stores a parameterised statement for later
18+
* {@code PREPARE name [(types)] AS statement}, which stores a parameterised statement for later
1419
* {@code EXECUTE}.
1520
*
1621
* @see <a href="https://duckdb.org/docs/stable/sql/query_syntax/prepared_statements">Prepared
@@ -19,6 +24,7 @@
1924
public class PrepareStatement implements Statement {
2025
private String name;
2126
private Statement statement;
27+
private List<ColDataType> parameterTypes;
2228

2329
public PrepareStatement() {}
2430

@@ -53,8 +59,32 @@ public PrepareStatement withStatement(Statement statement) {
5359
return this;
5460
}
5561

62+
/** Returns declared parameter types, or {@code null} when types are inferred. */
63+
public List<ColDataType> getParameterTypes() {
64+
return parameterTypes;
65+
}
66+
67+
public void setParameterTypes(List<ColDataType> parameterTypes) {
68+
this.parameterTypes = parameterTypes;
69+
}
70+
71+
public PrepareStatement withParameterTypes(List<ColDataType> parameterTypes) {
72+
setParameterTypes(parameterTypes);
73+
return this;
74+
}
75+
5676
public StringBuilder appendTo(StringBuilder builder) {
57-
builder.append("PREPARE ").append(name).append(" AS ").append(statement);
77+
return appendTo(builder, builder::append);
78+
}
79+
80+
/** Renders the nested statement through the caller's statement writer. */
81+
public StringBuilder appendTo(StringBuilder builder, Consumer<Statement> statementPrinter) {
82+
builder.append("PREPARE ").append(name);
83+
if (parameterTypes != null && !parameterTypes.isEmpty()) {
84+
builder.append(PlainSelect.getStringList(parameterTypes, true, true));
85+
}
86+
builder.append(" AS ");
87+
statementPrinter.accept(statement);
5888
return builder;
5989
}
6090

src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ public <S> StringBuilder visit(DisconnectStatement disconnectStatement, S contex
612612

613613
@Override
614614
public <S> StringBuilder visit(PrepareStatement prepareStatement, S context) {
615-
prepareStatement.appendTo(builder);
615+
prepareStatement.appendTo(builder, statement -> statement.accept(this, context));
616616
return builder;
617617
}
618618

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4701,14 +4701,28 @@ PrepareStatement PrepareStatement() #PrepareStatement:
47014701
PrepareStatement prepareStatement = new PrepareStatement();
47024702
ObjectNames name;
47034703
Statement statement;
4704+
List<ColDataType> parameterTypes = new ArrayList<ColDataType>();
4705+
ColDataType parameterType;
47044706
}
47054707
{
47064708
<K_PREPARE> name=RelObjectNames()
47074709
{ prepareStatement.setName(String.join(".", name.getNames())); }
4710+
[ "(" parameterType=PrepareParameterType() { parameterTypes.add(parameterType); }
4711+
( "," parameterType=PrepareParameterType() { parameterTypes.add(parameterType); } )*
4712+
")" { prepareStatement.setParameterTypes(parameterTypes); } ]
47084713
<K_AS> statement=SingleStatement() { prepareStatement.setStatement(statement); }
47094714
{ return prepareStatement; }
47104715
}
47114716

4717+
/** PostgreSQL UNKNOWN leaves a parameter type to be inferred from the prepared statement. */
4718+
ColDataType PrepareParameterType():
4719+
{ ColDataType type; Token token; }
4720+
{
4721+
( token=<K_UNKNOWN> { type = new ColDataType(token.image); }
4722+
| type=ColDataType() )
4723+
{ return type; }
4724+
}
4725+
47124726
/**
47134727
* DEALLOCATE [PREPARE] name.
47144728
*/
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
import static org.junit.jupiter.api.Assertions.assertNull;
14+
import static org.junit.jupiter.api.Assertions.assertThrows;
15+
16+
import net.sf.jsqlparser.JSQLParserException;
17+
import net.sf.jsqlparser.expression.LongValue;
18+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
19+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
20+
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
21+
import net.sf.jsqlparser.util.deparser.SelectDeParser;
22+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.ValueSource;
26+
27+
class PostgreSqlPrepareTest {
28+
@ParameterizedTest
29+
@ValueSource(strings = {
30+
"PREPARE p(bigint) AS SELECT * FROM users WHERE id = $1",
31+
"PREPARE p(bigint[], text) AS SELECT * FROM users WHERE id = ANY($1) AND name = $2",
32+
"PREPARE p(pg_catalog.int4, numeric(10, 2)) AS SELECT $1 + $2",
33+
"PREPARE \"prepared query\"(timestamp with time zone, double precision) AS SELECT $1, $2",
34+
"PREPARE p(unknown) AS SELECT $1::text",
35+
"PREPARE p(bigint, text) AS INSERT INTO users(id, name) VALUES ($1, $2)",
36+
"PREPARE p(text, bigint) AS UPDATE users SET name = $1 WHERE id = $2",
37+
"PREPARE p(bigint) AS DELETE FROM users WHERE id = $1",
38+
"PREPARE p AS SELECT * FROM users WHERE id = $1"
39+
})
40+
void preservesDeclaredTypesAndNestedStatements(String sql) throws JSQLParserException {
41+
PrepareStatement prepare = parse(sql);
42+
assertRoundTrip(prepare);
43+
assertEquals(prepare.toString(), CCJSqlParserUtil.parse(prepare.toString()).toString());
44+
}
45+
46+
@Test
47+
void exposesMutableTypesAndRetainsInferredForm() throws JSQLParserException {
48+
PrepareStatement prepare = parse("PREPARE p(bigint, text) AS SELECT $1, $2");
49+
assertEquals(2, prepare.getParameterTypes().size());
50+
assertEquals("bigint", prepare.getParameterTypes().get(0).getBaseTypeName());
51+
prepare.getParameterTypes().get(0).setDataType("integer");
52+
assertEquals("PREPARE p(integer, text) AS SELECT $1, $2", prepare.toString());
53+
assertRoundTrip(prepare);
54+
prepare.setParameterTypes(null);
55+
assertEquals("PREPARE p AS SELECT $1, $2", prepare.toString());
56+
assertNull(parse(prepare.toString()).getParameterTypes());
57+
assertRoundTrip(new PrepareStatement("p", prepare.getStatement()));
58+
}
59+
60+
@Test
61+
void passesNestedExpressionsToTheConfiguredDeparser() throws JSQLParserException {
62+
PrepareStatement prepare =
63+
parse("PREPARE p(bigint) AS SELECT $1 + 7 FROM users WHERE id > 8");
64+
StringBuilder buffer = new StringBuilder();
65+
ExpressionDeParser expressions = new ExpressionDeParser() {
66+
@Override
67+
public <S> StringBuilder visit(LongValue value, S context) {
68+
return getBuilder().append(value.getValue() + 100);
69+
}
70+
};
71+
prepare.accept(new StatementDeParser(expressions, new SelectDeParser(), buffer), null);
72+
assertEquals("PREPARE p(bigint) AS SELECT $1 + 107 FROM users WHERE id > 108",
73+
buffer.toString());
74+
assertRoundTrip(parse(buffer.toString()));
75+
}
76+
77+
@ParameterizedTest
78+
@ValueSource(strings = {"PREPARE p() AS SELECT 1", "PREPARE p(bigint,) AS SELECT $1",
79+
"PREPARE p(bigint text) AS SELECT $1", "PREPARE p(bigint) SELECT $1"})
80+
void rejectsMalformedTypeLists(String sql) {
81+
assertThrows(JSQLParserException.class, () -> parse(sql));
82+
}
83+
84+
private static PrepareStatement parse(String sql) throws JSQLParserException {
85+
return (PrepareStatement) CCJSqlParserUtil.parse(sql,
86+
p -> p.withDialect(Dialect.POSTGRESQL));
87+
}
88+
89+
private static void assertRoundTrip(PrepareStatement prepare) throws JSQLParserException {
90+
StringBuilder buffer = new StringBuilder();
91+
prepare.accept(new StatementDeParser(buffer), null);
92+
assertEquals(prepare.toString(), buffer.toString());
93+
assertEquals(prepare.toString(), parse(buffer.toString()).toString());
94+
}
95+
}

0 commit comments

Comments
 (0)