Skip to content

Commit 1e648c9

Browse files
committed
feat: parse PostgreSQL DISCARD, LISTEN, UNLISTEN, SHOW phrases and pre-9.0 COPY options
DISCARD, LISTEN and UNLISTEN failed, SHOW TIME ZONE, SHOW TRANSACTION ISOLATION LEVEL and SHOW SESSION AUTHORIZATION were unsupported statements, and COPY rejected the option syntax from before 9.0 that PostgreSQL 18 still accepts, such as COPY t TO STDOUT CSV HEADER, and COPY BINARY t. DISCARD parses to DiscardStatement with a Target enum, and LISTEN and UNLISTEN to ListenStatement next to NotifyStatement; LISTEN and UNLISTEN become non-reserved keywords. SET, RESET and SHOW read TIME ZONE and PostgreSQL's TRANSACTION ISOLATION LEVEL and SESSION AUTHORIZATION through one ConfigurationParameterName() rule: the SHOW phrases parse to ShowStatement and RESET takes them too. CopyStatement keeps the options in parentheses in getOptions() and adds getLegacyOptions(), a typed CopyLegacyOption per option in source order, and isBinary(). The forms are read with Dialect.POSTGRESQL and without a dialect; other dialects are unchanged. The statements are wired into the deparser, visitors, table finder, validation (discard, listen, and show for PostgreSQL) and feature analysis. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 12548b9 commit 1e648c9

28 files changed

Lines changed: 1559 additions & 12 deletions

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
150150
| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` |
151151
| **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` |
152152
| **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` |
153+
| **PostgreSQL session statements** | `DISCARD` · `LISTEN` · `UNLISTEN` · `SHOW`/`RESET TIME ZONE`/`TRANSACTION ISOLATION LEVEL`/`SESSION AUTHORIZATION` · `COPY` options from before 9.0 |
153154
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
154155
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |
155156
| **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` |

