diff --git a/README.md b/README.md
index e7a8db452..8f8361505 100644
--- a/README.md
+++ b/README.md
@@ -150,6 +150,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` |
| **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` |
| **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` |
+| **PostgreSQL session statements** | `DISCARD` · `LISTEN` · `UNLISTEN` · `SHOW`/`RESET TIME ZONE`/`TRANSACTION ISOLATION LEVEL`/`SESSION AUTHORIZATION` · `COPY` options from before 9.0 |
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |
| **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` |
diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
index dc3d137e3..de8355e11 100644
--- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
+++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
@@ -930,6 +930,18 @@ public enum Feature {
* @see ResetStatement
*/
reset,
+ /**
+ * PostgreSQL "DISCARD" statement is allowed
+ *
+ * @see net.sf.jsqlparser.statement.DiscardStatement
+ */
+ discard,
+ /**
+ * PostgreSQL "LISTEN" and "UNLISTEN" statements are allowed
+ *
+ * @see net.sf.jsqlparser.statement.notify.ListenStatement
+ */
+ listen,
/**
* @see Pivot
*/
diff --git a/src/main/java/net/sf/jsqlparser/statement/CopyLegacyOption.java b/src/main/java/net/sf/jsqlparser/statement/CopyLegacyOption.java
new file mode 100644
index 000000000..cbeafc4d6
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/CopyLegacyOption.java
@@ -0,0 +1,192 @@
+/*-
+ * #%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;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import net.sf.jsqlparser.expression.StringValue;
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+import net.sf.jsqlparser.schema.Column;
+
+/**
+ * One option of the COPY option syntax PostgreSQL used before 9.0 and still accepts: a keyword
+ * without parentheses or commas, such as {@code CSV}, {@code HEADER}, {@code DELIMITER AS ';'} or
+ * {@code FORCE QUOTE *}. The options follow the file name and the optional {@code WITH} in any
+ * order, as {@code copy_opt_item} in PostgreSQL's grammar.
+ *
+ * @see COPY, section Compatibility
+ */
+public class CopyLegacyOption extends ASTNodeAccessImpl {
+ private Kind kind;
+ private boolean asKeyword;
+ private StringValue value;
+ private final List columns = new ArrayList<>();
+ private boolean allColumns;
+
+ /** The option keyword, and what it takes after it. */
+ public enum Kind {
+ /** BINARY, the binary format */
+ BINARY("BINARY", Argument.NONE),
+ /** FREEZE */
+ FREEZE("FREEZE", Argument.NONE),
+ /** DELIMITER [AS] 'string' */
+ DELIMITER("DELIMITER", Argument.STRING_AFTER_OPTIONAL_AS),
+ /** NULL [AS] 'string' */
+ NULL("NULL", Argument.STRING_AFTER_OPTIONAL_AS),
+ /** CSV, the CSV format */
+ CSV("CSV", Argument.NONE),
+ /** HEADER */
+ HEADER("HEADER", Argument.NONE),
+ /** QUOTE [AS] 'string' */
+ QUOTE("QUOTE", Argument.STRING_AFTER_OPTIONAL_AS),
+ /** ESCAPE [AS] 'string' */
+ ESCAPE("ESCAPE", Argument.STRING_AFTER_OPTIONAL_AS),
+ /** FORCE QUOTE {column, ... | *} */
+ FORCE_QUOTE("FORCE QUOTE", Argument.COLUMNS),
+ /** FORCE NOT NULL {column, ... | *} */
+ FORCE_NOT_NULL("FORCE NOT NULL", Argument.COLUMNS),
+ /** FORCE NULL {column, ... | *} */
+ FORCE_NULL("FORCE NULL", Argument.COLUMNS),
+ /** ENCODING 'string' */
+ ENCODING("ENCODING", Argument.STRING);
+
+ private final String keyword;
+ private final Argument argument;
+
+ Kind(String keyword, Argument argument) {
+ this.keyword = keyword;
+ this.argument = argument;
+ }
+
+ /** The keywords of the option, such as {@code FORCE NOT NULL}. */
+ public String getKeyword() {
+ return keyword;
+ }
+
+ /** Whether a string follows the keyword. */
+ public boolean takesString() {
+ return argument == Argument.STRING || argument == Argument.STRING_AFTER_OPTIONAL_AS;
+ }
+
+ /** Whether {@code AS} may precede the string. */
+ public boolean allowsAsKeyword() {
+ return argument == Argument.STRING_AFTER_OPTIONAL_AS;
+ }
+
+ /** Whether a column list or {@code *} follows the keyword. */
+ public boolean takesColumns() {
+ return argument == Argument.COLUMNS;
+ }
+ }
+
+ private enum Argument {
+ NONE, STRING, STRING_AFTER_OPTIONAL_AS, COLUMNS
+ }
+
+ public CopyLegacyOption(Kind kind) {
+ setKind(kind);
+ }
+
+ public Kind getKind() {
+ return kind;
+ }
+
+ public void setKind(Kind kind) {
+ this.kind = Objects.requireNonNull(kind, "kind");
+ }
+
+ /** Whether {@code AS} precedes the string, as in {@code DELIMITER AS ','}. */
+ public boolean isAsKeyword() {
+ return asKeyword;
+ }
+
+ public void setAsKeyword(boolean asKeyword) {
+ this.asKeyword = asKeyword;
+ }
+
+ public CopyLegacyOption withAsKeyword(boolean asKeyword) {
+ setAsKeyword(asKeyword);
+ return this;
+ }
+
+ /** The string of DELIMITER, NULL, QUOTE, ESCAPE or ENCODING; null for the other options. */
+ public StringValue getValue() {
+ return value;
+ }
+
+ public void setValue(StringValue value) {
+ this.value = value;
+ }
+
+ public CopyLegacyOption withValue(StringValue value) {
+ setValue(value);
+ return this;
+ }
+
+ /** The columns of a FORCE option; empty for {@code *} and for the other options. */
+ public List getColumns() {
+ return columns;
+ }
+
+ public void setColumns(Collection extends Column> columns) {
+ List replacement =
+ columns == null ? Collections.emptyList() : new ArrayList<>(columns);
+ this.columns.clear();
+ this.columns.addAll(replacement);
+ }
+
+ public CopyLegacyOption addColumns(Column... columns) {
+ Collections.addAll(this.columns, columns);
+ return this;
+ }
+
+ public CopyLegacyOption withColumns(Collection extends Column> columns) {
+ setColumns(columns);
+ return this;
+ }
+
+ /** Whether a FORCE option applies to every column, written {@code *}. */
+ public boolean isAllColumns() {
+ return allColumns;
+ }
+
+ public void setAllColumns(boolean allColumns) {
+ this.allColumns = allColumns;
+ }
+
+ public CopyLegacyOption withAllColumns(boolean allColumns) {
+ setAllColumns(allColumns);
+ return this;
+ }
+
+ @Override
+ public StringBuilder appendTo(StringBuilder builder) {
+ builder.append(kind.getKeyword());
+ if (value != null) {
+ builder.append(asKeyword ? " AS " : " ").append(value);
+ }
+ if (allColumns) {
+ builder.append(" *");
+ } else {
+ for (int i = 0; i < columns.size(); i++) {
+ builder.append(i == 0 ? " " : ", ").append(columns.get(i).getFullyQualifiedName());
+ }
+ }
+ return builder;
+ }
+
+ @Override
+ public String toString() {
+ return appendTo(new StringBuilder()).toString();
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/CopyStatement.java b/src/main/java/net/sf/jsqlparser/statement/CopyStatement.java
index 2697677db..e25314368 100644
--- a/src/main/java/net/sf/jsqlparser/statement/CopyStatement.java
+++ b/src/main/java/net/sf/jsqlparser/statement/CopyStatement.java
@@ -16,15 +16,21 @@
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
/**
* {@code COPY table [(columns)] FROM path [(options)]} and
* {@code COPY {table | (query)} TO path [(options)]}, the bulk import and export statement in
- * DuckDB and PostgreSQL.
+ * DuckDB and PostgreSQL. PostgreSQL also accepts its option syntax from before 9.0, options without
+ * parentheses such as {@code CSV HEADER} (see {@link #getLegacyOptions()}), and
+ * {@code COPY BINARY table}.
*
* @see COPY
+ * @see PostgreSQL COPY
*/
public class CopyStatement extends ASTNodeAccessImpl implements Statement {
private Table table;
@@ -34,6 +40,8 @@ public class CopyStatement extends ASTNodeAccessImpl implements Statement {
private String path;
private List options;
private boolean withKeyword;
+ private boolean binary;
+ private final List legacyOptions = new ArrayList<>();
public Table getTable() {
return table;
@@ -101,6 +109,7 @@ public CopyStatement withPath(String path) {
return this;
}
+ /** The options in parentheses, each as written; null when the statement has none. */
public List getOptions() {
return options;
}
@@ -128,6 +137,47 @@ public CopyStatement withWithKeyword(boolean withKeyword) {
return this;
}
+ /** Whether the table is preceded by BINARY, PostgreSQL's spelling from before 7.3. */
+ public boolean isBinary() {
+ return binary;
+ }
+
+ public void setBinary(boolean binary) {
+ this.binary = binary;
+ }
+
+ public CopyStatement withBinary(boolean binary) {
+ setBinary(binary);
+ return this;
+ }
+
+ /**
+ * The options of PostgreSQL's syntax from before 9.0, in source order, such as {@code CSV} and
+ * {@code HEADER} in {@code COPY t TO STDOUT CSV HEADER}; empty when the statement has none. A
+ * statement has either these options or the options in parentheses.
+ */
+ public List getLegacyOptions() {
+ return legacyOptions;
+ }
+
+ public void setLegacyOptions(Collection extends CopyLegacyOption> legacyOptions) {
+ List replacement = legacyOptions == null
+ ? Collections.emptyList()
+ : new ArrayList<>(legacyOptions);
+ this.legacyOptions.clear();
+ this.legacyOptions.addAll(replacement);
+ }
+
+ public CopyStatement addLegacyOptions(CopyLegacyOption... legacyOptions) {
+ Collections.addAll(this.legacyOptions, legacyOptions);
+ return this;
+ }
+
+ public CopyStatement withLegacyOptions(Collection extends CopyLegacyOption> legacyOptions) {
+ setLegacyOptions(legacyOptions);
+ return this;
+ }
+
public StringBuilder appendTo(StringBuilder builder) {
return appendTo(builder, builder::append);
}
@@ -140,6 +190,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer