diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java index 4d9201e5e..92563d7d8 100644 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java @@ -119,7 +119,7 @@ public LikeExpression withRightExpression(Expression arg0) { } public enum KeyWord { - LIKE, ILIKE, RLIKE, REGEXP_LIKE, REGEXP, SIMILAR_TO, MATCH_ANY, MATCH_ALL, MATCH_PHRASE, MATCH_PHRASE_PREFIX, MATCH_REGEXP; + LIKE, ILIKE, RLIKE, REGEXP_LIKE, REGEXP, GLOB, SIMILAR_TO, MATCH_ANY, MATCH_ALL, MATCH_PHRASE, MATCH_PHRASE_PREFIX, MATCH_REGEXP; public static KeyWord from(String keyword) { return Enum.valueOf(KeyWord.class, 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 21ae94db5..0653d81fa 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -182,6 +182,10 @@ public enum Feature { * "SEMI" join */ joinSemi, + /** + * @see net.sf.jsqlparser.statement.select.Join#isAnti() + */ + joinAnti, /** * "INNER" join */ diff --git a/src/main/java/net/sf/jsqlparser/statement/AttachStatement.java b/src/main/java/net/sf/jsqlparser/statement/AttachStatement.java new file mode 100644 index 000000000..7ad71dd54 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/AttachStatement.java @@ -0,0 +1,121 @@ +/*- + * #%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 net.sf.jsqlparser.statement.select.PlainSelect; + +import java.util.List; + +/** + * DuckDB's {@code ATTACH [DATABASE] [IF NOT EXISTS] path [AS alias] [(options)]}, which adds + * another database file or remote database to the session. + * + * @see ATTACH + */ +public class AttachStatement implements Statement { + private String databasePath; + private String alias; + private boolean usingDatabaseKeyword; + private boolean ifNotExists; + private List options; + + public String getDatabasePath() { + return databasePath; + } + + public void setDatabasePath(String databasePath) { + this.databasePath = databasePath; + } + + public AttachStatement withDatabasePath(String databasePath) { + setDatabasePath(databasePath); + return this; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public AttachStatement withAlias(String alias) { + setAlias(alias); + return this; + } + + public boolean isUsingDatabaseKeyword() { + return usingDatabaseKeyword; + } + + public void setUsingDatabaseKeyword(boolean usingDatabaseKeyword) { + this.usingDatabaseKeyword = usingDatabaseKeyword; + } + + public AttachStatement withUsingDatabaseKeyword(boolean usingDatabaseKeyword) { + setUsingDatabaseKeyword(usingDatabaseKeyword); + return this; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean ifNotExists) { + this.ifNotExists = ifNotExists; + } + + public AttachStatement withIfNotExists(boolean ifNotExists) { + setIfNotExists(ifNotExists); + return this; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public AttachStatement withOptions(List options) { + setOptions(options); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ATTACH "); + if (usingDatabaseKeyword) { + builder.append("DATABASE "); + } + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + builder.append(databasePath); + if (alias != null) { + builder.append(" AS ").append(alias); + } + if (options != null && !options.isEmpty()) { + builder.append(" ").append(PlainSelect.getStringList(options, true, true)); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/ConnectStatement.java b/src/main/java/net/sf/jsqlparser/statement/ConnectStatement.java new file mode 100644 index 000000000..a5ae350c6 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/ConnectStatement.java @@ -0,0 +1,69 @@ +/*- + * #%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; + +/** + * DuckDB's {@code CONNECT target [AS alias]}, which routes the session's queries to a remote + * database. + */ +public class ConnectStatement implements Statement { + private String target; + private String alias; + + public ConnectStatement() {} + + public ConnectStatement(String target) { + this.target = target; + } + + public String getTarget() { + return target; + } + + public void setTarget(String target) { + this.target = target; + } + + public ConnectStatement withTarget(String target) { + setTarget(target); + return this; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public ConnectStatement withAlias(String alias) { + setAlias(alias); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("CONNECT ").append(target); + if (alias != null) { + builder.append(" AS ").append(alias); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/CopyStatement.java b/src/main/java/net/sf/jsqlparser/statement/CopyStatement.java new file mode 100644 index 000000000..0dbc2892b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/CopyStatement.java @@ -0,0 +1,139 @@ +/*- + * #%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 net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; + +import java.util.List; + +/** + * {@code COPY table [(columns)] FROM path [(options)]} and + * {@code COPY {table | (query)} TO path [(options)]}, DuckDB's bulk import and export statement. + * + * @see COPY + */ +public class CopyStatement implements Statement { + private Table table; + private ExpressionList columns; + private Select select; + private boolean from; + private String path; + private List options; + + public Table getTable() { + return table; + } + + public void setTable(Table table) { + this.table = table; + } + + public CopyStatement withTable(Table table) { + setTable(table); + return this; + } + + public ExpressionList getColumns() { + return columns; + } + + public void setColumns(ExpressionList columns) { + this.columns = columns; + } + + public CopyStatement withColumns(ExpressionList columns) { + setColumns(columns); + return this; + } + + public Select getSelect() { + return select; + } + + public void setSelect(Select select) { + this.select = select; + } + + public CopyStatement withSelect(Select select) { + setSelect(select); + return this; + } + + /** {@code true} for COPY ... FROM (import), {@code false} for COPY ... TO (export). */ + public boolean isFrom() { + return from; + } + + public void setFrom(boolean from) { + this.from = from; + } + + public CopyStatement withFrom(boolean from) { + setFrom(from); + return this; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public CopyStatement withPath(String path) { + setPath(path); + return this; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public CopyStatement withOptions(List options) { + setOptions(options); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("COPY "); + if (select != null) { + builder.append("(").append(select).append(")"); + } else { + builder.append(table); + if (columns != null && !columns.isEmpty()) { + builder.append(" (").append(columns).append(")"); + } + } + builder.append(from ? " FROM " : " TO ").append(path); + if (options != null && !options.isEmpty()) { + builder.append(" ").append(PlainSelect.getStringList(options, true, true)); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/DeallocateStatement.java b/src/main/java/net/sf/jsqlparser/statement/DeallocateStatement.java new file mode 100644 index 000000000..25d375e38 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/DeallocateStatement.java @@ -0,0 +1,69 @@ +/*- + * #%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; + +/** + * {@code DEALLOCATE [PREPARE] name}, which drops a statement prepared by {@link PrepareStatement}. + */ +public class DeallocateStatement implements Statement { + private String name; + private boolean usingPrepareKeyword; + + public DeallocateStatement() {} + + public DeallocateStatement(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public DeallocateStatement withName(String name) { + setName(name); + return this; + } + + public boolean isUsingPrepareKeyword() { + return usingPrepareKeyword; + } + + public void setUsingPrepareKeyword(boolean usingPrepareKeyword) { + this.usingPrepareKeyword = usingPrepareKeyword; + } + + public DeallocateStatement withUsingPrepareKeyword(boolean usingPrepareKeyword) { + setUsingPrepareKeyword(usingPrepareKeyword); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("DEALLOCATE "); + if (usingPrepareKeyword) { + builder.append("PREPARE "); + } + builder.append(name); + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/DescribeStatement.java b/src/main/java/net/sf/jsqlparser/statement/DescribeStatement.java index 10a92764c..054b64843 100644 --- a/src/main/java/net/sf/jsqlparser/statement/DescribeStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/DescribeStatement.java @@ -10,10 +10,12 @@ package net.sf.jsqlparser.statement; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.select.Select; public class DescribeStatement implements Statement { private Table table; + private Select select; private String describeType; public DescribeStatement() { @@ -32,9 +34,24 @@ public void setTable(Table table) { this.table = table; } + /** DuckDB also describes a query: {@code DESCRIBE SELECT ...}. */ + public Select getSelect() { + return select; + } + + public void setSelect(Select select) { + this.select = select; + } + + public DescribeStatement withSelect(Select select) { + setSelect(select); + return this; + } + @Override public String toString() { - return this.describeType + " " + table.getFullyQualifiedName(); + return this.describeType + " " + + (select != null ? select.toString() : table.getFullyQualifiedName()); } @Override diff --git a/src/main/java/net/sf/jsqlparser/statement/DetachStatement.java b/src/main/java/net/sf/jsqlparser/statement/DetachStatement.java new file mode 100644 index 000000000..80fe5780a --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/DetachStatement.java @@ -0,0 +1,88 @@ +/*- + * #%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; + +/** + * DuckDB's {@code DETACH [DATABASE] [IF EXISTS] name}, the counterpart of {@link AttachStatement}. + * + * @see DETACH + */ +public class DetachStatement implements Statement { + private String databaseName; + private boolean usingDatabaseKeyword; + private boolean ifExists; + + public DetachStatement() {} + + public DetachStatement(String databaseName) { + this.databaseName = databaseName; + } + + public String getDatabaseName() { + return databaseName; + } + + public void setDatabaseName(String databaseName) { + this.databaseName = databaseName; + } + + public DetachStatement withDatabaseName(String databaseName) { + setDatabaseName(databaseName); + return this; + } + + public boolean isUsingDatabaseKeyword() { + return usingDatabaseKeyword; + } + + public void setUsingDatabaseKeyword(boolean usingDatabaseKeyword) { + this.usingDatabaseKeyword = usingDatabaseKeyword; + } + + public DetachStatement withUsingDatabaseKeyword(boolean usingDatabaseKeyword) { + setUsingDatabaseKeyword(usingDatabaseKeyword); + return this; + } + + public boolean isIfExists() { + return ifExists; + } + + public void setIfExists(boolean ifExists) { + this.ifExists = ifExists; + } + + public DetachStatement withIfExists(boolean ifExists) { + setIfExists(ifExists); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("DETACH "); + if (usingDatabaseKeyword) { + builder.append("DATABASE "); + } + if (ifExists) { + builder.append("IF EXISTS "); + } + builder.append(databaseName); + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/DisconnectStatement.java b/src/main/java/net/sf/jsqlparser/statement/DisconnectStatement.java new file mode 100644 index 000000000..fdbe6f4e7 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/DisconnectStatement.java @@ -0,0 +1,54 @@ +/*- + * #%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; + +/** + * DuckDB's {@code DISCONNECT [name]}, the counterpart of {@link ConnectStatement}. + */ +public class DisconnectStatement implements Statement { + private String name; + + public DisconnectStatement() {} + + public DisconnectStatement(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public DisconnectStatement withName(String name) { + setName(name); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("DISCONNECT"); + if (name != null) { + builder.append(" ").append(name); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/ExtensionStatement.java b/src/main/java/net/sf/jsqlparser/statement/ExtensionStatement.java new file mode 100644 index 000000000..621927366 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/ExtensionStatement.java @@ -0,0 +1,107 @@ +/*- + * #%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; + +/** + * DuckDB's extension management statements {@code [FORCE] INSTALL extension [FROM repository]} and + * {@code LOAD extension}. + * + * @see Extensions + */ +public class ExtensionStatement implements Statement { + private Operation operation; + private boolean force; + private String extensionName; + private String repository; + + public ExtensionStatement() {} + + public ExtensionStatement(Operation operation, String extensionName) { + this.operation = operation; + this.extensionName = extensionName; + } + + public Operation getOperation() { + return operation; + } + + public void setOperation(Operation operation) { + this.operation = operation; + } + + public ExtensionStatement withOperation(Operation operation) { + setOperation(operation); + return this; + } + + public boolean isForce() { + return force; + } + + public void setForce(boolean force) { + this.force = force; + } + + public ExtensionStatement withForce(boolean force) { + setForce(force); + return this; + } + + public String getExtensionName() { + return extensionName; + } + + public void setExtensionName(String extensionName) { + this.extensionName = extensionName; + } + + public ExtensionStatement withExtensionName(String extensionName) { + setExtensionName(extensionName); + return this; + } + + public String getRepository() { + return repository; + } + + public void setRepository(String repository) { + this.repository = repository; + } + + public ExtensionStatement withRepository(String repository) { + setRepository(repository); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + if (force) { + builder.append("FORCE "); + } + builder.append(operation).append(" ").append(extensionName); + if (repository != null) { + builder.append(" FROM ").append(repository); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + public enum Operation { + INSTALL, LOAD + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/PragmaStatement.java b/src/main/java/net/sf/jsqlparser/statement/PragmaStatement.java new file mode 100644 index 000000000..c1692e6df --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/PragmaStatement.java @@ -0,0 +1,91 @@ +/*- + * #%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 net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +/** + * {@code PRAGMA name}, {@code PRAGMA name(arguments)} and {@code PRAGMA name = value}, used by + * DuckDB and SQLite to read or set database settings. + * + * @see PRAGMA statements + */ +public class PragmaStatement implements Statement { + private String name; + private ExpressionList parameters; + private Expression value; + + public PragmaStatement() {} + + public PragmaStatement(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public PragmaStatement withName(String name) { + setName(name); + return this; + } + + public ExpressionList getParameters() { + return parameters; + } + + public void setParameters(ExpressionList parameters) { + this.parameters = parameters; + } + + public PragmaStatement withParameters(ExpressionList parameters) { + setParameters(parameters); + return this; + } + + public Expression getValue() { + return value; + } + + public void setValue(Expression value) { + this.value = value; + } + + public PragmaStatement withValue(Expression value) { + setValue(value); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("PRAGMA ").append(name); + if (parameters != null) { + builder.append("(").append(parameters).append(")"); + } + if (value != null) { + builder.append(" = ").append(value); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/PrepareStatement.java b/src/main/java/net/sf/jsqlparser/statement/PrepareStatement.java new file mode 100644 index 000000000..e4675b340 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/PrepareStatement.java @@ -0,0 +1,70 @@ +/*- + * #%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; + +/** + * {@code PREPARE name AS statement}, which stores a parameterised statement for later + * {@code EXECUTE}. + * + * @see Prepared + * statements + */ +public class PrepareStatement implements Statement { + private String name; + private Statement statement; + + public PrepareStatement() {} + + public PrepareStatement(String name, Statement statement) { + this.name = name; + this.statement = statement; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public PrepareStatement withName(String name) { + setName(name); + return this; + } + + public Statement getStatement() { + return statement; + } + + public void setStatement(Statement statement) { + this.statement = statement; + } + + public PrepareStatement withStatement(Statement statement) { + setStatement(statement); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("PREPARE ").append(name).append(" AS ").append(statement); + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 861612fec..305b91a9c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -93,6 +93,8 @@ import java.util.Locale; import java.util.Set; import java.util.function.Predicate; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; /** * Derives a {@link StatementFeatures} verdict from a statement tree. @@ -521,6 +523,83 @@ public Void visit(Drop drop, S context) { return null; } + @Override + public Void visit(PragmaStatement pragmaStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(ExtensionStatement extensionStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(AttachStatement attachStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(DetachStatement detachStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(ConnectStatement connectStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(DisconnectStatement disconnectStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(PrepareStatement prepareStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(DeallocateStatement deallocateStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + return null; + } + + @Override + public Void visit(CopyStatement copyStatement, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.READS_DATA, StmtFeature.MODIFIES_DATA); + return null; + } + + @Override + public Void visit(CreateMacro createMacro, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + return null; + } + + @Override + public Void visit(CreateExtensionRepository createExtensionRepository, S context) { + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SCHEMA); + return null; + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { analysis.claimTopLevel(); diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 8ec9dc1bc..d0a01a4a1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -68,6 +68,8 @@ import net.sf.jsqlparser.statement.update.ParenthesedUpdate; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.upsert.Upsert; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; public interface StatementVisitor { @@ -371,6 +373,72 @@ default void visit(RenameTableStatement renameTableStatement) { this.visit(renameTableStatement, null); } + T visit(PragmaStatement pragmaStatement, S context); + + default void visit(PragmaStatement pragmaStatement) { + this.visit(pragmaStatement, null); + } + + T visit(ExtensionStatement extensionStatement, S context); + + default void visit(ExtensionStatement extensionStatement) { + this.visit(extensionStatement, null); + } + + T visit(AttachStatement attachStatement, S context); + + default void visit(AttachStatement attachStatement) { + this.visit(attachStatement, null); + } + + T visit(DetachStatement detachStatement, S context); + + default void visit(DetachStatement detachStatement) { + this.visit(detachStatement, null); + } + + T visit(ConnectStatement connectStatement, S context); + + default void visit(ConnectStatement connectStatement) { + this.visit(connectStatement, null); + } + + T visit(DisconnectStatement disconnectStatement, S context); + + default void visit(DisconnectStatement disconnectStatement) { + this.visit(disconnectStatement, null); + } + + T visit(PrepareStatement prepareStatement, S context); + + default void visit(PrepareStatement prepareStatement) { + this.visit(prepareStatement, null); + } + + T visit(DeallocateStatement deallocateStatement, S context); + + default void visit(DeallocateStatement deallocateStatement) { + this.visit(deallocateStatement, null); + } + + T visit(CopyStatement copyStatement, S context); + + default void visit(CopyStatement copyStatement) { + this.visit(copyStatement, null); + } + + T visit(CreateMacro createMacro, S context); + + default void visit(CreateMacro createMacro) { + this.visit(createMacro, null); + } + + T visit(CreateExtensionRepository createExtensionRepository, S context); + + default void visit(CreateExtensionRepository createExtensionRepository) { + this.visit(createExtensionRepository, null); + } + T visit(PurgeStatement purgeStatement, S context); default void visit(PurgeStatement purgeStatement) { diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index ab05725ad..a64b21960 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -89,6 +89,8 @@ import java.util.List; import net.sf.jsqlparser.util.TableDefinitionTraversal; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; @SuppressWarnings({"PMD.UncommentedEmptyMethodBody"}) public class StatementVisitorAdapter implements StatementVisitor { @@ -618,6 +620,61 @@ public T visit(RenameTableStatement renameTableStatement, S context) { return null; } + @Override + public T visit(PragmaStatement pragmaStatement, S context) { + return null; + } + + @Override + public T visit(ExtensionStatement extensionStatement, S context) { + return null; + } + + @Override + public T visit(AttachStatement attachStatement, S context) { + return null; + } + + @Override + public T visit(DetachStatement detachStatement, S context) { + return null; + } + + @Override + public T visit(ConnectStatement connectStatement, S context) { + return null; + } + + @Override + public T visit(DisconnectStatement disconnectStatement, S context) { + return null; + } + + @Override + public T visit(PrepareStatement prepareStatement, S context) { + return null; + } + + @Override + public T visit(DeallocateStatement deallocateStatement, S context) { + return null; + } + + @Override + public T visit(CopyStatement copyStatement, S context) { + return null; + } + + @Override + public T visit(CreateMacro createMacro, S context) { + return null; + } + + @Override + public T visit(CreateExtensionRepository createExtensionRepository, S context) { + return null; + } + @Override public T visit(PurgeStatement purgeStatement, S context) { return null; diff --git a/src/main/java/net/sf/jsqlparser/statement/create/extension/CreateExtensionRepository.java b/src/main/java/net/sf/jsqlparser/statement/create/extension/CreateExtensionRepository.java new file mode 100644 index 000000000..f3b85ccee --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/extension/CreateExtensionRepository.java @@ -0,0 +1,85 @@ +/*- + * #%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.create.extension; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * DuckDB 2.0's {@code CREATE EXTENSION REPOSITORY [IF NOT EXISTS] name [WITH PREFIX prefix]}, which + * registers a custom repository extensions may be installed from. + */ +public class CreateExtensionRepository implements Statement { + private String name; + private boolean ifNotExists; + private Expression prefix; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public CreateExtensionRepository withName(String name) { + setName(name); + return this; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean ifNotExists) { + this.ifNotExists = ifNotExists; + } + + public CreateExtensionRepository withIfNotExists(boolean ifNotExists) { + setIfNotExists(ifNotExists); + return this; + } + + public Expression getPrefix() { + return prefix; + } + + public void setPrefix(Expression prefix) { + this.prefix = prefix; + } + + public CreateExtensionRepository withPrefix(Expression prefix) { + setPrefix(prefix); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("CREATE EXTENSION REPOSITORY "); + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + builder.append(name); + if (prefix != null) { + builder.append(" WITH PREFIX ").append(prefix); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/macro/CreateMacro.java b/src/main/java/net/sf/jsqlparser/statement/create/macro/CreateMacro.java new file mode 100644 index 000000000..c08b866bb --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/macro/CreateMacro.java @@ -0,0 +1,178 @@ +/*- + * #%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.create.macro; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.select.Select; + +import java.io.Serializable; +import java.util.List; + +/** + * DuckDB's {@code CREATE [OR REPLACE] [TEMPORARY] MACRO name (parameters) AS [TABLE] body}, a named + * expression or query that is inlined at call sites. + * + * @see CREATE MACRO + */ +public class CreateMacro implements Statement { + private boolean orReplace; + private boolean temporary; + private String name; + private List parameters; + private Expression expression; + private Select select; + + public boolean isOrReplace() { + return orReplace; + } + + public void setOrReplace(boolean orReplace) { + this.orReplace = orReplace; + } + + public CreateMacro withOrReplace(boolean orReplace) { + setOrReplace(orReplace); + return this; + } + + public boolean isTemporary() { + return temporary; + } + + public void setTemporary(boolean temporary) { + this.temporary = temporary; + } + + public CreateMacro withTemporary(boolean temporary) { + setTemporary(temporary); + return this; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public CreateMacro withName(String name) { + setName(name); + return this; + } + + public List getParameters() { + return parameters; + } + + public void setParameters(List parameters) { + this.parameters = parameters; + } + + public CreateMacro withParameters(List parameters) { + setParameters(parameters); + return this; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public CreateMacro withExpression(Expression expression) { + setExpression(expression); + return this; + } + + public Select getSelect() { + return select; + } + + public void setSelect(Select select) { + this.select = select; + } + + public CreateMacro withSelect(Select select) { + setSelect(select); + return this; + } + + /** A table macro returns a relation, a scalar macro an expression. */ + public boolean isTable() { + return select != null; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("CREATE "); + if (orReplace) { + builder.append("OR REPLACE "); + } + if (temporary) { + builder.append("TEMPORARY "); + } + builder.append("MACRO ").append(name).append(" ("); + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) { + builder.append(", "); + } + builder.append(parameters.get(i)); + } + builder.append(") AS "); + if (select != null) { + builder.append("TABLE ").append(select); + } else { + builder.append(expression); + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + /** A macro parameter, optionally carrying a default value ({@code b := 5}). */ + public static class Parameter implements Serializable { + private final String name; + private final Expression defaultValue; + + public Parameter(String name) { + this(name, null); + } + + public Parameter(String name, Expression defaultValue) { + this.name = name; + this.defaultValue = defaultValue; + } + + public String getName() { + return name; + } + + public Expression getDefaultValue() { + return defaultValue; + } + + @Override + public String toString() { + return defaultValue == null ? name : name + " := " + defaultValue; + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java b/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java index 105115683..4c3b8f3f8 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java @@ -241,7 +241,9 @@ public void visit(Consumer tables, Consumer expressions) { } public void appendTo(StringBuilder sql, Consumer visitor) { - if (routine == null) { + // MySQL's shorter form has neither a routine nor any of the standard clauses + if (routine == null && transitionRelations.isEmpty() && !constraint + && orientation != Orientation.STATEMENT) { sql.append(mysqlSql()); return; } @@ -252,8 +254,13 @@ public void appendTo(StringBuilder sql, Consumer visitor) { visitor.accept(whenExpression); sql.append(')'); } - sql.append(" EXECUTE ").append(executeKeyword).append(' '); - visitor.accept(routine); + if (routine != null) { + sql.append(" EXECUTE ").append(executeKeyword).append(' '); + visitor.accept(routine); + } else { + // DuckDB 2.0 runs a statement rather than a function + sql.append(' ').append(body); + } } private void appendPostgreSqlHeader(StringBuilder sql, Consumer visitor) { diff --git a/src/main/java/net/sf/jsqlparser/statement/insert/Insert.java b/src/main/java/net/sf/jsqlparser/statement/insert/Insert.java index 307d0384b..3541c1022 100644 --- a/src/main/java/net/sf/jsqlparser/statement/insert/Insert.java +++ b/src/main/java/net/sf/jsqlparser/statement/insert/Insert.java @@ -59,6 +59,7 @@ public enum OverridingMode { private InsertConflictAction conflictAction; private InsertDuplicateAction duplicateAction; private Alias rowAlias; + private ColumnMatching columnMatching; private boolean oracleMultiInsert = false; private boolean oracleMultiInsertFirst = false; private List oracleMultiInsertBranches; @@ -231,6 +232,38 @@ public void setWithItemsList(List> withItemsList) { this.withItemsList = withItemsList; } + /** + * DuckDB matches the source columns of an INSERT either positionally or by name: + * {@code INSERT INTO tbl BY NAME SELECT ...}. + */ + public enum ColumnMatching { + BY_NAME("BY NAME"), BY_POSITION("BY POSITION"); + + private final String keywords; + + ColumnMatching(String keywords) { + this.keywords = keywords; + } + + @Override + public String toString() { + return keywords; + } + } + + public ColumnMatching getColumnMatching() { + return columnMatching; + } + + public void setColumnMatching(ColumnMatching columnMatching) { + this.columnMatching = columnMatching; + } + + public Insert withColumnMatching(ColumnMatching columnMatching) { + setColumnMatching(columnMatching); + return this; + } + public OverridingMode getOverridingMode() { return overridingMode; } @@ -406,6 +439,10 @@ private void appendInsertTargetAndValues(StringBuilder sql) { sql.append(") "); } + if (columnMatching != null) { + sql.append(columnMatching).append(" "); + } + if (isOverriding()) { sql.append("OVERRIDING ").append(overridingMode).append(" VALUE "); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java index 69af24f55..f5c623577 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitor.java @@ -92,6 +92,14 @@ default void visit(PivotQuery pivotQuery) { this.visit(pivotQuery, null); } + default T visit(UnPivotQuery unPivotQuery, S context) { + return null; + } + + default void visit(UnPivotQuery unPivotQuery) { + this.visit(unPivotQuery, null); + } + T visit(SetOperationList setOperationList, S context); default void visit(SetOperationList setOperationList) { diff --git a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java index 3c88d63c4..c7ce99901 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/FromItemVisitorAdapter.java @@ -129,6 +129,11 @@ public T visit(PivotQuery pivotQuery, S context) { return pivotQuery.accept(selectVisitor, context); } + @Override + public T visit(UnPivotQuery unPivotQuery, S context) { + return unPivotQuery.accept(selectVisitor, context); + } + @Override public T visit(SetOperationList setOperationList, S context) { ArrayList results = new ArrayList<>(); diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Join.java b/src/main/java/net/sf/jsqlparser/statement/select/Join.java index d134689f6..d06a5df1f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Join.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Join.java @@ -36,6 +36,9 @@ public class Join extends ASTNodeAccessImpl { private boolean simple = false; private boolean cross = false; private boolean semi = false; + private boolean anti = false; + private Long approxNearest; + private Expression similarity; private boolean any = false; private boolean all = false; private boolean straight = false; @@ -194,6 +197,49 @@ public Join withFetch(boolean b) { * * @return true if is a "SEMI" join */ + /** + * DuckDB 2.0's similarity join: + * {@code JOIN products APPROX NEAREST 2 BY SIMILARITY array_cosine_similarity(...)}. + */ + public Long getApproxNearest() { + return approxNearest; + } + + public void setApproxNearest(Long approxNearest) { + this.approxNearest = approxNearest; + } + + public Join withApproxNearest(Long approxNearest) { + setApproxNearest(approxNearest); + return this; + } + + public Expression getSimilarity() { + return similarity; + } + + public void setSimilarity(Expression similarity) { + this.similarity = similarity; + } + + public Join withSimilarity(Expression similarity) { + setSimilarity(similarity); + return this; + } + + public boolean isAnti() { + return anti; + } + + public void setAnti(boolean b) { + anti = b; + } + + public Join withAnti(boolean b) { + this.setAnti(b); + return this; + } + public boolean isSemi() { return semi; } @@ -511,6 +557,8 @@ public StringBuilder appendJoinTypeTo(StringBuilder builder) { builder.append("INNER "); } else if (isSemi()) { builder.append("SEMI "); + } else if (isAnti()) { + builder.append("ANTI "); } if (isArray()) { @@ -561,6 +609,11 @@ public String toString() { builder.append(fromItem).append((joinWindow != null) ? " WITHIN " + joinWindow : ""); } + if (approxNearest != null) { + builder.append(" APPROX NEAREST ").append(approxNearest) + .append(" BY SIMILARITY ").append(similarity); + } + for (Expression onExpression : onExpressions) { builder.append(" ON ").append(onExpression); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java b/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java index f84bf6ea9..58510e04f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/PlainSelect.java @@ -66,6 +66,7 @@ public class PlainSelect extends Select { private boolean isUsingFinal = false; private boolean isUsingOnly = false; private boolean useWithNoLog = false; + private boolean fromFirst = false; private Table intoTempTable = null; private List settings = null; @@ -543,9 +544,83 @@ public void setWindowDefinitions(List windowDefinitions) { @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ExcessiveMethodLength", "PMD.NPathComplexity"}) + /** + * DuckDB allows the FROM clause to come first, with the SELECT clause optional: + * {@code FROM tbl SELECT a} and {@code FROM tbl WHERE a > 1}. + */ + public boolean isFromFirst() { + return fromFirst; + } + + public void setFromFirst(boolean fromFirst) { + this.fromFirst = fromFirst; + } + + public PlainSelect withFromFirst(boolean fromFirst) { + setFromFirst(fromFirst); + return this; + } + public StringBuilder appendSelectBodyTo(StringBuilder builder) { + if (fromFirst) { + // DuckDB allows the FROM clause to lead: FROM tbl [SELECT ...] [WHERE ...] + appendFromClauseTo(builder); + if (selectItems != null && !selectItems.isEmpty()) { + builder.append(" "); + appendSelectClauseTo(builder); + appendIntoClausesTo(builder); + } + } else { + appendSelectClauseTo(builder); + appendIntoClausesTo(builder); + appendFromClauseTo(builder); + } + + if (preWhere != null) { + builder.append(" PREWHERE ").append(preWhere); + } + if (where != null) { + builder.append(" WHERE ").append(where); + } + if (oracleHierarchical != null) { + builder.append(oracleHierarchical); + } + if (preferringClause != null) { + builder.append(" ").append(preferringClause); + } + if (groupBy != null) { + builder.append(" ").append(groupBy); + } + if (having != null) { + builder.append(" HAVING ").append(having); + } + if (qualify != null) { + builder.append(" QUALIFY ").append(qualify); + } + if (windowDefinitions != null) { + builder.append(" WINDOW "); + builder.append(windowDefinitions.stream().map(WindowDefinition::toString) + .collect(joining(", "))); + } + appendEmitClauseTo(builder); + if (intoTempTable != null) { + builder.append(" INTO TEMP ").append(intoTempTable); + } + if (useWithNoLog) { + builder.append(" WITH NO LOG"); + } + return builder; + } + + private void appendSelectClauseTo(StringBuilder builder) { builder.append("SELECT "); + appendSelectHintsTo(builder); + appendSelectQualifiersTo(builder); + appendMySqlSelectFlagsTo(builder); + builder.append(getStringList(selectItems)); + } + private void appendSelectHintsTo(StringBuilder builder) { if (this.mySqlHintStraightJoin) { builder.append("STRAIGHT_JOIN "); } @@ -561,7 +636,9 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) { if (first != null) { builder.append(first).append(" "); } + } + private void appendSelectQualifiersTo(StringBuilder builder) { if (distinct != null) { builder.append(distinct).append(" "); } @@ -580,14 +657,19 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) { if (top != null) { builder.append(top).append(" "); } + } + + private void appendMySqlSelectFlagsTo(StringBuilder builder) { if (mySqlCacheFlag != null) { builder.append(mySqlCacheFlag.name()).append(" "); } + if (mySqlSqlCalcFoundRows) { builder.append("SQL_CALC_FOUND_ROWS").append(" "); } - builder.append(getStringList(selectItems)); + } + private void appendIntoClausesTo(StringBuilder builder) { if (intoTables != null) { builder.append(" INTO "); for (Iterator
iter = intoTables.iterator(); iter.hasNext();) { @@ -603,9 +685,11 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) { .getPosition() == MySqlSelectIntoClause.Position.BEFORE_FROM) { builder.append(" ").append(mySqlSelectIntoClause); } + } + private void appendFromClauseTo(StringBuilder builder) { if (fromItem != null) { - builder.append(" FROM "); + builder.append(fromFirst ? "FROM " : " FROM "); if (isUsingOnly) { builder.append("ONLY "); } @@ -633,42 +717,9 @@ public StringBuilder appendSelectBodyTo(StringBuilder builder) { builder.append(" WINDOW ").append(ksqlWindow); } } - if (preWhere != null) { - builder.append(" PREWHERE ").append(preWhere); - } - if (where != null) { - builder.append(" WHERE ").append(where); - } - if (oracleHierarchical != null) { - builder.append(oracleHierarchical); - } - if (preferringClause != null) { - builder.append(" ").append(preferringClause); - } - if (groupBy != null) { - builder.append(" ").append(groupBy); - } - if (having != null) { - builder.append(" HAVING ").append(having); - } - if (qualify != null) { - builder.append(" QUALIFY ").append(qualify); - } - if (windowDefinitions != null) { - builder.append(" WINDOW "); - builder.append(windowDefinitions.stream().map(WindowDefinition::toString) - .collect(joining(", "))); - } - appendEmitClauseTo(builder); - if (intoTempTable != null) { - builder.append(" INTO TEMP ").append(intoTempTable); - } - if (useWithNoLog) { - builder.append(" WITH NO LOG"); - } - return builder; } + @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ExcessiveMethodLength", "PMD.NPathComplexity"}) diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SampleClause.java b/src/main/java/net/sf/jsqlparser/statement/select/SampleClause.java index e6bc8f21d..a3c0203df 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SampleClause.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SampleClause.java @@ -17,6 +17,7 @@ public class SampleClause { private Number percentageArgument; private String percentageUnit; private boolean argumentInBrackets = true; + private boolean methodInBrackets = false; // ClickHouse specific private Number offsetArgument; private Number repeatArgument; @@ -130,10 +131,23 @@ public SampleClause setMethod(String method) { return this; } + /** + * DuckDB writes the sampling method behind the sample size: + * {@code USING SAMPLE 10 PERCENT (bernoulli)}. + */ + public boolean isMethodInBrackets() { + return methodInBrackets; + } + + public SampleClause setMethodInBrackets(boolean methodInBrackets) { + this.methodInBrackets = methodInBrackets; + return this; + } + public StringBuilder appendTo(StringBuilder builder) { builder.append(" ").append(keyword); - if (method != null) { + if (method != null && !methodInBrackets) { builder.append(" ").append(method); } @@ -144,11 +158,20 @@ public StringBuilder appendTo(StringBuilder builder) { } else { builder.append(" ").append(percentageArgument); if (percentageUnit != null) { - builder.append(" ").append(percentageUnit); + // "10%" has no blank, "10 PERCENT" and "10 ROWS" do + builder.append("%".equals(percentageUnit) ? "" : " ").append(percentageUnit); } } } + if (methodInBrackets) { + builder.append(" (").append(method); + if (seedArgument != null) { + builder.append(", ").append(seedArgument); + } + builder.append(")"); + } + if (offsetArgument != null) { builder.append(" OFFSET ").append(offsetArgument); } @@ -157,7 +180,7 @@ public StringBuilder appendTo(StringBuilder builder) { builder.append(" REPEATABLE (").append(repeatArgument).append(")"); } - if (seedArgument != null) { + if (seedArgument != null && !methodInBrackets) { builder.append(" SEED (").append(seedArgument).append(")"); } @@ -189,7 +212,7 @@ public String toString() { } public enum SampleMethod { - BERNOULLI, SYSTEM, BLOCK; + BERNOULLI, SYSTEM, BLOCK, RESERVOIR; public static SampleMethod from(String sampleMethod) { return Enum.valueOf(SampleMethod.class, diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java index f5b88bd35..5591f4bf8 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitor.java @@ -47,6 +47,14 @@ default void visit(PivotQuery pivotQuery) { this.visit(pivotQuery, null); } + default T visit(UnPivotQuery unPivotQuery, S context) { + return null; + } + + default void visit(UnPivotQuery unPivotQuery) { + this.visit(unPivotQuery, null); + } + T visit(FromQuery fromQuery, S context); T visit(SetOperationList setOpList, S context); diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java index 051f3929e..28e9525bc 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java @@ -272,6 +272,16 @@ public T visit(PivotQuery pivotQuery, S context) { return null; } + @Override + public T visit(UnPivotQuery unPivotQuery, S context) { + visitWithItems(unPivotQuery.getWithItemsList(), context); + + fromItemVisitor.visitFromItem(unPivotQuery.getFromItem(), context); + expressionVisitor.visitExpressions(unPivotQuery.getOnExpressions(), context); + expressionVisitor.visitExpressions(unPivotQuery.getValueColumns(), context); + return null; + } + @Override public T visit(FromQuery fromQuery, S context) { return null; diff --git a/src/main/java/net/sf/jsqlparser/statement/select/UnPivotQuery.java b/src/main/java/net/sf/jsqlparser/statement/select/UnPivotQuery.java new file mode 100644 index 000000000..799e6b612 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/UnPivotQuery.java @@ -0,0 +1,115 @@ +/*- + * #%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.select; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.schema.Column; + +/** + * DuckDB's simplified UNPIVOT statement: + * {@code UNPIVOT tbl ON columns [INTO NAME name VALUE values]}, the counterpart of + * {@link PivotQuery}. + * + * @see UNPIVOT + */ +public class UnPivotQuery extends Select { + private FromItem fromItem; + private ExpressionList onExpressions; + private String nameColumn; + private ExpressionList valueColumns; + + public FromItem getFromItem() { + return fromItem; + } + + public void setFromItem(FromItem fromItem) { + this.fromItem = fromItem; + } + + public UnPivotQuery withFromItem(FromItem fromItem) { + setFromItem(fromItem); + return this; + } + + public ExpressionList getOnExpressions() { + return onExpressions; + } + + public void setOnExpressions(ExpressionList onExpressions) { + this.onExpressions = onExpressions; + } + + public UnPivotQuery withOnExpressions(ExpressionList onExpressions) { + setOnExpressions(onExpressions); + return this; + } + + public String getNameColumn() { + return nameColumn; + } + + public void setNameColumn(String nameColumn) { + this.nameColumn = nameColumn; + } + + public UnPivotQuery withNameColumn(String nameColumn) { + setNameColumn(nameColumn); + return this; + } + + public ExpressionList getValueColumns() { + return valueColumns; + } + + public void setValueColumns(ExpressionList valueColumns) { + this.valueColumns = valueColumns; + } + + public UnPivotQuery withValueColumns(ExpressionList valueColumns) { + setValueColumns(valueColumns); + return this; + } + + @Override + public StringBuilder appendSelectBodyTo(StringBuilder builder) { + builder.append("UNPIVOT ").append(fromItem); + if (onExpressions != null) { + builder.append(" ON ").append(onExpressions); + } + if (nameColumn != null) { + builder.append(" INTO NAME ").append(nameColumn); + if (valueColumns != null) { + builder.append(" VALUE ").append(valueColumns); + } + } + return builder; + } + + @Override + public T accept(SelectVisitor selectVisitor, S context) { + return selectVisitor.visit(this, context); + } + + @Override + public T accept(FromItemVisitor fromItemVisitor, S context) { + return fromItemVisitor.visit(this, context); + } + + @Override + public SampleClause getSampleClause() { + return null; + } + + @Override + public FromItem setSampleClause(SampleClause sampleClause) { + return null; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/WithItem.java b/src/main/java/net/sf/jsqlparser/statement/select/WithItem.java index 767f96846..49fcff0cb 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/WithItem.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/WithItem.java @@ -11,6 +11,7 @@ import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import java.io.Serializable; import java.util.ArrayList; @@ -35,6 +36,7 @@ public class WithItem implements Serializable { private boolean recursive = false; private boolean usingNot = false; private boolean materialized = false; + private ExpressionList usingKeyExpressions; public WithItem(K statement, Alias alias) { this.statement = statement; @@ -166,6 +168,23 @@ public WithItem withCycleClause(WithCycleClause cycleClause) { return this; } + /** + * DuckDB 2.0 aggregates a recursive CTE by key: + * {@code WITH RECURSIVE tbl (a, b) USING KEY (a, avg(b)) AS (...)}. + */ + public ExpressionList getUsingKeyExpressions() { + return usingKeyExpressions; + } + + public void setUsingKeyExpressions(ExpressionList usingKeyExpressions) { + this.usingKeyExpressions = usingKeyExpressions; + } + + public WithItem withUsingKeyExpressions(ExpressionList usingKeyExpressions) { + setUsingKeyExpressions(usingKeyExpressions); + return this; + } + public StringBuilder appendRecursiveClausesTo(StringBuilder builder, Consumer expressionPrinter) { if (searchClause != null) { @@ -197,6 +216,9 @@ public String toString() { } builder.append(")"); } + if (usingKeyExpressions != null) { + builder.append(" USING KEY (").append(usingKeyExpressions).append(")"); + } builder.append(" AS "); if (materialized) { builder.append(usingNot diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 3bf8df2e0..489297ce7 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -189,6 +189,7 @@ import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.Pivot; import net.sf.jsqlparser.statement.select.PivotQuery; +import net.sf.jsqlparser.statement.select.UnPivotQuery; import net.sf.jsqlparser.statement.select.PivotVisitor; import net.sf.jsqlparser.statement.select.PivotXml; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -209,6 +210,17 @@ import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.update.UpdateSet; import net.sf.jsqlparser.statement.upsert.Upsert; +import net.sf.jsqlparser.statement.PragmaStatement; +import net.sf.jsqlparser.statement.ExtensionStatement; +import net.sf.jsqlparser.statement.AttachStatement; +import net.sf.jsqlparser.statement.DetachStatement; +import net.sf.jsqlparser.statement.ConnectStatement; +import net.sf.jsqlparser.statement.DisconnectStatement; +import net.sf.jsqlparser.statement.PrepareStatement; +import net.sf.jsqlparser.statement.DeallocateStatement; +import net.sf.jsqlparser.statement.CopyStatement; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; /** @@ -483,6 +495,20 @@ public Void visit(PivotQuery pivotQuery, S context) { return null; } + @Override + public Void visit(UnPivotQuery unPivotQuery, S context) { + visitWithItems(unPivotQuery.getWithItemsList(), context); + visitFromItem(unPivotQuery.getFromItem(), context); + visitExpressions(unPivotQuery.getOnExpressions(), context); + visitExpressions(unPivotQuery.getValueColumns(), context); + return null; + } + + @Override + public void visit(UnPivotQuery unPivotQuery) { + SelectVisitor.super.visit(unPivotQuery); + } + private void visitSelectItems(List> selectItems, S context) { if (selectItems != null) { for (SelectItem item : selectItems) { @@ -2044,7 +2070,11 @@ public void visit(Values values) { @Override public Void visit(DescribeStatement describe, S context) { - describe.getTable().accept(this, context); + if (describe.getSelect() != null) { + describe.getSelect().accept((SelectVisitor) this, context); + } else { + describe.getTable().accept(this, context); + } return null; } @@ -2464,6 +2494,133 @@ public void visit(RenameTableStatement renameTableStatement) { StatementVisitor.super.visit(renameTableStatement); } + @Override + public Void visit(PragmaStatement pragmaStatement, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(PragmaStatement pragmaStatement) { + StatementVisitor.super.visit(pragmaStatement); + } + + @Override + public Void visit(ExtensionStatement extensionStatement, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(ExtensionStatement extensionStatement) { + StatementVisitor.super.visit(extensionStatement); + } + + @Override + public Void visit(AttachStatement attachStatement, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(AttachStatement attachStatement) { + StatementVisitor.super.visit(attachStatement); + } + + @Override + public Void visit(DetachStatement detachStatement, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(DetachStatement detachStatement) { + StatementVisitor.super.visit(detachStatement); + } + + @Override + public Void visit(ConnectStatement connectStatement, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(ConnectStatement connectStatement) { + StatementVisitor.super.visit(connectStatement); + } + + @Override + public Void visit(DisconnectStatement disconnectStatement, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(DisconnectStatement disconnectStatement) { + StatementVisitor.super.visit(disconnectStatement); + } + + @Override + public Void visit(PrepareStatement prepareStatement, S context) { + prepareStatement.getStatement().accept(this, context); + return null; + } + + @Override + public void visit(PrepareStatement prepareStatement) { + StatementVisitor.super.visit(prepareStatement); + } + + @Override + public Void visit(DeallocateStatement deallocateStatement, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(DeallocateStatement deallocateStatement) { + StatementVisitor.super.visit(deallocateStatement); + } + + @Override + public Void visit(CopyStatement copyStatement, S context) { + if (copyStatement.getSelect() != null) { + copyStatement.getSelect().accept((SelectVisitor) this, context); + } else if (copyStatement.getTable() != null) { + copyStatement.getTable().accept(this, context); + } + return null; + } + + @Override + public void visit(CopyStatement copyStatement) { + StatementVisitor.super.visit(copyStatement); + } + + @Override + public Void visit(CreateMacro createMacro, S context) { + if (createMacro.getSelect() != null) { + createMacro.getSelect().accept((SelectVisitor) this, context); + } + return null; + } + + @Override + public void visit(CreateMacro createMacro) { + StatementVisitor.super.visit(createMacro); + } + + @Override + public Void visit(CreateExtensionRepository createExtensionRepository, S context) { + // no tables involved in this statement + return null; + } + + @Override + public void visit(CreateExtensionRepository createExtensionRepository) { + StatementVisitor.super.visit(createExtensionRepository); + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { if (purgeStatement.getPurgeObjectType() == PurgeObjectType.TABLE) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/DmlDeParserSupport.java b/src/main/java/net/sf/jsqlparser/util/deparser/DmlDeParserSupport.java index 9231d2b0e..e11b26586 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/DmlDeParserSupport.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/DmlDeParserSupport.java @@ -93,6 +93,9 @@ StringBuilder deparseWithItem(WithItem item, S context) { builder.append(' ') .append(PlainSelect.getStringList(item.getWithItemList(), true, true)); } + if (item.getUsingKeyExpressions() != null) { + builder.append(" USING KEY (").append(item.getUsingKeyExpressions()).append(")"); + } builder.append(" AS "); if (item.isMaterialized()) { builder.append(item.isUsingNot() ? "NOT MATERIALIZED " : "MATERIALIZED "); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/InsertDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/InsertDeParser.java index 596af0456..7abcbc0c5 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/InsertDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/InsertDeParser.java @@ -104,6 +104,10 @@ public void deParse(Insert insert) { builder.append(")"); } + if (insert.getColumnMatching() != null) { + builder.append(" ").append(insert.getColumnMatching()); + } + if (insert.isOverriding()) { builder.append(" OVERRIDING ").append(insert.getOverridingMode()).append(" VALUE"); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index 0dc7d84b8..da0f5fcec 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -63,6 +63,7 @@ import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.Pivot; import net.sf.jsqlparser.statement.select.PivotQuery; +import net.sf.jsqlparser.statement.select.UnPivotQuery; import net.sf.jsqlparser.statement.select.PivotVisitor; import net.sf.jsqlparser.statement.select.PivotXml; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -198,101 +199,16 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { } } - builder.append("SELECT "); - - if (plainSelect.getMySqlHintStraightJoin()) { - builder.append("STRAIGHT_JOIN "); - } - - OracleHint hint = plainSelect.getOracleHint(); - if (hint != null) { - builder.append(hint).append(" "); - } - - Skip skip = plainSelect.getSkip(); - if (skip != null) { - builder.append(skip).append(" "); - } - - First first = plainSelect.getFirst(); - if (first != null) { - builder.append(first).append(" "); - } - - deparseDistinctClause(plainSelect.getDistinct()); - - if (plainSelect.getBigQuerySelectQualifier() != null) { - switch (plainSelect.getBigQuerySelectQualifier()) { - case AS_STRUCT: - builder.append("AS STRUCT "); - break; - case AS_VALUE: - builder.append("AS VALUE "); - break; - } - } - - Top top = plainSelect.getTop(); - if (top != null) { - visit(top); - } - - if (plainSelect.getMySqlSqlCacheFlag() != null) { - builder.append(plainSelect.getMySqlSqlCacheFlag().name()).append(" "); - } - - if (plainSelect.getMySqlSqlCalcFoundRows()) { - builder.append("SQL_CALC_FOUND_ROWS").append(" "); - } - - deparseSelectItemsClause(plainSelect.getSelectItems()); - - if (plainSelect.getIntoTables() != null) { - builder.append(" INTO "); - for (Iterator
iter = plainSelect.getIntoTables().iterator(); iter.hasNext();) { - visit(iter.next(), context); - if (iter.hasNext()) { - builder.append(", "); - } - } - } - - deparseMySqlSelectInto(plainSelect, MySqlSelectIntoClause.Position.BEFORE_FROM, context); - - if (plainSelect.getFromItem() != null) { - builder.append(" FROM "); - if (plainSelect.isUsingOnly()) { - builder.append("ONLY "); - } - plainSelect.getFromItem().accept(this, context); - - if (plainSelect.getFromItem() instanceof Table) { - Table table = (Table) plainSelect.getFromItem(); - if (table.getSampleClause() != null) { - table.getSampleClause().appendTo(builder); - } - } - } - - if (plainSelect.getLateralViews() != null) { - for (LateralView lateralView : plainSelect.getLateralViews()) { - deparseLateralView(lateralView); - } - } - - if (plainSelect.getJoins() != null) { - for (Join join : plainSelect.getJoins()) { - deparseJoin(join); + if (plainSelect.isFromFirst()) { + // DuckDB allows the FROM clause to lead, with an optional SELECT clause behind it + deparseFromClause(plainSelect, context); + if (plainSelect.getSelectItems() != null && !plainSelect.getSelectItems().isEmpty()) { + builder.append(" "); + deparseSelectClause(plainSelect, context); } - } - - if (plainSelect.isUsingFinal()) { - builder.append(" FINAL"); - } - - if (plainSelect.getKsqlWindow() != null) { - builder.append(" WINDOW "); - builder.append(plainSelect.getKsqlWindow().toString()); + } else { + deparseSelectClause(plainSelect, context); + deparseFromClause(plainSelect, context); } deparsePreWhereClause(plainSelect); @@ -419,6 +335,119 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { return builder; } + private void deparseSelectClause(PlainSelect plainSelect, S context) { + builder.append("SELECT "); + + if (plainSelect.getMySqlHintStraightJoin()) { + builder.append("STRAIGHT_JOIN "); + } + + OracleHint hint = plainSelect.getOracleHint(); + if (hint != null) { + builder.append(hint).append(" "); + } + + Skip skip = plainSelect.getSkip(); + if (skip != null) { + builder.append(skip).append(" "); + } + + First first = plainSelect.getFirst(); + if (first != null) { + builder.append(first).append(" "); + } + + deparseDistinctClause(plainSelect.getDistinct()); + + if (plainSelect.getBigQuerySelectQualifier() != null) { + switch (plainSelect.getBigQuerySelectQualifier()) { + case AS_STRUCT: + builder.append("AS STRUCT "); + break; + case AS_VALUE: + builder.append("AS VALUE "); + break; + } + } + + Top top = plainSelect.getTop(); + if (top != null) { + visit(top); + } + + if (plainSelect.getMySqlSqlCacheFlag() != null) { + builder.append(plainSelect.getMySqlSqlCacheFlag().name()).append(" "); + } + + if (plainSelect.getMySqlSqlCalcFoundRows()) { + builder.append("SQL_CALC_FOUND_ROWS").append(" "); + } + + deparseSelectItemsClause(plainSelect.getSelectItems()); + + if (plainSelect.getIntoTables() != null) { + builder.append(" INTO "); + for (Iterator
iter = plainSelect.getIntoTables().iterator(); iter.hasNext();) { + visit(iter.next(), context); + if (iter.hasNext()) { + builder.append(", "); + } + } + } + + deparseMySqlSelectInto(plainSelect, MySqlSelectIntoClause.Position.BEFORE_FROM, context); + } + + private void deparseFromClause(PlainSelect plainSelect, S context) { + if (plainSelect.getFromItem() != null) { + builder.append(plainSelect.isFromFirst() ? "FROM " : " FROM "); + if (plainSelect.isUsingOnly()) { + builder.append("ONLY "); + } + plainSelect.getFromItem().accept(this, context); + + if (plainSelect.getFromItem() instanceof Table) { + Table table = (Table) plainSelect.getFromItem(); + if (table.getSampleClause() != null) { + table.getSampleClause().appendTo(builder); + } + } + } + + if (plainSelect.getLateralViews() != null) { + for (LateralView lateralView : plainSelect.getLateralViews()) { + deparseLateralView(lateralView); + } + } + + if (plainSelect.getJoins() != null) { + for (Join join : plainSelect.getJoins()) { + deparseJoin(join); + } + } + + if (plainSelect.isUsingFinal()) { + builder.append(" FINAL"); + } + + if (plainSelect.getKsqlWindow() != null) { + builder.append(" WINDOW "); + builder.append(plainSelect.getKsqlWindow().toString()); + } + } + + + @Override + public StringBuilder visit(UnPivotQuery unPivotQuery, S context) { + unPivotQuery.appendSelectBodyTo(builder); + return builder; + } + + @Override + public void visit(UnPivotQuery unPivotQuery) { + SelectVisitor.super.visit(unPivotQuery); + } + @Override public StringBuilder visit(PivotQuery pivotQuery, S context) { List> withItemsList = pivotQuery.getWithItemsList(); @@ -744,6 +773,12 @@ public void deparseJoin(Join join) { builder.append(" WITHIN "); builder.append(join.getJoinWindow().toString()); } + if (join.getApproxNearest() != null) { + builder.append(" APPROX NEAREST ").append(join.getApproxNearest()) + .append(" BY SIMILARITY "); + join.getSimilarity().accept(expressionVisitor, null); + } + for (Expression onExpression : join.getOnExpressions()) { builder.append(" ON "); onExpression.accept(expressionVisitor, null); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index 69cb9770c..63e33a444 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -97,6 +97,17 @@ import net.sf.jsqlparser.statement.update.ParenthesedUpdate; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.upsert.Upsert; +import net.sf.jsqlparser.statement.PragmaStatement; +import net.sf.jsqlparser.statement.ExtensionStatement; +import net.sf.jsqlparser.statement.AttachStatement; +import net.sf.jsqlparser.statement.DetachStatement; +import net.sf.jsqlparser.statement.ConnectStatement; +import net.sf.jsqlparser.statement.DisconnectStatement; +import net.sf.jsqlparser.statement.PrepareStatement; +import net.sf.jsqlparser.statement.DeallocateStatement; +import net.sf.jsqlparser.statement.CopyStatement; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; public class StatementDeParser extends AbstractDeParser implements StatementVisitor { @@ -444,7 +455,11 @@ public StringBuilder visit(Comment comment, S context) { public StringBuilder visit(DescribeStatement describe, S context) { builder.append(describe.getDescribeType()); builder.append(" "); - builder.append(describe.getTable()); + if (describe.getSelect() != null) { + describe.getSelect().accept(this, context); + } else { + builder.append(describe.getTable()); + } return builder; } @@ -552,6 +567,72 @@ public StringBuilder visit(RenameTableStatement renameTableStatement, S cont return builder; } + @Override + public StringBuilder visit(PragmaStatement pragmaStatement, S context) { + pragmaStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(ExtensionStatement extensionStatement, S context) { + extensionStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(AttachStatement attachStatement, S context) { + attachStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(DetachStatement detachStatement, S context) { + detachStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(ConnectStatement connectStatement, S context) { + connectStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(DisconnectStatement disconnectStatement, S context) { + disconnectStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(PrepareStatement prepareStatement, S context) { + prepareStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(DeallocateStatement deallocateStatement, S context) { + deallocateStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(CopyStatement copyStatement, S context) { + copyStatement.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(CreateMacro createMacro, S context) { + createMacro.appendTo(builder); + return builder; + } + + @Override + public StringBuilder visit(CreateExtensionRepository createExtensionRepository, S context) { + createExtensionRepository.appendTo(builder); + return builder; + } + @Override public StringBuilder visit(PurgeStatement purgeStatement, S context) { purgeStatement.appendTo(builder); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java index 0048f8db8..f8c5694aa 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java @@ -59,6 +59,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS Feature.joinCross, Feature.joinOuter, Feature.joinSemi, + Feature.joinAnti, Feature.joinInner, Feature.joinStraight, Feature.joinApply, diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java index 71c33f902..a5295db77 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/SelectValidator.java @@ -34,6 +34,7 @@ import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.Pivot; import net.sf.jsqlparser.statement.select.PivotQuery; +import net.sf.jsqlparser.statement.select.UnPivotQuery; import net.sf.jsqlparser.statement.select.PivotVisitor; import net.sf.jsqlparser.statement.select.PivotXml; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -160,6 +161,14 @@ public Void visit(PlainSelect plainSelect, S context) { return null; } + @Override + public Void visit(UnPivotQuery unPivotQuery, S context) { + validateFeature(Feature.pivot); + validateOptionalFromItem(unPivotQuery.getFromItem()); + validateOptionalExpressions(unPivotQuery.getOnExpressions()); + return null; + } + @Override public Void visit(PivotQuery pivotQuery, S context) { if (isNotEmpty(pivotQuery.getWithItemsList())) { @@ -328,6 +337,7 @@ public void validateOptionalJoin(Join join) { validateFeature(c, join.isOuter(), Feature.joinOuter); validateFeature(c, join.isInner(), Feature.joinInner); validateFeature(c, join.isSemi(), Feature.joinSemi); + validateFeature(c, join.isAnti(), Feature.joinAnti); validateFeature(c, join.isStraight(), Feature.joinStraight); validateFeature(c, join.isApply(), Feature.joinApply); validateFeature(c, join.isAsOf(), Feature.joinAsOf); @@ -463,6 +473,11 @@ public void visit(PivotQuery pivotQuery) { visit(pivotQuery, null); } + @Override + public void visit(UnPivotQuery unPivotQuery) { + visit(unPivotQuery, null); + } + public void visit(SelectItem selectExpressionItem) { visit(selectExpressionItem, null); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index 6764ef740..13d48fd18 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -99,6 +99,17 @@ import net.sf.jsqlparser.statement.upsert.Upsert; import net.sf.jsqlparser.util.validation.ValidationCapability; import net.sf.jsqlparser.util.validation.metadata.NamedObject; +import net.sf.jsqlparser.statement.PragmaStatement; +import net.sf.jsqlparser.statement.ExtensionStatement; +import net.sf.jsqlparser.statement.AttachStatement; +import net.sf.jsqlparser.statement.DetachStatement; +import net.sf.jsqlparser.statement.ConnectStatement; +import net.sf.jsqlparser.statement.DisconnectStatement; +import net.sf.jsqlparser.statement.PrepareStatement; +import net.sf.jsqlparser.statement.DeallocateStatement; +import net.sf.jsqlparser.statement.CopyStatement; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; /** * @author gitmotte @@ -477,6 +488,72 @@ public Void visit(RenameTableStatement renameTableStatement, S context) { return null; } + @Override + public Void visit(PragmaStatement pragmaStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(ExtensionStatement extensionStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(AttachStatement attachStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(DetachStatement detachStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(ConnectStatement connectStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(DisconnectStatement disconnectStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(PrepareStatement prepareStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(DeallocateStatement deallocateStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(CopyStatement copyStatement, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(CreateMacro createMacro, S context) { + // TODO: not yet implemented + return null; + } + + @Override + public Void visit(CreateExtensionRepository createExtensionRepository, S context) { + // TODO: not yet implemented + return null; + } + @Override public Void visit(PurgeStatement purgeStatement, S context) { // TODO: not yet implemented @@ -693,6 +770,50 @@ public void visit(RenameTableStatement renameTableStatement) { visit(renameTableStatement, null); } + public void visit(PragmaStatement pragmaStatement) { + visit(pragmaStatement, null); + } + + public void visit(ExtensionStatement extensionStatement) { + visit(extensionStatement, null); + } + + public void visit(AttachStatement attachStatement) { + visit(attachStatement, null); + } + + public void visit(DetachStatement detachStatement) { + visit(detachStatement, null); + } + + public void visit(ConnectStatement connectStatement) { + visit(connectStatement, null); + } + + public void visit(DisconnectStatement disconnectStatement) { + visit(disconnectStatement, null); + } + + public void visit(PrepareStatement prepareStatement) { + visit(prepareStatement, null); + } + + public void visit(DeallocateStatement deallocateStatement) { + visit(deallocateStatement, null); + } + + public void visit(CopyStatement copyStatement) { + visit(copyStatement, null); + } + + public void visit(CreateMacro createMacro) { + visit(createMacro, null); + } + + public void visit(CreateExtensionRepository createExtensionRepository) { + visit(createExtensionRepository, null); + } + public void visit(PurgeStatement purgeStatement) { visit(purgeStatement, null); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 4ab8d1505..1d4e9cea8 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -60,6 +60,7 @@ import net.sf.jsqlparser.statement.create.extension.*; import net.sf.jsqlparser.statement.create.event.*; import net.sf.jsqlparser.statement.create.function.*; import net.sf.jsqlparser.statement.create.index.*; +import net.sf.jsqlparser.statement.create.macro.*; import net.sf.jsqlparser.statement.create.policy.*; import net.sf.jsqlparser.statement.create.procedure.*; import net.sf.jsqlparser.statement.create.schema.*; @@ -1063,9 +1064,11 @@ public class CCJSqlParser extends AbstractJSqlParser { case K_CONNECT: return nextKind != K_BY; case K_START: return nextKind != K_WITH; case K_LEFT: return nextKind != K_JOIN && nextKind != K_OUTER - && nextKind != K_SEMI && nextKind != K_ARRAY_LITERAL; + && nextKind != K_SEMI && nextKind != K_ANTI + && nextKind != K_ARRAY_LITERAL; case K_RIGHT: return nextKind != K_JOIN && nextKind != K_OUTER - && nextKind != K_SEMI && nextKind != K_ARRAY_LITERAL; + && nextKind != K_SEMI && nextKind != K_ANTI + && nextKind != K_ARRAY_LITERAL; case K_ARRAY_LITERAL: return nextKind != K_JOIN; case K_ALL: return nextKind != K_JOIN; @@ -1209,6 +1212,81 @@ public class CCJSqlParser extends AbstractJSqlParser { return isReservedKeywordSafeByFollower(); } + /** + * DuckDB's FROM-first query: a FROM clause followed by query clauses the pipe syntax cannot + * consume. Scanning stops at a pipe operator or the end of the statement, and clause keywords + * nested in brackets (a sub-select, for instance) do not count. + */ + private boolean isFromFirstSelectAhead() { + if (getToken(1).kind != K_FROM) { + return false; + } + int depth = 0; + for (int i = 2;; i++) { + Token token = getToken(i); + if (token == null || token.kind == EOF || ";".equals(token.image) + || "|>".equals(token.image)) { + return false; + } + if ("(".equals(token.image)) { + depth++; + } else if (")".equals(token.image)) { + depth--; + } else if (depth == 0) { + switch (token.kind) { + case K_SELECT: + case K_WHERE: + case K_GROUP: + case K_HAVING: + case K_QUALIFY: + case K_ORDER: + case K_LIMIT: + case K_OFFSET: + return true; + default: + break; + } + } + } + } + + /** CREATE [TEMP|TEMPORARY] MACRO, DuckDB's inlined function definition. */ + private boolean isCreateMacroAhead() { + int offset = 1; + int kind = getToken(offset).kind; + if (kind == K_TEMP || kind == K_TEMPORARY) { + offset++; + } + return getToken(offset).kind == K_MACRO; + } + + /** DuckDB sampling methods, which are ordinary identifiers except for SYSTEM/BERNOULLI. */ + private boolean isSampleMethod(Token token) { + return token.kind == K_SYSTEM || token.kind == K_BERNOULLI + || (token.image != null && "RESERVOIR".equalsIgnoreCase(token.image)); + } + + /** DuckDB's INSERT ... BY NAME|POSITION column matching, never a table alias. */ + private boolean isColumnMatchingAhead() { + return getToken(1).kind == K_BY + && ("NAME".equalsIgnoreCase(getToken(2).image) + || "POSITION".equalsIgnoreCase(getToken(2).image)); + } + + /** DuckDB 2.0's APPROX NEAREST similarity join, never a table alias. */ + private boolean isApproxNearestAhead() { + return getToken(1).kind == K_APPROX && getToken(2).kind == K_NEAREST; + } + + /** SEMI/ANTI stay contextual identifiers unless they prefix a JOIN. */ + private boolean isSemiAntiJoinAhead() { + int kind = getToken(1).kind; + if (kind != K_SEMI && kind != K_ANTI) { + return false; + } + return getToken(2).kind == K_JOIN; + } + /** ASOF stays a contextual identifier outside a join prefix. */ private boolean isAsOfJoinAhead() { if (!isKeywordAhead("ASOF")) { @@ -1238,7 +1316,7 @@ public class CCJSqlParser extends AbstractJSqlParser { * reserved keywords. */ private boolean isAliasAhead() { - if (isAsOfJoinAhead()) { + if (isAsOfJoinAhead() || isSemiAntiJoinAhead() || isApproxNearestAhead()) { return false; } Token t = getToken(1); @@ -1480,6 +1558,8 @@ public class CCJSqlParser extends AbstractJSqlParser { Token token = getToken(i); if (token.kind == EOF || ";".equals(token.image) || token.kind == K_BEGIN) { return false; } if (token.kind == K_EXECUTE && (getToken(i + 1).kind == K_FUNCTION || getToken(i + 1).kind == K_PROCEDURE)) { return true; } + // DuckDB 2.0 uses the standard header with a statement body instead of EXECUTE + if ("REFERENCING".equalsIgnoreCase(token.image) || token.kind == K_STATEMENT) { return true; } } } @@ -1713,6 +1793,7 @@ public class CCJSqlParser extends AbstractJSqlParser { case K_RLIKE: // RLIKE case K_REGEXP_LIKE: // REGEXP_LIKE case K_REGEXP: // REGEXP + case K_GLOB: // GLOB (DuckDB/SQLite) case K_SIMILAR_TO: // SIMILAR TO (in LikeExpression) case K_SIMILAR: // SIMILAR TO (in SimilarToExpression) case K_MATCH_ANY: // MATCH_ANY @@ -1929,8 +2010,10 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1977,6 +2060,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -1994,6 +2078,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2055,6 +2140,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2072,6 +2158,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2100,6 +2187,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2116,6 +2204,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2133,6 +2222,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2174,6 +2264,8 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= + | tk= | tk= | tk= | tk= @@ -2236,6 +2328,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2912,6 +3005,28 @@ Statement SingleStatement() : | LOOKAHEAD({ isKeywordAhead("REVOKE") }) stm = Revoke(false) | + LOOKAHEAD({ getToken(1).kind == K_PRAGMA }) stm = PragmaStatement() + | + LOOKAHEAD({ getToken(1).kind == K_INSTALL || getToken(1).kind == K_LOAD + || (getToken(1).kind == K_FORCE && getToken(2).kind == K_INSTALL) }) + stm = ExtensionStatement() + | + LOOKAHEAD({ getToken(1).kind == K_ATTACH }) stm = AttachStatement() + | + LOOKAHEAD({ getToken(1).kind == K_DETACH }) stm = DetachStatement() + | + LOOKAHEAD({ getToken(1).kind == K_CONNECT && getToken(2).kind != K_BY }) + stm = ConnectStatement() + | + LOOKAHEAD({ getToken(1).kind == K_DISCONNECT && getToken(2).kind != K_SESSION }) + stm = DisconnectStatement() + | + LOOKAHEAD({ getToken(1).kind == K_PREPARE }) stm = PrepareStatement() + | + LOOKAHEAD({ getToken(1).kind == K_DEALLOCATE }) stm = DeallocateStatement() + | + LOOKAHEAD({ getToken(1).kind == K_COPY }) stm = CopyStatement() + | stm = PurgeStatement() | stm = SessionStatement() @@ -4313,6 +4428,287 @@ RenameTableStatement RenameTableStatement(): { } } +/** + * DuckDB's CREATE [OR REPLACE] [TEMPORARY] MACRO name (parameters) AS [TABLE] body. + */ +CreateMacro CreateMacro(boolean isUsingOrReplace) #CreateMacro: +{ + CreateMacro createMacro = new CreateMacro(); + List parameters = new ArrayList(); + ObjectNames name; + Expression expression; + Select select; + String parameterName; + Expression defaultValue; +} +{ + { createMacro.setOrReplace(isUsingOrReplace); } + [ ( | ) { createMacro.setTemporary(true); } ] + name=RelObjectNames() { createMacro.setName(String.join(".", name.getNames())); } + "(" + [ + parameterName=RelObjectName() { defaultValue = null; } + [ ":=" defaultValue=Expression() ] + { parameters.add(new CreateMacro.Parameter(parameterName, defaultValue)); } + ( + "," parameterName=RelObjectName() { defaultValue = null; } + [ ":=" defaultValue=Expression() ] + { parameters.add(new CreateMacro.Parameter(parameterName, defaultValue)); } + )* + ] + ")" { createMacro.setParameters(parameters); } + + ( + LOOKAHEAD(2) select=Select() { createMacro.setSelect(select); } + | + expression=Expression() { createMacro.setExpression(expression); } + ) + { return createMacro; } +} + +/** + * DuckDB's COPY table [(columns)] FROM path [(options)] and + * COPY {table | (query)} TO path [(options)]. + */ +CopyStatement CopyStatement() #CopyStatement: +{ + CopyStatement copyStatement = new CopyStatement(); + ExpressionList columns = null; + List options = new ArrayList(); + String option; + Select select; + Table table; + Token token; +} +{ + + ( + LOOKAHEAD(2) "(" select=Select() ")" { copyStatement.setSelect(select); } + | + table=Table() { copyStatement.setTable(table); } + [ LOOKAHEAD(2) "(" columns=ColumnList() ")" { copyStatement.setColumns(columns); } ] + ) + ( + { copyStatement.setFrom(true); } + | + + ) + ( + token= { copyStatement.setPath(token.image); } + | + token= { copyStatement.setPath(token.image); } + ) + [ + LOOKAHEAD(2) "(" option=DatabaseOption() { options.add(option); } + ( "," option=DatabaseOption() { options.add(option); } )* + ")" { copyStatement.setOptions(options); } + ] + { return copyStatement; } +} + +/** + * PREPARE name AS statement, storing a parameterised statement for later EXECUTE. + */ +PrepareStatement PrepareStatement() #PrepareStatement: +{ + PrepareStatement prepareStatement = new PrepareStatement(); + ObjectNames name; + Statement statement; +} +{ + name=RelObjectNames() + { prepareStatement.setName(String.join(".", name.getNames())); } + statement=SingleStatement() { prepareStatement.setStatement(statement); } + { return prepareStatement; } +} + +/** + * DEALLOCATE [PREPARE] name. + */ +DeallocateStatement DeallocateStatement() #DeallocateStatement: +{ + DeallocateStatement deallocateStatement = new DeallocateStatement(); + ObjectNames name; +} +{ + + [ LOOKAHEAD(2) { deallocateStatement.setUsingPrepareKeyword(true); } ] + name=RelObjectNames() { deallocateStatement.setName(String.join(".", name.getNames())); } + { return deallocateStatement; } +} + +/** + * DuckDB's CONNECT target [AS alias], routing the session to a remote database. + */ +ConnectStatement ConnectStatement() #ConnectStatement: +{ + ConnectStatement connectStatement = new ConnectStatement(); + ObjectNames name; + Token token; +} +{ + + ( + token= { connectStatement.setTarget(token.image); } + | + name=RelObjectNames() { connectStatement.setTarget(String.join(".", name.getNames())); } + ) + [ name=RelObjectNames() { connectStatement.setAlias(String.join(".", name.getNames())); } ] + { return connectStatement; } +} + +/** + * DuckDB's DISCONNECT [name]. + */ +DisconnectStatement DisconnectStatement() #DisconnectStatement: +{ + DisconnectStatement disconnectStatement = new DisconnectStatement(); + ObjectNames name; +} +{ + + [ + LOOKAHEAD(2) + name=RelObjectNames() { disconnectStatement.setName(String.join(".", name.getNames())); } + ] + { return disconnectStatement; } +} + +/** + * DuckDB's ATTACH [DATABASE] [IF NOT EXISTS] path [AS alias] [(options)]. + */ +AttachStatement AttachStatement() #AttachStatement: +{ + AttachStatement attachStatement = new AttachStatement(); + List options = new ArrayList(); + String option; + ObjectNames name; + Token token; +} +{ + + // greedy: DATABASE right after ATTACH is the keyword, not the database name + [ LOOKAHEAD(2) { attachStatement.setUsingDatabaseKeyword(true); } ] + [ LOOKAHEAD(2) { attachStatement.setIfNotExists(true); } ] + ( + token= { attachStatement.setDatabasePath(token.image); } + | + name=RelObjectNames() { attachStatement.setDatabasePath(String.join(".", name.getNames())); } + ) + [ name=RelObjectNames() { attachStatement.setAlias(String.join(".", name.getNames())); } ] + [ + LOOKAHEAD(2) "(" option=DatabaseOption() { options.add(option); } + ( "," option=DatabaseOption() { options.add(option); } )* + ")" { attachStatement.setOptions(options); } + ] + { return attachStatement; } +} + +/** + * One ATTACH or COPY option, such as READ_ONLY, TYPE SQLITE or FORMAT PARQUET. Option names and + * values are free-form keywords, so the tokens are collected verbatim up to the next comma or the + * closing bracket. + */ +JAVACODE +String DatabaseOption() { + StringBuilder builder = new StringBuilder(); + int depth = 0; + while (true) { + Token token = getToken(1); + if (token == null || token.kind == EOF) { + break; + } + if (depth == 0 && (",".equals(token.image) || ")".equals(token.image))) { + break; + } + if ("(".equals(token.image)) { + depth++; + } else if (")".equals(token.image)) { + depth--; + } + getNextToken(); + if (builder.length() > 0) { + builder.append(" "); + } + builder.append(token.image); + } + return builder.toString(); +} + +/** + * DuckDB's DETACH [DATABASE] [IF EXISTS] name. + */ +DetachStatement DetachStatement() #DetachStatement: +{ + DetachStatement detachStatement = new DetachStatement(); + ObjectNames name; +} +{ + + // greedy: DATABASE right after DETACH is the keyword, not the database name + [ LOOKAHEAD(2) { detachStatement.setUsingDatabaseKeyword(true); } ] + [ LOOKAHEAD(2) { detachStatement.setIfExists(true); } ] + name=RelObjectNames() { detachStatement.setDatabaseName(String.join(".", name.getNames())); } + { return detachStatement; } +} + +/** + * DuckDB's [FORCE] INSTALL extension [FROM repository] and LOAD extension. + */ +ExtensionStatement ExtensionStatement() #ExtensionStatement: +{ + ExtensionStatement extensionStatement = new ExtensionStatement(); + ObjectNames name; + Token token; +} +{ + [ { extensionStatement.setForce(true); } ] + ( + { extensionStatement.setOperation(ExtensionStatement.Operation.INSTALL); } + | + { extensionStatement.setOperation(ExtensionStatement.Operation.LOAD); } + ) + ( + token= { extensionStatement.setExtensionName(token.image); } + | + name=RelObjectNames() { extensionStatement.setExtensionName(String.join(".", name.getNames())); } + ) + [ + LOOKAHEAD(2) + ( + token= { extensionStatement.setRepository(token.image); } + | + name=RelObjectNames() { extensionStatement.setRepository(String.join(".", name.getNames())); } + ) + ] + { return extensionStatement; } +} + +/** + * DuckDB/SQLite PRAGMA name [(arguments) | = value]. + */ +PragmaStatement PragmaStatement() #PragmaStatement: +{ + PragmaStatement pragmaStatement = new PragmaStatement(); + ObjectNames name; + ExpressionList parameters; + Expression value; +} +{ + name=RelObjectNames() + { pragmaStatement.setName(String.join(".", name.getNames())); } + // greedy: parentheses right after the pragma name are its arguments + [ + LOOKAHEAD(2) + ( + "(" parameters=ExpressionList() ")" { pragmaStatement.setParameters(parameters); } + | + "=" value=SimpleExpression() { pragmaStatement.setValue(value); } + ) + ] + { return pragmaStatement; } +} + PurgeStatement PurgeStatement(): { PurgeStatement purgeStatement = null; Table table; @@ -4346,11 +4742,19 @@ PurgeStatement PurgeStatement(): { DescribeStatement Describe(): { Table table; + Select select; DescribeStatement stmt = new DescribeStatement(); Token tk = null; } { (tk= | tk=) - table = Table() { stmt.setDescribeType(tk.image).setTable(table); } + ( + // DuckDB describes the result of a query, not only a table + LOOKAHEAD({ getToken(1).kind == K_SELECT || getToken(1).kind == K_WITH + || getToken(1).kind == K_FROM || getToken(1).kind == OPENING_BRACKET }) + select = Select() { stmt.setDescribeType(tk.image).setSelect(select); } + | + table = Table() { stmt.setDescribeType(tk.image).setTable(table); } + ) { return stmt; } @@ -5114,12 +5518,26 @@ Insert Insert(): ] table=TableReference() [ LOOKAHEAD(2) "(" partitions=Partitions() ")" ] - [ LOOKAHEAD({ isAliasAhead() && !(getToken(1).kind == K_OVERRIDING + [ LOOKAHEAD({ isAliasAhead() && !isColumnMatchingAhead() + && !(getToken(1).kind == K_OVERRIDING && (getToken(2).kind == K_SYSTEM || getToken(2).kind == K_USER)) }) [ { useAs = true; } ] name=RelObjectName() { table.setAlias(new Alias(name,useAs)); }] [ LOOKAHEAD(2) "(" columns=ColumnList() ")" ] + // DuckDB: INSERT INTO tbl BY NAME|POSITION SELECT ... + [ + LOOKAHEAD({ isColumnMatchingAhead() }) + + ( + { insert.setColumnMatching(Insert.ColumnMatching.BY_NAME); } + | + // POSITION is lexed as a string-function name + + { insert.setColumnMatching(Insert.ColumnMatching.BY_POSITION); } + ) + ] + [ LOOKAHEAD(2) ( { insert.setOverridingMode(Insert.OverridingMode.SYSTEM); } | { insert.setOverridingMode(Insert.OverridingMode.USER); } ) ] @@ -5836,6 +6254,7 @@ SampleClause SampleClause(): Number offsetArgument = null; Number repeatArgument=null; Number seedArgument=null; + boolean methodInBrackets = false; } { ( @@ -5852,9 +6271,13 @@ SampleClause SampleClause(): ) | ( - // Duck DB + // Duck DB: the method is optional and may follow the sample size instead { keyword = "USING SAMPLE"; } - ( token = | token = ) { method = token.image; } + [ + LOOKAHEAD({ isSampleMethod(getToken(1)) }) + ( token = | token = | token = ) + { method = token.image; } + ] ) ) @@ -5874,12 +6297,34 @@ SampleClause SampleClause(): [ LOOKAHEAD(2) "(" seedArgument = Number() ")" ] | percentageArgument = Number() { argumentInBrackets = false; } + // greedy: a unit directly after the sample size always belongs to the clause + [ + LOOKAHEAD(1) + ( + "%" { percentageUnit="%"; } + | + { percentageUnit="PERCENT"; } + | + { percentageUnit="ROWS"; } + ) + ] [ LOOKAHEAD(2) offsetArgument = Number() ] ) + // DuckDB: USING SAMPLE 10 PERCENT (bernoulli[, seed]) + [ + LOOKAHEAD({ method == null && getToken(1).kind == OPENING_BRACKET + && isSampleMethod(getToken(2)) }) + "(" ( token = | token = | token = ) + { method = token.image; methodInBrackets = true; } + [ "," seedArgument = Number() ] + ")" + ] + { sampleClause = new SampleClause(keyword, method, percentageArgument, percentageUnit, repeatArgument, seedArgument); sampleClause.setArgumentInBrackets(argumentInBrackets); + sampleClause.setMethodInBrackets(methodInBrackets); sampleClause.setOffsetArgument(offsetArgument); return sampleClause; } @@ -5911,12 +6356,16 @@ Select Select() #Select: { [ with=WithList() ] ( + LOOKAHEAD({ isFromFirstSelectAhead() }) select = FromFirstSelect() + | LOOKAHEAD(3) select = FromQuery() | ( ( LOOKAHEAD(2) select = PivotQuery() | + LOOKAHEAD(2) select = UnPivotQuery() + | LOOKAHEAD(3) select = PlainSelect() | LOOKAHEAD(3) select = Values() @@ -5942,6 +6391,53 @@ Select Select() #Select: } } +/** + * DuckDB's FROM-first query: FROM tbl [SELECT ...] [WHERE ...] [GROUP BY ...] [HAVING ...] + * [QUALIFY ...] [ORDER BY ...] [LIMIT ...] [OFFSET ...]. Without a SELECT clause all columns + * are returned. + */ +PlainSelect FromFirstSelect() #FromFirstSelect: +{ + PlainSelect plainSelect = new PlainSelect(); + List> selectItems = null; + FromItem fromItem; + List joins = null; + Expression where = null; + GroupByElement groupBy = null; + Expression having = null; + Expression qualify = null; + List orderByElements = null; + Limit limit = null; + Offset offset = null; +} +{ + fromItem=FromItem() + { + plainSelect.setFromItem(fromItem); + plainSelect.setFromFirst(true); + } + [ LOOKAHEAD(2) joins=JoinsList() { plainSelect.setJoins(joins); } ] + // greedy: a SELECT right after the FROM clause belongs to this query + [ + LOOKAHEAD(2) + + [ LOOKAHEAD(2) ( | { plainSelect.setDistinct(new Distinct()); } ) ] + selectItems=SelectItemsList() { plainSelect.setSelectItems(selectItems); } + ] + [ LOOKAHEAD(2) where=WhereClause() { plainSelect.setWhere(where); } ] + [ LOOKAHEAD(2) groupBy=GroupByColumnReferences() { plainSelect.setGroupByElement(groupBy); } ] + [ LOOKAHEAD(2) having=Having() { plainSelect.setHaving(having); } ] + [ LOOKAHEAD(2) qualify=Qualify() { plainSelect.setQualify(qualify); } ] + [ LOOKAHEAD( ) orderByElements=OrderByElements() + { plainSelect.setOrderByElements(orderByElements); } ] + [ LOOKAHEAD() limit=LimitWithOffset() { plainSelect.setLimit(limit); } ] + [ LOOKAHEAD() offset=Offset() { plainSelect.setOffset(offset); } ] + { + linkAST(plainSelect, jjtThis); + return plainSelect; + } +} + FromQuery FromQuery() #FromQuery: { FromQuery fromQuery; @@ -6878,6 +7374,7 @@ WithItem WithItem() #WithItem: ParenthesedStatement statement = null; WithSearchClause withSearchClause = null; WithCycleClause withCycleClause = null; + ExpressionList usingKeyExpressions = null; WithItem withItem; } { @@ -6892,6 +7389,8 @@ WithItem WithItem() #WithItem: [ LOOKAHEAD(2) { recursive = true; } ] name=RelObjectName() [ "(" selectItems=SelectItemsList() ")" ] + // DuckDB 2.0: WITH RECURSIVE tbl (a, b) USING KEY (a, avg(b)) AS (...) + [ LOOKAHEAD(2) "(" usingKeyExpressions=ExpressionList() ")" ] [ LOOKAHEAD(2) [ { usingNot = true; } ] { materialized = true; } ] ( @@ -6907,6 +7406,7 @@ WithItem WithItem() #WithItem: withItem = new WithItem(statement, new Alias(name, false)) .withRecursive(recursive, usingNot, materialized) .withWithItemList(selectItems); + withItem.setUsingKeyExpressions(usingKeyExpressions); } ) ) @@ -7242,6 +7742,30 @@ List> PivotFunctionItems(): { return functionItems; } } +/** + * DuckDB's simplified UNPIVOT statement: + * UNPIVOT tbl ON columns [INTO NAME name VALUE values]. + */ +UnPivotQuery UnPivotQuery(): +{ + UnPivotQuery unPivotQuery = new UnPivotQuery(); + ExpressionList onExpressions; + ExpressionList valueColumns; + FromItem fromItem; + String nameColumn; +} +{ + fromItem=FromItem() { unPivotQuery.setFromItem(fromItem); } + onExpressions=ExpressionList() { unPivotQuery.setOnExpressions(onExpressions); } + // greedy: INTO after the ON list belongs to this UNPIVOT + [ + LOOKAHEAD(2) + nameColumn=RelObjectName() { unPivotQuery.setNameColumn(nameColumn); } + valueColumns=ColumnList() { unPivotQuery.setValueColumns(valueColumns); } + ] + { return unPivotQuery; } +} + PivotQuery PivotQuery(): { PivotQuery pivotQuery = new PivotQuery(); @@ -7724,6 +8248,7 @@ Join JoinerExpression() #JoinerExpression: List columns = null; KSQLJoinWindow joinWindow = null; JoinHint joinHint = null; + Token tk = null; } { [ LOOKAHEAD({ isAsOfJoinAhead() }) { join.setAsOf(true); } ] @@ -7733,15 +8258,19 @@ Join JoinerExpression() #JoinerExpression: [ ( - { join.setLeft(true); } [ { join.setSemi(true); } | { join.setOuter(true); } | { join.setAny(true); } | { join.setAll(true); } ] + { join.setLeft(true); } [ { join.setSemi(true); } | { join.setAnti(true); } | { join.setOuter(true); } | { join.setAny(true); } | { join.setAll(true); } ] | ( { join.setRight(true); } | { join.setFull(true); } - ) [ { join.setOuter(true); } | { join.setAny(true); } | { join.setAll(true); } ] + ) [ { join.setSemi(true); } | { join.setAnti(true); } | { join.setOuter(true); } | { join.setAny(true); } | { join.setAll(true); } ] | { join.setInner(true); } + | + // DuckDB allows SEMI/ANTI without a LEFT prefix + LOOKAHEAD({ isSemiAntiJoinAhead() }) + ( { join.setSemi(true); } | { join.setAnti(true); } ) ) | { join.setCross(true); } @@ -7775,6 +8304,12 @@ Join JoinerExpression() #JoinerExpression: right=FromItem() + // DuckDB 2.0: JOIN products APPROX NEAREST 2 BY SIMILARITY array_cosine_similarity(...) + [ + LOOKAHEAD({ isApproxNearestAhead() }) + tk= { join.setApproxNearest(Long.valueOf(tk.image)); } + onExpression=Expression() { join.setSimilarity(onExpression); } + ] [ LOOKAHEAD(2) ( [ joinWindow = KSQLJoinWindowClause() {join.setJoinWindow(joinWindow);} ] @@ -8981,6 +9516,7 @@ Expression LikeExpression(Expression leftExpression) #LikeExpression: | token = | token = | token = + | token = | token = | token = | token = @@ -12594,7 +13130,9 @@ List IndexAlgorithmOrLockOption(): { ( keyword= | keyword= ) { option.add(keyword.image); } [ "=" { option.add("="); } ] - ( value= | value= | value= | value= ) + // COPY is one of MySQL's ALGORITHM values and also starts the COPY statement + ( value= | value= | value= | value= + | value= ) { option.add(value.image); return option; } } @@ -13827,6 +14365,9 @@ ColDataType ColDataType(): | tk= | + // DuckDB parameterises types with other types: MAP(VARCHAR, INTEGER) + tk= + | tk= ) ) @@ -16316,14 +16857,20 @@ RollbackStatement RollbackStatement(): { { rollbackStatement = new RollbackStatement(); } [ { rollbackStatement.setUsingWorkKeyword(true); } ] - [ ( - [ { rollbackStatement.setUsingSavepointKeyword(true); }] - token= { rollbackStatement.setSavepointName(token.image); } - ) - | + // explicit lookahead: FORCE INSTALL starts a new statement, FORCE 'id' belongs here + [ + LOOKAHEAD(2) ( - token= { rollbackStatement.setForceDistributedTransactionIdentifier(token.image); } - ) ] + ( + [ { rollbackStatement.setUsingSavepointKeyword(true); }] + token= { rollbackStatement.setSavepointName(token.image); } + ) + | + ( + token= { rollbackStatement.setForceDistributedTransactionIdentifier(token.image); } + ) + ) + ] { return rollbackStatement; @@ -16679,6 +17226,7 @@ CreateTrigger PostgreSqlTrigger(boolean replace): { CreateTrigger result = new CreateTrigger(); String name; Table table; CreateTrigger.Timing timing; TriggerEvent event; TransitionRelation transition; Expression expression; Token token; boolean negative = false; + Statement body = null; Function routine = new Function(); ExpressionList arguments = new ExpressionList(); } { @@ -16691,7 +17239,8 @@ CreateTrigger PostgreSqlTrigger(boolean replace): event=PostgreSqlTriggerEvent() { result.getEvents().add(event); } ( event=PostgreSqlTriggerEvent() { result.getEvents().add(event); } )* table=Table() { result.setTable(table); } - [ table=Table() { result.setReferencedTable(table); } ] + // greedy: FROM here names the referenced table, it never starts the trigger body + [ LOOKAHEAD(2) table=Table() { result.setReferencedTable(table); } ] [ [ { negative = true; } ] { result.getConstraintAttributes().setDeferrable(!negative); } ] [ LOOKAHEAD({ isAccessKeywordAhead("INITIALLY") }) AccessKeyword("INITIALLY") ( token= ) @@ -16704,12 +17253,18 @@ CreateTrigger PostgreSqlTrigger(boolean replace): ( { result.setOrientation(CreateTrigger.Orientation.ROW); } | { result.setOrientation(CreateTrigger.Orientation.STATEMENT); } ) ] [ "(" expression=Expression() ")" { result.setWhenExpression(expression); } ] - ( | { result.setExecuteKeyword(CreateTrigger.ExecuteKeyword.PROCEDURE); } ) - name=AccessQualifiedName() { routine.setName(name); } - "(" [ expression=PostgreSqlTriggerArgument() { arguments.add(expression); } - ( "," expression=PostgreSqlTriggerArgument() { arguments.add(expression); } )* ] ")" + ( + LOOKAHEAD(2) + ( | { result.setExecuteKeyword(CreateTrigger.ExecuteKeyword.PROCEDURE); } ) + name=AccessQualifiedName() { routine.setName(name); } + "(" [ expression=PostgreSqlTriggerArgument() { arguments.add(expression); } + ( "," expression=PostgreSqlTriggerArgument() { arguments.add(expression); } )* ] ")" + { routine.setParameters(arguments); result.setRoutine(routine); } + | + // DuckDB 2.0 runs a statement rather than a function + body=SingleStatement() { result.setBody(body); } + ) { - routine.setParameters(arguments); result.setRoutine(routine); requireAccessSyntax(!result.isConstraint() || !replace && result.getTiming() == CreateTrigger.Timing.AFTER && result.getOrientation() == CreateTrigger.Orientation.ROW, "A constraint trigger requires AFTER FOR EACH ROW without OR REPLACE"); requireAccessSyntax(result.isConstraint() || result.getReferencedTable() == null @@ -16894,6 +17449,12 @@ Statement Create(): | LOOKAHEAD() { requireTypeDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for CREATE DOMAIN"); } statement = CreateDomain() | + LOOKAHEAD({ isKeywordAhead("EXTENSION") + && "REPOSITORY".equalsIgnoreCase(getToken(2).image) }) + { requireTypeDdlSyntax(!isUsingOrReplace, + "OR REPLACE is not supported for CREATE EXTENSION REPOSITORY"); } + statement = CreateExtensionRepository() + | LOOKAHEAD({ isKeywordAhead("EXTENSION") }) { requireTypeDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for CREATE EXTENSION"); } statement = CreateExtension() | LOOKAHEAD({ isKeywordAhead("PUBLICATION") }) @@ -16922,6 +17483,8 @@ Statement Create(): | statement = CreateSynonym(isUsingOrReplace) | + LOOKAHEAD({ isCreateMacroAhead() }) statement = CreateMacro(isUsingOrReplace) + | LOOKAHEAD(3) statement = CreateTable(isUsingOrReplace) | LOOKAHEAD(2) statement = CreateView(isUsingOrReplace) @@ -17295,6 +17858,28 @@ AlterDomain AlterDomain(): { return result; } } +/** + * DuckDB 2.0's CREATE EXTENSION REPOSITORY [IF NOT EXISTS] name [WITH PREFIX prefix]. + */ +CreateExtensionRepository CreateExtensionRepository() #CreateExtensionRepository: +{ + CreateExtensionRepository createExtensionRepository = new CreateExtensionRepository(); + Expression prefix; + String name; +} +{ + TypeDdlKeyword("EXTENSION") TypeDdlKeyword("REPOSITORY") + [ LOOKAHEAD(3) + { createExtensionRepository.setIfNotExists(true); } ] + name=RelObjectName() { createExtensionRepository.setName(name); } + // greedy: WITH PREFIX belongs to the repository definition + [ + LOOKAHEAD(2) TypeDdlKeyword("PREFIX") prefix=SimpleExpression() + { createExtensionRepository.setPrefix(prefix); } + ] + { return createExtensionRepository; } +} + CreateExtension CreateExtension(): { CreateExtension result = new CreateExtension(); String name; Expression version; } { diff --git a/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java b/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java index aad68683b..68e98f101 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/DuckDBTest.java @@ -11,6 +11,21 @@ import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.DescribeStatement; +import net.sf.jsqlparser.statement.PragmaStatement; +import net.sf.jsqlparser.statement.ExtensionStatement; +import net.sf.jsqlparser.statement.AttachStatement; +import net.sf.jsqlparser.statement.DetachStatement; +import net.sf.jsqlparser.statement.ConnectStatement; +import net.sf.jsqlparser.statement.DisconnectStatement; +import net.sf.jsqlparser.statement.PrepareStatement; +import net.sf.jsqlparser.statement.DeallocateStatement; +import net.sf.jsqlparser.statement.CopyStatement; +import net.sf.jsqlparser.statement.create.macro.CreateMacro; +import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -36,4 +51,642 @@ void testCreateWithStruct() throws JSQLParserException { ");"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testGlobOperator() throws JSQLParserException { + String sqlStr = "SELECT * FROM t WHERE b GLOB 'y*'"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testNotGlobOperator() throws JSQLParserException { + String sqlStr = "SELECT * FROM t WHERE b NOT GLOB 'y*'"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testGlobRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT glob FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testSemiJoinWithoutLeft() throws JSQLParserException { + String sqlStr = "SELECT * FROM t SEMI JOIN u USING (id)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Join join = select.getJoins().get(0); + + Assertions.assertTrue(join.isSemi()); + Assertions.assertFalse(join.isLeft()); + Assertions.assertNull(((Table) select.getFromItem()).getAlias()); + } + + @Test + void testAntiJoinWithoutLeft() throws JSQLParserException { + String sqlStr = "SELECT * FROM t ANTI JOIN u ON t.id = u.id"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Join join = select.getJoins().get(0); + + Assertions.assertTrue(join.isAnti()); + Assertions.assertNull(((Table) select.getFromItem()).getAlias()); + } + + @Test + void testLeftAntiJoin() throws JSQLParserException { + String sqlStr = "SELECT * FROM t LEFT ANTI JOIN u ON t.id = u.id"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Join join = select.getJoins().get(0); + + Assertions.assertTrue(join.isLeft()); + Assertions.assertTrue(join.isAnti()); + } + + @Test + void testAntiRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT anti FROM t anti"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testMapColumnType() throws JSQLParserException { + String sqlStr = "CREATE TABLE t (m MAP(VARCHAR, INTEGER))"; + CreateTable createTable = + (CreateTable) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("MAP", + createTable.getColumnDefinitions().get(0).getColDataType().getDataType()); + Assertions.assertEquals(java.util.Arrays.asList("VARCHAR", "INTEGER"), + createTable.getColumnDefinitions().get(0).getColDataType() + .getArgumentsStringList()); + } + + @Test + void testMapCast() throws JSQLParserException { + String sqlStr = "SELECT CAST(m AS MAP(VARCHAR, INTEGER)) FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testDecimalTypeArgumentsStillParse() throws JSQLParserException { + String sqlStr = "CREATE TABLE t (a DECIMAL(10, 2), b VARCHAR(255))"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testInsertByName() throws JSQLParserException { + String sqlStr = "INSERT INTO t BY NAME SELECT 1 AS b, 2 AS a"; + Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(Insert.ColumnMatching.BY_NAME, insert.getColumnMatching()); + } + + @Test + void testInsertByPosition() throws JSQLParserException { + String sqlStr = "INSERT INTO t BY POSITION SELECT 1, 2"; + Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(Insert.ColumnMatching.BY_POSITION, insert.getColumnMatching()); + } + + @Test + void testInsertWithoutColumnMatching() throws JSQLParserException { + String sqlStr = "INSERT INTO t (a, b) VALUES (1, 2)"; + Insert insert = (Insert) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNull(insert.getColumnMatching()); + } + + @Test + void testUsingSamplePercentShorthand() throws JSQLParserException { + String sqlStr = "SELECT * FROM t USING SAMPLE 10%"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + SampleClause sampleClause = select.getFromItem().getSampleClause(); + + Assertions.assertEquals("%", sampleClause.getPercentageUnit()); + Assertions.assertNull(sampleClause.getMethod()); + } + + @Test + void testUsingSamplePercentWithMethodInBrackets() throws JSQLParserException { + String sqlStr = "SELECT * FROM t USING SAMPLE 10 PERCENT (bernoulli)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(SampleClause.SampleMethod.BERNOULLI, + select.getFromItem().getSampleClause().getMethod()); + } + + @Test + void testUsingSampleRowsWithSeed() throws JSQLParserException { + String sqlStr = "SELECT * FROM t USING SAMPLE 10 ROWS (system, 377)"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testUsingSampleReservoir() throws JSQLParserException { + String sqlStr = "SELECT * FROM t USING SAMPLE reservoir (50 ROWS)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(SampleClause.SampleMethod.RESERVOIR, + select.getFromItem().getSampleClause().getMethod()); + } + + @Test + void testDescribeQuery() throws JSQLParserException { + String sqlStr = "DESCRIBE SELECT * FROM t"; + DescribeStatement describe = + (DescribeStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNotNull(describe.getSelect()); + Assertions.assertNull(describe.getTable()); + } + + @Test + void testDescribeQueryTablesNamesFinder() throws JSQLParserException { + String sqlStr = "DESCRIBE SELECT * FROM ds.t"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.t"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } + + @Test + void testDescribeTableStillParses() throws JSQLParserException { + String sqlStr = "DESCRIBE t"; + DescribeStatement describe = + (DescribeStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("t", describe.getTable().getName()); + } + + @Test + void testPragmaWithArguments() throws JSQLParserException { + String sqlStr = "PRAGMA table_info('t')"; + PragmaStatement pragma = + (PragmaStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("table_info", pragma.getName()); + Assertions.assertEquals(1, pragma.getParameters().size()); + } + + @Test + void testPragmaAssignment() throws JSQLParserException { + String sqlStr = "PRAGMA memory_limit = '1GB'"; + PragmaStatement pragma = + (PragmaStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("'1GB'", pragma.getValue().toString()); + } + + @Test + void testPragmaWithoutArguments() throws JSQLParserException { + String sqlStr = "PRAGMA database_list"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testPragmaRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT pragma FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testLoadExtension() throws JSQLParserException { + String sqlStr = "LOAD httpfs"; + ExtensionStatement extension = + (ExtensionStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(ExtensionStatement.Operation.LOAD, extension.getOperation()); + Assertions.assertEquals("httpfs", extension.getExtensionName()); + } + + @Test + void testInstallExtension() throws JSQLParserException { + String sqlStr = "INSTALL spatial"; + ExtensionStatement extension = + (ExtensionStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(ExtensionStatement.Operation.INSTALL, extension.getOperation()); + Assertions.assertFalse(extension.isForce()); + } + + @Test + void testForceInstallExtensionFromRepository() throws JSQLParserException { + String sqlStr = "FORCE INSTALL h3 FROM community"; + ExtensionStatement extension = + (ExtensionStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(extension.isForce()); + Assertions.assertEquals("community", extension.getRepository()); + } + + @Test + void testInstallExtensionFromUrl() throws JSQLParserException { + String sqlStr = "INSTALL 'path/to/ext.duckdb_extension'"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testLoadRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT load, install FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testAttachDatabaseWithAlias() throws JSQLParserException { + String sqlStr = "ATTACH 'file.db' AS mydb"; + AttachStatement attach = + (AttachStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("'file.db'", attach.getDatabasePath()); + Assertions.assertEquals("mydb", attach.getAlias()); + } + + @Test + void testAttachDatabaseWithOptions() throws JSQLParserException { + String sqlStr = "ATTACH DATABASE IF NOT EXISTS 'file.db' AS mydb (TYPE SQLITE, READ_ONLY)"; + AttachStatement attach = + (AttachStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(attach.isIfNotExists()); + Assertions.assertTrue(attach.isUsingDatabaseKeyword()); + Assertions.assertEquals(java.util.Arrays.asList("TYPE SQLITE", "READ_ONLY"), + attach.getOptions()); + } + + @Test + void testDetachDatabase() throws JSQLParserException { + String sqlStr = "DETACH mydb"; + DetachStatement detach = + (DetachStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("mydb", detach.getDatabaseName()); + } + + @Test + void testDetachDatabaseIfExists() throws JSQLParserException { + String sqlStr = "DETACH DATABASE IF EXISTS mydb"; + DetachStatement detach = + (DetachStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(detach.isIfExists()); + } + + @Test + void testAttachRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT attach, detach FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testConnectToRemoteDatabase() throws JSQLParserException { + String sqlStr = "CONNECT 'postgres://localhost/mydb'"; + ConnectStatement connect = + (ConnectStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("'postgres://localhost/mydb'", connect.getTarget()); + Assertions.assertNull(connect.getAlias()); + } + + @Test + void testConnectWithAlias() throws JSQLParserException { + String sqlStr = "CONNECT 'postgres://localhost/mydb' AS remote"; + ConnectStatement connect = + (ConnectStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("remote", connect.getAlias()); + } + + @Test + void testDisconnect() throws JSQLParserException { + String sqlStr = "DISCONNECT"; + DisconnectStatement disconnect = + (DisconnectStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNull(disconnect.getName()); + } + + @Test + void testDisconnectNamedConnection() throws JSQLParserException { + String sqlStr = "DISCONNECT remote"; + DisconnectStatement disconnect = + (DisconnectStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("remote", disconnect.getName()); + } + + @Test + void testConnectByStillParses() throws JSQLParserException { + String sqlStr = "SELECT * FROM t START WITH id = 1 CONNECT BY PRIOR id = parent_id"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testPrepareStatement() throws JSQLParserException { + String sqlStr = "PREPARE q AS SELECT * FROM t WHERE a = $1"; + PrepareStatement prepare = + (PrepareStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("q", prepare.getName()); + Assertions.assertNotNull(prepare.getStatement()); + } + + @Test + void testPrepareInsertStatement() throws JSQLParserException { + String sqlStr = "PREPARE ins AS INSERT INTO t VALUES ($1, $2)"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testPrepareTablesNamesFinder() throws JSQLParserException { + String sqlStr = "PREPARE q AS SELECT * FROM ds.t"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.t"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } + + @Test + void testDeallocatePreparedStatement() throws JSQLParserException { + String sqlStr = "DEALLOCATE PREPARE q"; + DeallocateStatement deallocate = + (DeallocateStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("q", deallocate.getName()); + Assertions.assertTrue(deallocate.isUsingPrepareKeyword()); + } + + @Test + void testExecutePreparedStatement() throws JSQLParserException { + String sqlStr = "EXECUTE q (1)"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCopyFromFile() throws JSQLParserException { + String sqlStr = "COPY t FROM 'in.csv'"; + CopyStatement copy = + (CopyStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(copy.isFrom()); + Assertions.assertEquals("t", copy.getTable().getName()); + Assertions.assertEquals("'in.csv'", copy.getPath()); + } + + @Test + void testCopyColumnsFromFileWithOptions() throws JSQLParserException { + String sqlStr = "COPY t (a, b) FROM 'in.csv' (AUTO_DETECT true)"; + CopyStatement copy = + (CopyStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(2, copy.getColumns().size()); + Assertions.assertEquals(java.util.Collections.singletonList("AUTO_DETECT true"), + copy.getOptions()); + } + + @Test + void testCopyQueryToFile() throws JSQLParserException { + String sqlStr = + "COPY (SELECT * FROM t) TO 'out.parquet' (FORMAT PARQUET, COMPRESSION ZSTD)"; + CopyStatement copy = + (CopyStatement) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertFalse(copy.isFrom()); + Assertions.assertNotNull(copy.getSelect()); + } + + @Test + void testCopyTableToFile() throws JSQLParserException { + String sqlStr = "COPY t TO 'out.csv' (FORMAT CSV, DELIMITER '|')"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCopyTablesNamesFinder() throws JSQLParserException { + String sqlStr = "COPY ds.t FROM 'in.csv'"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.t"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } + + @Test + void testCreateScalarMacro() throws JSQLParserException { + String sqlStr = "CREATE MACRO add_one(a) AS a + 1"; + CreateMacro createMacro = + (CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("add_one", createMacro.getName()); + Assertions.assertEquals(1, createMacro.getParameters().size()); + Assertions.assertFalse(createMacro.isTable()); + } + + @Test + void testCreateOrReplaceTemporaryMacroWithDefault() throws JSQLParserException { + String sqlStr = "CREATE OR REPLACE TEMPORARY MACRO add(a, b := 5) AS a + b"; + CreateMacro createMacro = + (CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(createMacro.isOrReplace()); + Assertions.assertTrue(createMacro.isTemporary()); + Assertions.assertEquals("5", + createMacro.getParameters().get(1).getDefaultValue().toString()); + } + + @Test + void testCreateTableMacro() throws JSQLParserException { + String sqlStr = "CREATE MACRO get_t(i) AS TABLE SELECT * FROM t WHERE id = i"; + CreateMacro createMacro = + (CreateMacro) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(createMacro.isTable()); + Assertions.assertNotNull(createMacro.getSelect()); + } + + @Test + void testCreateExtensionRepository() throws JSQLParserException { + String sqlStr = + "CREATE EXTENSION REPOSITORY my_repo WITH PREFIX 'https://extensions.example.org'"; + CreateExtensionRepository createRepository = + (CreateExtensionRepository) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("my_repo", createRepository.getName()); + Assertions.assertEquals("'https://extensions.example.org'", + createRepository.getPrefix().toString()); + } + + @Test + void testCreateExtensionRepositoryIfNotExists() throws JSQLParserException { + String sqlStr = "CREATE EXTENSION REPOSITORY IF NOT EXISTS my_repo"; + CreateExtensionRepository createRepository = + (CreateExtensionRepository) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(createRepository.isIfNotExists()); + } + + @Test + void testCreatePostgresExtensionStillParses() throws JSQLParserException { + String sqlStr = "CREATE EXTENSION IF NOT EXISTS hstore WITH SCHEMA public CASCADE"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testCreateTriggerWithTransitionTablesAndStatementBody() throws JSQLParserException { + String sqlStr = "CREATE TRIGGER trg_audit AFTER UPDATE ON target " + + "REFERENCING OLD TABLE AS o NEW TABLE AS n FOR EACH STATEMENT " + + "INSERT INTO audit SELECT n.id, o.val, n.val FROM o JOIN n ON o.id = n.id"; + CreateTrigger createTrigger = + (CreateTrigger) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals(2, createTrigger.getTransitionRelations().size()); + Assertions.assertEquals(CreateTrigger.Orientation.STATEMENT, + createTrigger.getOrientation()); + Assertions.assertNotNull(createTrigger.getBody()); + } + + @Test + void testCreateTriggerBeforeDeleteWithStatementBody() throws JSQLParserException { + String sqlStr = "CREATE TRIGGER trg BEFORE DELETE ON t REFERENCING OLD TABLE AS o " + + "FOR EACH STATEMENT DELETE FROM audit WHERE id IN (SELECT id FROM o)"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testPostgreSqlTriggerStillParses() throws JSQLParserException { + String sqlStr = "CREATE TRIGGER trg AFTER UPDATE ON target " + + "REFERENCING OLD TABLE AS o NEW TABLE AS n FOR EACH STATEMENT " + + "EXECUTE FUNCTION f()"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testFromFirstWithSelect() throws JSQLParserException { + String sqlStr = "FROM t SELECT a, b"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(select.isFromFirst()); + Assertions.assertEquals(2, select.getSelectItems().size()); + } + + @Test + void testFromFirstWithWhere() throws JSQLParserException { + String sqlStr = "FROM t WHERE a > 1"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertTrue(select.isFromFirst()); + Assertions.assertNull(select.getSelectItems()); + } + + @Test + void testFromFirstWithClauses() throws JSQLParserException { + String sqlStr = "FROM t SELECT a, count(*) WHERE a > 1 GROUP BY a HAVING count(*) > 2 " + + "ORDER BY a LIMIT 10"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testFromFirstWithJoin() throws JSQLParserException { + String sqlStr = "FROM t JOIN u ON t.id = u.id SELECT t.a"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testFromFirstTablesNamesFinder() throws JSQLParserException { + String sqlStr = "FROM ds.t WHERE a > 1"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.t"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } + + @Test + void testBarePipeQueryStillParses() throws JSQLParserException { + String sqlStr = "FROM t |> WHERE a > 1"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testUnPivotStatement() throws JSQLParserException { + String sqlStr = "UNPIVOT monthly_sales ON jan, feb INTO NAME month VALUE sales"; + UnPivotQuery unPivotQuery = + (UnPivotQuery) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertEquals("month", unPivotQuery.getNameColumn()); + Assertions.assertEquals(2, unPivotQuery.getOnExpressions().size()); + Assertions.assertEquals(1, unPivotQuery.getValueColumns().size()); + } + + @Test + void testUnPivotStatementWithoutIntoClause() throws JSQLParserException { + String sqlStr = "UNPIVOT monthly_sales ON jan, feb"; + UnPivotQuery unPivotQuery = + (UnPivotQuery) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNull(unPivotQuery.getNameColumn()); + } + + @Test + void testUnPivotStatementTablesNamesFinder() throws JSQLParserException { + String sqlStr = "UNPIVOT ds.monthly_sales ON jan, feb INTO NAME month VALUE sales"; + + Assertions.assertEquals(java.util.Collections.singletonList("ds.monthly_sales"), + new net.sf.jsqlparser.util.TablesNamesFinder() + .getTableList(net.sf.jsqlparser.parser.CCJSqlParserUtil.parse(sqlStr))); + } + + @Test + void testUnPivotInFromClauseStillParses() throws JSQLParserException { + String sqlStr = "SELECT * FROM monthly_sales UNPIVOT (sales FOR month IN (jan, feb))"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testApproxNearestJoin() throws JSQLParserException { + String sqlStr = "SELECT * FROM queries q INNER JOIN products t " + + "APPROX NEAREST 2 BY SIMILARITY array_cosine_similarity(q.vec, t.vec)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + Join join = select.getJoins().get(0); + + Assertions.assertEquals(2L, join.getApproxNearest()); + Assertions.assertEquals("array_cosine_similarity(q.vec, t.vec)", + join.getSimilarity().toString()); + } + + @Test + void testApproxNearestJoinWithoutAlias() throws JSQLParserException { + String sqlStr = "SELECT * FROM queries JOIN products " + + "APPROX NEAREST 5 BY SIMILARITY array_distance(queries.vec, products.vec)"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNull(((Table) select.getJoins().get(0).getFromItem()).getAlias()); + } + + @Test + void testApproxRemainsUsableAsIdentifier() throws JSQLParserException { + String sqlStr = "SELECT approx, nearest, similarity FROM t"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testRecursiveCteUsingKey() throws JSQLParserException { + String sqlStr = "WITH RECURSIVE tbl (a, b) USING KEY (a, avg(b)) AS " + + "(SELECT 1, 2 UNION ALL SELECT a + 1, b FROM tbl WHERE a < 3) SELECT * FROM tbl"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + WithItem withItem = select.getWithItemsList().get(0); + + Assertions.assertEquals(2, withItem.getUsingKeyExpressions().size()); + } + + @Test + void testRecursiveCteUsingKeySingleColumn() throws JSQLParserException { + String sqlStr = "WITH RECURSIVE tbl USING KEY (a) AS (SELECT 1 AS a) SELECT * FROM tbl"; + TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + void testRecursiveCteWithoutUsingKey() throws JSQLParserException { + String sqlStr = "WITH RECURSIVE tbl (a) AS (SELECT 1) SELECT * FROM tbl"; + PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); + + Assertions.assertNull(select.getWithItemsList().get(0).getUsingKeyExpressions()); + } }