Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
192 changes: 192 additions & 0 deletions src/main/java/net/sf/jsqlparser/statement/CopyLegacyOption.java
Original file line number Diff line number Diff line change
@@ -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 <a href="https://www.postgresql.org/docs/18/sql-copy.html">COPY, section Compatibility</a>
*/
public class CopyLegacyOption extends ASTNodeAccessImpl {
private Kind kind;
private boolean asKeyword;
private StringValue value;
private final List<Column> 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<Column> getColumns() {
return columns;
}

public void setColumns(Collection<? extends Column> columns) {
List<Column> 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();
}
}
58 changes: 57 additions & 1 deletion src/main/java/net/sf/jsqlparser/statement/CopyStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="https://duckdb.org/docs/stable/sql/statements/copy">COPY</a>
* @see <a href="https://www.postgresql.org/docs/18/sql-copy.html">PostgreSQL COPY</a>
*/
public class CopyStatement extends ASTNodeAccessImpl implements Statement {
private Table table;
Expand All @@ -34,6 +40,8 @@ public class CopyStatement extends ASTNodeAccessImpl implements Statement {
private String path;
private List<String> options;
private boolean withKeyword;
private boolean binary;
private final List<CopyLegacyOption> legacyOptions = new ArrayList<>();

public Table getTable() {
return table;
Expand Down Expand Up @@ -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<String> getOptions() {
return options;
}
Expand Down Expand Up @@ -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<CopyLegacyOption> getLegacyOptions() {
return legacyOptions;
}

public void setLegacyOptions(Collection<? extends CopyLegacyOption> legacyOptions) {
List<CopyLegacyOption> 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);
}
Expand All @@ -140,6 +190,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Select> selectPrin
selectPrinter.accept(select);
builder.append(')');
} else {
if (binary) {
builder.append("BINARY ");
}
builder.append(table);
if (columns != null && !columns.isEmpty()) {
builder.append(" (").append(columns).append(")");
Expand All @@ -149,6 +202,9 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<Select> selectPrin
if (withKeyword) {
builder.append(" WITH");
}
for (CopyLegacyOption legacyOption : legacyOptions) {
legacyOption.appendTo(builder.append(' '));
}
if (options != null && !options.isEmpty()) {
builder.append(" ").append(PlainSelect.getStringList(options, true, true));
}
Expand Down
Loading
Loading