‎src/main/java/net/sf/jsqlparser/parser/feature/Feature.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,18 @@ public enum Feature {
930930
* @see ResetStatement
931931
*/
932932
reset,
933+
/**
934+
* PostgreSQL "DISCARD" statement is allowed
935+
*
936+
* @see net.sf.jsqlparser.statement.DiscardStatement
937+
*/
938+
discard,
939+
/**
940+
* PostgreSQL "LISTEN" and "UNLISTEN" statements are allowed
941+
*
942+
* @see net.sf.jsqlparser.statement.notify.ListenStatement
943+
*/
944+
listen,
933945
/**
934946
* @see Pivot
935947
*/
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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 java.util.ArrayList;
13+
import java.util.Collection;
14+
import java.util.Collections;
15+
import java.util.List;
16+
import java.util.Objects;
17+
import net.sf.jsqlparser.expression.StringValue;
18+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
19+
import net.sf.jsqlparser.schema.Column;
20+
21+
/**
22+
* One option of the COPY option syntax PostgreSQL used before 9.0 and still accepts: a keyword
23+
* without parentheses or commas, such as {@code CSV}, {@code HEADER}, {@code DELIMITER AS ';'} or
24+
* {@code FORCE QUOTE *}. The options follow the file name and the optional {@code WITH} in any
25+
* order, as {@code copy_opt_item} in PostgreSQL's grammar.
26+
*
27+
* @see <a href="https://www.postgresql.org/docs/18/sql-copy.html">COPY, section Compatibility</a>
28+
*/
29+
public class CopyLegacyOption extends ASTNodeAccessImpl {
30+
private Kind kind;
31+
private boolean asKeyword;
32+
private StringValue value;
33+
private final List<Column> columns = new ArrayList<>();
34+
private boolean allColumns;
35+
36+
/** The option keyword, and what it takes after it. */
37+
public enum Kind {
38+
/** BINARY, the binary format */
39+
BINARY("BINARY", Argument.NONE),
40+
/** FREEZE */
41+
FREEZE("FREEZE", Argument.NONE),
42+
/** DELIMITER [AS] 'string' */
43+
DELIMITER("DELIMITER", Argument.STRING_AFTER_OPTIONAL_AS),
44+
/** NULL [AS] 'string' */
45+
NULL("NULL", Argument.STRING_AFTER_OPTIONAL_AS),
46+
/** CSV, the CSV format */
47+
CSV("CSV", Argument.NONE),
48+
/** HEADER */
49+
HEADER("HEADER", Argument.NONE),
50+
/** QUOTE [AS] 'string' */
51+
QUOTE("QUOTE", Argument.STRING_AFTER_OPTIONAL_AS),
52+
/** ESCAPE [AS] 'string' */
53+
ESCAPE("ESCAPE", Argument.STRING_AFTER_OPTIONAL_AS),
54+
/** FORCE QUOTE {column, ... | *} */
55+
FORCE_QUOTE("FORCE QUOTE", Argument.COLUMNS),
56+
/** FORCE NOT NULL {column, ... | *} */
57+
FORCE_NOT_NULL("FORCE NOT NULL", Argument.COLUMNS),
58+
/** FORCE NULL {column, ... | *} */
59+
FORCE_NULL("FORCE NULL", Argument.COLUMNS),
60+
/** ENCODING 'string' */
61+
ENCODING("ENCODING", Argument.STRING);
62+
63+
private final String keyword;
64+
private final Argument argument;
65+
66+
Kind(String keyword, Argument argument) {
67+
this.keyword = keyword;
68+
this.argument = argument;
69+
}
70+
71+
/** The keywords of the option, such as {@code FORCE NOT NULL}. */
72+
public String getKeyword() {
73+
return keyword;
74+
}
75+
76+
/** Whether a string follows the keyword. */
77+
public boolean takesString() {
78+
return argument == Argument.STRING || argument == Argument.STRING_AFTER_OPTIONAL_AS;
79+
}
80+
81+
/** Whether {@code AS} may precede the string. */
82+
public boolean allowsAsKeyword() {
83+
return argument == Argument.STRING_AFTER_OPTIONAL_AS;
84+
}
85+
86+
/** Whether a column list or {@code *} follows the keyword. */
87+
public boolean takesColumns() {
88+
return argument == Argument.COLUMNS;
89+
}
90+
}
91+
92+
private enum Argument {
93+
NONE, STRING, STRING_AFTER_OPTIONAL_AS, COLUMNS
94+
}
95+
96+
public CopyLegacyOption(Kind kind) {
97+
setKind(kind);
98+
}
99+
100+
public Kind getKind() {
101+
return kind;
102+
}
103+
104+
public void setKind(Kind kind) {
105+
this.kind = Objects.requireNonNull(kind, "kind");
106+
}
107+
108+
/** Whether {@code AS} precedes the string, as in {@code DELIMITER AS ','}. */
109+
public boolean isAsKeyword() {
110+
return asKeyword;
111+
}
112+
113+
public void setAsKeyword(boolean asKeyword) {
114+
this.asKeyword = asKeyword;
115+
}
116+
117+
public CopyLegacyOption withAsKeyword(boolean asKeyword) {
118+
setAsKeyword(asKeyword);
119+
return this;
120+
}
121+
122+
/** The string of DELIMITER, NULL, QUOTE, ESCAPE or ENCODING; null for the other options. */
123+
public StringValue getValue() {
124+
return value;
125+
}
126+
127+
public void setValue(StringValue value) {
128+
this.value = value;
129+
}
130+
131+
public CopyLegacyOption withValue(StringValue value) {
132+
setValue(value);
133+
return this;
134+
}
135+
136+
/** The columns of a FORCE option; empty for {@code *} and for the other options. */
137+
public List<Column> getColumns() {
138+
return columns;
139+
}
140+
141+
public void setColumns(Collection<? extends Column> columns) {
142+
List<Column> replacement =
143+
columns == null ? Collections.emptyList() : new ArrayList<>(columns);
144+
this.columns.clear();
145+
this.columns.addAll(replacement);
146+
}
147+
148+
public CopyLegacyOption addColumns(Column... columns) {
149+
Collections.addAll(this.columns, columns);
150+
return this;
151+
}
152+
153+
public CopyLegacyOption withColumns(Collection<? extends Column> columns) {
154+
setColumns(columns);
155+
return this;
156+
}
157+
158+
/** Whether a FORCE option applies to every column, written {@code *}. */
159+
public boolean isAllColumns() {
160+
return allColumns;
161+
}
162+
163+
public void setAllColumns(boolean allColumns) {
164+
this.allColumns = allColumns;
165+
}
166+
167+
public CopyLegacyOption withAllColumns(boolean allColumns) {
168+
setAllColumns(allColumns);
169+
return this;
170+
}
171+
172+
@Override
173+
public StringBuilder appendTo(StringBuilder builder) {
174+
builder.append(kind.getKeyword());
175+
if (value != null) {
176+
builder.append(asKeyword ? " AS " : " ").append(value);
177+
}
178+
if (allColumns) {
179+
builder.append(" *");
180+
} else {
181+
for (int i = 0; i < columns.size(); i++) {
182+
builder.append(i == 0 ? " " : ", ").append(columns.get(i).getFullyQualifiedName());
183+
}
184+
}
185+
return builder;
186+
}
187+
188+
@Override
189+
public String toString() {
190+
return appendTo(new StringBuilder()).toString();
191+
}
192+
}

‎src/main/java/net/sf/jsqlparser/statement/CopyStatement.java‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,21 @@
1616
import net.sf.jsqlparser.statement.select.PlainSelect;
1717
import net.sf.jsqlparser.statement.select.Select;
1818

19+
import java.util.ArrayList;
20+
import java.util.Collection;
21+
import java.util.Collections;
1922
import java.util.List;
2023
import java.util.function.Consumer;
2124

2225
/**
2326
* {@code COPY table [(columns)] FROM path [(options)]} and
2427
* {@code COPY {table | (query)} TO path [(options)]}, the bulk import and export statement in
25-
* DuckDB and PostgreSQL.
28+
* DuckDB and PostgreSQL. PostgreSQL also accepts its option syntax from before 9.0, options without
29+
* parentheses such as {@code CSV HEADER} (see {@link #getLegacyOptions()}), and
30+
* {@code COPY BINARY table}.
2631
*
2732
* @see <a href="https://duckdb.org/docs/stable/sql/statements/copy">COPY</a>
33+
* @see <a href="https://www.postgresql.org/docs/18/sql-copy.html">PostgreSQL COPY</a>
2834
*/
2935
public class CopyStatement extends ASTNodeAccessImpl implements Statement {
3036
private Table table;
@@ -34,6 +40,8 @@ public class CopyStatement extends ASTNodeAccessImpl implements Statement {
3440
private String path;
3541
private List<String> options;
3642
private boolean withKeyword;
43+
private boolean binary;
44+
private final List<CopyLegacyOption> legacyOptions = new ArrayList<>();
3745

3846
public Table getTable() {
3947
return table;
@@ -101,6 +109,7 @@ public CopyStatement withPath(String path) {
101109
return this;
102110
}
103111

112+
/** The options in parentheses, each as written; null when the statement has none. */
104113
public List<String> getOptions() {
105114
return options;
106115
}
@@ -128,6 +137,47 @@ public CopyStatement withWithKeyword(boolean withKeyword) {
128137
return this;
129138
}
130139

140+
/** Whether the table is preceded by BINARY, PostgreSQL's spelling from before 7.3. */
141+
public boolean isBinary() {
142+
return binary;
143+
}
144+
145+
public void setBinary(boolean binary) {
146+
this.binary = binary;
147+
}
148+
149+
public CopyStatement withBinary(boolean binary) {
150+
setBinary(binary);
151+
return this;
152+
}
153+
154+
/**
155+
* The options of PostgreSQL's syntax from before 9.0, in source order, such as {@code CSV} and
156+
* {@code HEADER} in {@code COPY t TO STDOUT CSV HEADER}; empty when the statement has none. A
157+
* statement has either these options or the options in parentheses.
158+
*/
159+
public List<CopyLegacyOption> getLegacyOptions() {
160+
return legacyOptions;
161+
}
162+
163+
public void setLegacyOptions(Collection<? extends CopyLegacyOption> legacyOptions) {
164+
List<CopyLegacyOption> replacement = legacyOptions == null
165+
? Collections.emptyList()
166+
: new ArrayList<>(legacyOptions);
167+
this.legacyOptions.clear();
168+
this.legacyOptions.addAll(replacement);
169+
}
170+
171+
public CopyStatement addLegacyOptions(CopyLegacyOption... legacyOptions) {
172+
Collections.addAll(this.legacyOptions, legacyOptions);
173+
return this;
174+
}
175+
176+
public CopyStatement withLegacyOptions(Collection<? extends CopyLegacyOption> legacyOptions) {
177+
setLegacyOptions(legacyOptions);
178+
return this;
179+
}
180+
131181
public StringBuilder appendTo(StringBuilder builder) {
132182
return appendTo(builder, builder::append);
133183
}
@@ -140,6 +190,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Select> selectPrin
140190
selectPrinter.accept(select);
141191
builder.append(')');
142192
} else {
193+
if (binary) {
194+
builder.append("BINARY ");
195+
}
143196
builder.append(table);
144197
if (columns != null && !columns.isEmpty()) {
145198
builder.append(" (").append(columns).append(")");
@@ -149,6 +202,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Select> selectPrin
149202
if (withKeyword) {
150203
builder.append(" WITH");
151204
}
205+
for (CopyLegacyOption legacyOption : legacyOptions) {
206+
legacyOption.appendTo(builder.append(' '));
207+
}
152208
if (options != null && !options.isEmpty()) {
153209
builder.append(" ").append(PlainSelect.getStringList(options, true, true));
154210
}

0 commit comments

Comments
 (0)