How to use it¶
JSQLParser turns SQL text into a tree of Java objects, lets you inspect or rewrite that tree, and prints it back out as SQL. Everything on this page is built on those three moves.
Statement statement = CCJSqlParserUtil.parse("SELECT a FROM my_table WHERE id = 42");
// inspect it
PlainSelect select = (PlainSelect) statement;
Table table = (Table) select.getFromItem(); // my_table
// print it back
String sql = statement.toString();
Tip
New here? Read Add JSQLParser to your Project, then Parse a SQL Statement, then Explore the Parsed Tree. Everything after that is optional and can be read in any order.
Section |
Use it when you want to … |
|---|---|
pull in the dependency, and pick between the Manticore and upstream builds |
|
turn SQL text into Java objects |
|
find your way around the object model |
|
know whether SQL reads, writes or returns rows — before you run it |
|
list every table a statement touches |
|
walk the whole tree and react to specific nodes |
|
construct SQL from Java instead of from text |
|
keep going when one statement in a script is broken |
|
parse T-SQL brackets, MySQL escapes, BigQuery quoting … |
|
build JSQLParser yourself or contribute |
Add JSQLParser to your Project¶
There are two sets of artifacts on Maven Central, built from the same source under the same dual licence:
Artifact |
|
Cut from |
|---|---|---|
Manticore build (recommended) |
|
the current development line, released continuously |
Upstream release |
|
the official release cadence |
Upstream snapshot |
|
the latest commit, overwritten in place |
Upstream releases are cut infrequently. Between two of them a lot of grammar and performance work lands — the 11× parse speed-up, JavaCC 8 support, new dialect syntax — and waiting for the next official version to catch up can mean months on a build that already has the fix you need.
Snapshots are not the answer either: a -SNAPSHOT coordinate is mutable, so the same version string can resolve to different bytes tomorrow. That is fine for trying something out and wrong for a reproducible build.
The Manticore builds fill that gap. Each one is an immutable, versioned release published to Maven Central from the current development line, so you get the fixes early and a build that stays reproducible. Use them unless you have a reason to pin to the official release — and note the different groupId, the artifact name is the same.
<dependency>
<groupId>com.manticore-projects.jsqlformatter</groupId>
<artifactId>jsqlparser</artifactId>
<version>[5.3.218,)</version>
</dependency>
The range [5.3.218,) takes the newest available build. Pin an exact version instead once you ship.
repositories {
mavenCentral()
}
dependencies {
implementation 'com.manticore-projects.jsqlformatter:jsqlparser:+'
}
+ takes the newest available build. Pin an exact version instead once you ship.
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>5.4.272</version>
</dependency>
<repositories>
<repository>
<id>jsqlparser-snapshots</id>
<snapshots>
<enabled>true</enabled>
</snapshots>
<url>https://oss.sonatype.org/content/groups/public/</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>5.5.272-SNAPSHOT</version>
</dependency>
repositories {
mavenCentral()
}
dependencies {
implementation 'com.github.jsqlparser:jsqlparser:5.4.272'
}
repositories {
maven {
url = uri('https://oss.sonatype.org/content/groups/public/')
}
}
dependencies {
implementation 'com.github.jsqlparser:jsqlparser:5.5.272-SNAPSHOT'
}
Note
Features documented here may reach the Manticore builds before the next upstream release. If a class or method on this page is missing, check which of the two you are resolving.
Parse a SQL Statement¶
CCJSqlParserUtil.parse() is the entry point. It returns a Statement, which you cast to the concrete type you expect.
String sqlStr = "select 1 from dual where a=b";
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sqlStr);
SelectItem selectItem =
select.getSelectItems().get(0);
Assertions.assertEquals(
new LongValue(1)
, selectItem.getExpression());
Table table = (Table) select.getFromItem();
Assertions.assertEquals("dual", table.getName());
EqualsTo equalsTo = (EqualsTo) select.getWhere();
Column a = (Column) equalsTo.getLeftExpression();
Column b = (Column) equalsTo.getRightExpression();
Assertions.assertEquals("a", a.getColumnName());
Assertions.assertEquals("b", b.getColumnName());
For several statements at once, use CCJSqlParserUtil.parseStatements(), which returns a Statements — an ArrayList<Statement>.
Statements script = CCJSqlParserUtil.parseStatements(
"UPDATE t SET a = 1; SELECT a FROM t;");
assertEquals(2, script.size());
Note
Supported statement separators are semicolon ;, GO, slash / and two empty lines \n\n\n.
Use withBlankLinesAsSeparator(false) to treat empty lines as plain whitespace instead.
If parsing fails on syntax JSQLParser does not know, see Handle Parse Errors — and please open an issue, missing syntax gets added on demand.
Explore the Parsed Tree¶
The fastest way to learn the object model is to look at it. Paste your SQL into JSQLFormatter and it will draw the tree, with the Java class of every node:
SQL Text
└─Statements: net.sf.jsqlparser.statement.select.Select
├─selectItems -> Collection<SelectItem>
│ └─LongValue: 1
├─Table: dual
└─where: net.sf.jsqlparser.expression.operators.relational.EqualsTo
├─Column: a
└─Column: b
Read that as a map: each line is a getter away. select.getSelectItems(), select.getFromItem(), select.getWhere(). Once the tree gets deeper than a couple of levels, stop casting by hand and use Use the Visitor Patterns.
MySQL index hints, insert sources and DELETE … USING¶
A table reference keeps every MySQL index hint in Table.getIndexHints(), in
source order, for example t USE INDEX (i1) IGNORE INDEX FOR ORDER BY (i2).
MySQLIndexHint.getScope() returns JOIN, ORDER_BY or GROUP_BY for a
FOR clause and null without one. USE INDEX () has an empty index list;
IGNORE and FORCE require at least one index. getIndexHint() still returns
the first hint, and setHint() replaces all hints with one.
WEIGHT_STRING(str AS CHAR(n)) and WEIGHT_STRING(str AS BINARY(n)) keep the cast
in Function.getCastType(), a ColDataType as in CAST.
INSERT INTO t VALUES ROW(1, 2), ROW(3, 4) and INSERT INTO t TABLE s use the same
Values and TableStatement models as the queries VALUES ROW(...) and
TABLE s. INSERT INTO t () VALUES () has an empty, non-null
getColumns(); REPLACE accepts the same forms. toString() and
InsertDeParser both render through Insert.appendTo, which writes PARTITION
before the column list.
DELETE FROM t1 USING t1 JOIN t2 ON t1.id = t2.id keeps the joins of the USING
clause in Delete.getUsingJoins(), after getUsingFromItemList(). PostgreSQL’s
DELETE FROM t USING s JOIN u ON ... uses the same properties. getJoins()
holds the joins of the target, as in DELETE FROM t1, t2 USING ..., and is
rendered before USING.
PostgreSQL CHECK inheritance¶
With Dialect.POSTGRESQL, CREATE and ALTER CHECK constraints support
NO INHERIT. CheckConstraint.isNoInherit() reports this flag; use
setNoInherit or withNoInherit to change it. The check expression remains
an editable Expression visited by the existing table traversal and deparsers.
For ALTER TABLE t ADD CHECK (id > 0) NO INHERIT NOT VALID, the two options
are independent: isNoInherit() is true and
getConstraintAttributes().isNotValid() is true. Clearing one flag preserves
the other when the statement is rendered.
DROP targets¶
Drop keeps the parts of a DROP statement that are not plain object names in
typed properties:
FUNCTION, PROCEDURE, ROUTINE and AGGREGATE targets are
RoutineReferenceitems ingetRoutines(), the model that GRANT and COMMENT use. Every target keeps its own signature, soDROP PROCEDURE p(int)andDROP FUNCTION f(int), g(text)render as written. Null arguments mean an omitted signature; an empty list is an explicit().USER, ROLE and GROUP targets and the roles of PostgreSQL
DROP OWNED BYareUserAccountitems ingetAccounts(), read with the MySQL account-name rule that CREATE USER uses. MySQL accounts keep their host, as in'app'@'%', and a quoted PostgreSQL role name such as"Admin"keeps"ingetQuoteStr().DROP TRIGGER t ON tblandDROP RULE r ON tblexposetblthroughgetTable(), likeDROP INDEX i ON tbl, and table discovery reports it. SQL Server’sON DATABASEandON ALL SERVERtrigger scopes stay parameters.getBehavior()holds an explicit CASCADE or RESTRICT as aCascadeBehavior. It is rendered last.isUndo(),getEngine()andisEngineUseEquals()cover MySQLDROP [UNDO] TABLESPACE name [ENGINE [=] engine].isForce()covers PostgreSQLDROP DATABASE name [WITH] (FORCE); the optional WITH is always rendered.getAccessMethod()holds the index access method of PostgreSQLDROP OPERATOR CLASS|FAMILY name USING method, which takes one name.isProcedural()records thePROCEDURAL LANGUAGEspelling.PostgreSQL names casts, operators and transforms by types.
getCast()is theCastSignatureofDROP CAST (source_type AS target_type).getOperators()holds anOperatorSignatureper target ofDROP OPERATOR [schema.]op ({left_type | NONE}, {right_type | NONE}) [, ...]; its operator is theExclusionOperatorreference that COMMENT ON OPERATOR uses, and a null operand type stands for NONE.getTransform()is theTransformSignatureofDROP TRANSFORM FOR type_name LANGUAGE lang_name. Oracle’sDROP OPERATOR namekeeps a plain name, which the PostgreSQL dialect rejects.
TYPE, DOMAIN, EVENT, EVENT TRIGGER, TABLESPACE, USER and GROUP are recognized
object types, and so are PostgreSQL’s ACCESS METHOD, CAST, LANGUAGE, OPERATOR,
OPERATOR CLASS, OPERATOR FAMILY, TRANSFORM and TEXT SEARCH CONFIGURATION,
DICTIONARY, PARSER and TEMPLATE.
getType() returns their words separated by spaces, as in ACCESS METHOD. MySQL’s
DROP SPATIAL REFERENCE SYSTEM [IF EXISTS] srid has the object type
SPATIAL_REFERENCE_SYSTEM and its name is the SRID as written. PostgreSQL REASSIGN OWNED BY old_role [, ...] TO new_role is
parsed into ReassignOwned, whose roles use the same UserAccount model.
Drop drop = (Drop) CCJSqlParserUtil.parse("DROP FUNCTION f(int), g(text) CASCADE",
parser -> parser.withDialect(Dialect.POSTGRESQL));
RoutineReference g = drop.getRoutines().get(1);
g.getArguments().get(0).setDataType(new ColDataType("varchar"));
// DROP FUNCTION f(int), g(varchar) CASCADE
String sql = drop.toString();
The legacy accessors remain and are derived from these properties:
getName()andgetNames()return read-only snapshots of routine, account and operator names. EditgetRoutines(),getAccounts()orgetOperators()to change them. A cast or transform has no name.getTypeToParameters()andgetParamsByType()return a read-only map with the argument declarations of the last routine, where the legacy single-signature form kept them. The map now also covers PROCEDURE, ROUTINE and AGGREGATE.getParameters()is a snapshot that includes ON with the owner table, the MySQL ALGORITHM and LOCK clauses and the CASCADE or RESTRICT keyword, in source order.setParameters()replaces the clause with raw tokens and clears all three;addParameters()keeps them. Raw tokens are not parsed:addParameters("CASCADE")leavesgetBehavior()null.setName(),setNames(),addNames()andsetTypeToParameters()first convert routine and account targets to the legacy names and single signature, sosetName()onDROP FUNCTION f(int)still rendersDROP FUNCTION g(int). UsegetRoutines()to keep several signatures.
Migration: mutating the map returned by getTypeToParameters() for a parsed
routine now throws UnsupportedOperationException; edit its
RoutineReference instead. getObjectType() reports the new AGGREGATE,
DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE,
RULE, TYPE and USER kinds where it reported OTHER.
DROP OWNED BY r no longer returns the name BY.
DROP INDEX owners¶
DROP INDEX ix ON app.t exposes app.t through Drop.getTable();
getName() continues to identify the index. The owner is populated for the
single-index ON form used by MySQL and SQL Server. A DROP INDEX without ON,
as in PostgreSQL, has a null owner: resolving the index’s table requires a catalog.
SQL Server’s multi-owner list and WITH options are outside this extension.
getParameters() includes ON and the rendered table as a legacy token snapshot
when a structured owner exists. Mutate getTable() or use setTable() to
update the owner; addParameters() appends options without losing it.
setTable(null) removes ON, while setParameters() replaces the entire
parameter clause with raw tokens and clears the structured owner. Raw parameter
setters do not parse SQL or infer a table.
Table discovery and statement visitors traverse DROP TABLE/VIEW targets and explicit index owners, without reporting catalog-only object names as tables. The statement deparser delegates real tables to its configured select deparser. MySQL ALGORITHM and LOCK clauses retain their order and optional equals signs as typed nodes; see Share MySQL index options.
MySQL named constraint drops¶
Without a dialect and with Dialect.MYSQL, DROP FOREIGN KEY name and
DROP CHECK name expose the target through AlterExpression.getConstraintName().
Dialect.MARIADB accepts the named FOREIGN KEY form; MariaDB has no DROP CHECK.
Other dialects drop constraints with DROP CONSTRAINT. A parenthesized
DROP FOREIGN KEY (...) keeps the legacy column-list form. Inspect
getOperation() for DROP_FOREIGN_KEY or DROP_CHECK. Identifier
quotes are preserved, and changing the name changes both SQL renderers.
These named targets do not populate the legacy getPkColumns() list.
Alter alter = (Alter) CCJSqlParserUtil.parse(
"ALTER TABLE child DROP FOREIGN KEY fk_parent",
parser -> parser.withDialect(Dialect.MYSQL));
AlterExpression drop = alter.getAlterExpressions().get(0);
drop.setConstraintName("fk_customer");
// ALTER TABLE child DROP FOREIGN KEY fk_customer
String sql = alter.toString();
// The same API also supports constructing an action without parsing SQL.
AlterExpression checkDrop = new AlterExpression()
.withOperation(AlterOperation.DROP_CHECK).withConstraintName("positive_id");
Edit table and view column renames¶
AlterExpression.getColumnRename() exposes a ColumnRenameAction with
getSourceName() and getTargetName(). Their setters update the existing
AST directly, preserving the optional COLUMN keyword. Names include their
written identifier quotes; include quotes in replacement names when needed.
Alter alter = (Alter) CCJSqlParserUtil.parse(
"ALTER TABLE accounts RENAME COLUMN old_id TO new_id",
parser -> parser.withDialect(Dialect.MYSQL));
ColumnRenameAction rename = alter.getAlterExpressions().get(0).getColumnRename();
rename.getSourceName(); // old_id
rename.setTargetName("customer_id");
// ALTER TABLE accounts RENAME COLUMN old_id TO customer_id
String sql = alter.toString();
Import ColumnRenameAction from net.sf.jsqlparser.statement.alter.
PostgreSQL table renames of columns use the same API. With Dialect.POSTGRESQL,
column renames on views and materialized views expose it through the entries in
AlterRelation.getActions(). The lookup returns null for other operations,
including renaming the table, view, index or constraint itself. Existing
action-specific getters and setters remain available and see the same changes.
MySQL table and partition comments¶
A table COMMENT in CREATE TABLE, CREATE TABLE … AS SELECT and ALTER TABLE is a
StringValue in TableOption.getComment(). Partition and subpartition comments,
including those of ALTER TABLE ADD and REORGANIZE PARTITION, are in
PartitionDefinition.getCommentValue(). The literal records the escaping of the
parser configuration, so getNotExcapedValue() returns the text MySQL stores.
CreateTable table = (CreateTable) CCJSqlParserUtil.parse(
"CREATE TABLE t (id INT) COMMENT = 'it\\'s' "
+ "PARTITION BY HASH (id) (PARTITION p0 COMMENT 'a\\\\b')",
parser -> parser.withDialect(Dialect.MYSQL));
TableOption option = table.getTableOption(TableOption.Kind.COMMENT).orElseThrow();
option.getComment().getNotExcapedValue(); // it's
option.getValue(); // 'it\'s'
table.getPartitioning().getPartitionDefinitions().get(0)
.getCommentValue().getNotExcapedValue(); // a\b
getValue(), getTokens(), CreateTable.getTableOptionsStrings(),
AlterExpressionTableOption.getCommentText() and PartitionDefinition.getComment()
still return the literal as written, and the SQL output is unchanged except that a
string prefix such as n'…' is printed in upper case, as in other string literals.
The table comment literal is rendered by the expression deparser and visited like a
column COMMENT literal.
setComment, withComment and the TableOption.comment factory replace the
option’s contents with the literal and select COMMENT. They keep the equals spelling,
and an existing COMMENT also keeps its name; a null literal omits the value.
setValue stores a plain value and setTokens opaque tokens. Neither is parsed,
so getComment() then returns null. Selecting another kind keeps the literal’s SQL
text as a plain value. A COMMENT value that is not a string literal also stays a plain
value, for example a double-quoted string parsed without the MySQL dialect, where it
is a quoted identifier.
PartitionDefinition.setComment(String) reads the SQL text of a literal with
standard escaping; pass a StringValue with another escaping to
setCommentValue.
Inspect PostgreSQL schema statements¶
PostgreSQL schema clauses extend the existing CreateView, CreateTable, Alter and Sequence models. Use their typed properties to inspect the clauses and preserve distinctions between omitted options and explicit values.
CreateView view = (CreateView) CCJSqlParserUtil.parse(
"CREATE MATERIALIZED VIEW IF NOT EXISTS account_totals "
+ "AS SELECT id FROM accounts WITH NO DATA");
view.isMaterialized(); // true
view.isIfNotExists(); // true
view.getWithData(); // Boolean.FALSE; null means the clause was omitted
Ordinary views expose ordered ViewOption values for security_barrier, security_invoker and check_option. getCheckOption() describes the trailing WITH CHECK OPTION clause: null means absent, DEFAULT preserves the bare clause, and LOCAL and CASCADED preserve explicit keywords. getEffectiveCheckOption() resolves the bare clause to CASCADED. Materialized views expose their access method, storage parameters and tablespace separately. See the PostgreSQL documentation for CREATE VIEW and CREATE MATERIALIZED VIEW.
CreateTable table = (CreateTable) CCJSqlParserUtil.parse(
"CREATE TABLE accounts_copy (LIKE accounts INCLUDING ALL EXCLUDING INDEXES)");
LikeClause like = table.getTableElements(LikeClause.class).get(0);
like.getOptions(); // ordered INCLUDING/EXCLUDING clauses
like.isIncluding(LikeClause.OptionKind.DEFAULTS); // true, inherited from ALL
like.isIncluding(LikeClause.OptionKind.INDEXES); // false, overridden by EXCLUDING
getTableElements() preserves the order of columns, constraints and LIKE clauses, including multiple source tables. The three legacy nullable LikeClause getters report explicit options of that kind; isIncluding(OptionKind) also resolves ALL in declaration order. A typed table’s OF type is available through CreateTable.getOfType().
Table constraints expose their names and ConstraintAttributes independently from indexes. KeyConstraint and ExcludeConstraint expose IndexOptions for null treatment, INCLUDE columns, storage parameters and tablespace options. Their KeyElement nodes expose key expressions, ordering and exclusion operators. Column identity clauses are represented by ColumnOption.Kind.IDENTITY and IdentityDefinition, with a generation mode and ordered Sequence.Parameter values. These APIs cover the schema clauses described in CREATE TABLE.
Inspect indexes and constraints separately¶
Index represents index declarations. Constraints are independent
NamedConstraint nodes: KeyConstraint for PRIMARY KEY/UNIQUE,
ForeignKeyConstraint, CheckConstraint, ExcludeConstraint,
DefaultConstraint, NotNullConstraint and ConstraintUsingIndex.
Constraint nodes do not extend Index. Their ConstraintKind is separate
from the physical index classification Index.Kind.
CreateTable.getIndexes() returns only indexes, while
getTableConstraints() returns table-level constraints. Both are mutable
views of getTableElements() when ordered elements are present; removing a
constraint preserves indexes and their source positions. To inspect column
and table constraints together, use getConstraints(). This returns
ConstraintDeclaration views in declaration order.
CreateTable table = (CreateTable) CCJSqlParserUtil.parse(
"CREATE TABLE t (id INT PRIMARY KEY, label TEXT)");
ConstraintDeclaration primary = table.getConstraints().stream()
.filter(declaration -> declaration.getKind() == ConstraintKind.PRIMARY_KEY)
.findFirst().orElseThrow();
primary.getColumn().setColumnName("new_id");
// CREATE TABLE t (new_id INT PRIMARY KEY, label TEXT)
getColumn() retains the owner of an inline declaration and is null for a
table constraint. getColumnOption() exposes an inline option;
getConstraint() exposes its original NamedConstraint when one exists.
Column REFERENCES and nullability options retain their own payloads instead
of manufacturing a table constraint or a key-column list. Edit the original
source nodes to change SQL. Explicit NOT NULL and REFERENCES are classified;
ordinary column DEFAULT values are options, while explicit
DefaultConstraint nodes are classified as DEFAULT. NULL, raw tokens and implied constraints are not
inferred.
The declaration list is an unmodifiable membership snapshot. Its views read
live source nodes, so type edits change getKind(). Request a new list after
adding, removing or changing column options. Structured constraint nodes
remain included when their kind is OTHER. Without ordered table elements,
manually constructed columns precede table constraints and indexes.
ColumnOption.getConstraintKind() and
AlterExpression.getConstraintKind() also return ConstraintKind.
getOperation() distinguishes addition from removal. ALTER ADD constraints
use getConstraint(); ADD INDEX uses getIndex(). These getters expose
stored definitions; after changing an action, getConstraintKind() describes
the active syntax rather than an inactive stored definition. Column actions
retain constraints on their column options. RENAME CONSTRAINT uses
getConstraintName() for the source and getNewConstraintName() for the
target; it does not manufacture index nodes. A named DROP CONSTRAINT pk reports
OTHER because SQL alone does not reveal what pk names. This query does
not validate a definition’s completeness or consult the database catalogue.
MySQL table-level UNIQUE, UNIQUE KEY and UNIQUE INDEX declarations
are KeyConstraint nodes. A standalone CREATE UNIQUE INDEX remains
CreateIndex with an Index payload. The parser does not invent backing
index nodes for PRIMARY KEY/UNIQUE constraints. PostgreSQL USING INDEX
retains the existing index name on ConstraintUsingIndex rather than
creating key elements or an index definition.
An ALTER foreign key owns one ForeignKeyConstraint and its mutable
ForeignKeyReference. The legacy getFk*/setFk* accessors and
referential-action helpers delegate to these same nodes, so edits through
either API affect both SQL renderers.
Alter alter = (Alter) CCJSqlParserUtil.parse(
"ALTER TABLE child ADD FOREIGN KEY (parent_id) REFERENCES parent (id)");
AlterExpression action = alter.getAlterExpressions().get(0);
ForeignKeyConstraint foreignKey = (ForeignKeyConstraint) action.getConstraint();
foreignKey.getReference().getTable().setSchemaName("archive");
action.setFkSourceTable("parent_history");
// Both APIs now refer to archive.parent_history.
foreignKey.setReferentialAction(ReferentialAction.Type.DELETE,
ReferentialAction.Action.CASCADE);
New code should use the structured constraint API. getFkColumns() returns
a rendered snapshot, like getPkColumns() and getUkColumns(); use
setFkColumns()/addFkColumns() to replace or append names, or edit
foreignKey.getColumns() directly. Referenced-column lists and
referential-action objects are live objects on the shared reference.
A non-null legacy FK setter creates a foreign-key definition when none is
present. It rejects an existing non-FK constraint or index with
IllegalStateException instead of retaining a second hidden definition;
replace the definition explicitly with setConstraint() first. Reading,
clearing or removing absent FK properties does not create a definition.
Replacing or clearing setConstraint() also replaces or clears what the
legacy FK accessors expose. These accessors inspect stored definitions; use
getOperation()/getConstraintKind() to identify the active action.
Migration from the former combined model:
Replace table-constraint reads from
getIndexes()withgetTableConstraints()and ALTER constraint reads fromgetIndex()withgetConstraint().ConstraintDeclaration.getIndex()is replaced bygetConstraint().Replace
ForeignKeyIndexwithForeignKeyConstraintand concretenew NamedConstraint()construction with the appropriate constraint class, usuallyKeyConstraint.NamedConstraintis now abstract.Constraint enum values move from
Index.KindtoConstraintKind;Index.Kindretains only UNIQUE, INDEX, FULLTEXT, SPATIAL and OTHER.Shared key and option values are now
KeyElement(formerlyIndex.ColumnParams),IndexOption(formerlyIndex.Option) andIndexOptions.Clustering(formerlyIndex.Clustering). These are standalone shared components, not aliases to nested index types. ReplacegetColumnWithParams()/setColumnNamesWithParams()withgetColumns()/setColumns()usingKeyElementvalues.
getType() preserves the rendered spelling. setType() refreshes kind
metadata for indexes and key constraints; setKind() does not rewrite SQL.
Constraints with a fixed renderer, such as CHECK, always report their actual
constraint kind. Replacing an index with a table constraint requires replacing
the AST node; changing an index’s type string cannot change its Java type.
ALTER key definitions use getKeyConstraint() for typed access to the same
object returned by getConstraint(). PRIMARY KEY and UNIQUE builder setters
also create this canonical definition; they no longer keep separate column,
name or KEY/INDEX state:
Alter add = (Alter) CCJSqlParserUtil.parse(
"ALTER TABLE t ADD UNIQUE KEY uq (id)",
parser -> parser.withDialect(Dialect.MYSQL));
KeyConstraint key = add.getAlterExpressions().get(0).getKeyConstraint();
key.setColumns(List.of(new KeyElement("account_id")));
key.setIndexName("uq_account");
System.out.println(add); // ALTER TABLE t ADD UNIQUE KEY uq_account (account_id)
Parenthesized deletion targets have their own getDropColumns() and
setDropColumns() API. It covers multi-column DROP (...), DROP UNIQUE
(...) and the parser’s legacy parenthesized DROP FOREIGN KEY (...) form.
Named constraint drops continue to use getConstraintName(). No key
definition is manufactured for a drop:
Alter drop = (Alter) CCJSqlParserUtil.parse("ALTER TABLE t DROP (old_a, old_b)");
drop.getAlterExpressions().get(0).getDropColumns().set(0, "obsolete_a");
System.out.println(drop); // ALTER TABLE t DROP (obsolete_a, old_b)
setDropColumns copies its input; getDropColumns returns the owned mutable
list. withDropColumns and addDropColumns also work before selecting the
operation. Stored drop targets and key definitions remain independent when an
operation changes.
Migration of older ALTER builders:
getPkColumns()andgetUkColumns()now consistently return snapshots, including manually built keys. Use their setters/adders or editgetKeyConstraint().getColumns()to change SQL. The string setters replace elements without retaining expression or ordering attributes.The legacy PK accessor remains a DROP-target alias when DROP, DROP UNIQUE or DROP FOREIGN KEY is already selected. Replace builders that set PK columns before selecting DROP with
withDropColumns(...); changing the operation does not transfer a key definition into deletion targets.getUkNameaccesses the UNIQUE definition’s index name; the constraint symbol remainsgetKeyConstraint().getName(). KEY/INDEX flags read that same definition.setUkTypeSpecified(false)removes the spelling; re-enabling it without a spelling selects INDEX.Non-null key setters create a definition only when none exists. They throw
IllegalStateExceptionfor a competing constraint or index; usesetConstraintfor an explicit replacement. Reads and null clears never create a definition or restore values from a replaced one.
Exclusion keys expose getExclusionOperatorReference() for structured access
to an operator’s schemaName, name and useOperatorKeyword flag. In
Dialect.POSTGRESQL, both CREATE and ALTER accept WITH OPERATOR(pg_catalog.&&).
Editing the reference updates both statement renderers. The existing string
getExclusionOperator() returns its complete SQL spelling; its setter remains
available for replacing opaque operator text. New code can construct a reference
with new ExclusionOperator().withSchemaName("pg_catalog").withName("&&")
.withUseOperatorKeyword(true).
Alter alter = (Alter) CCJSqlParserUtil.parse(
"ALTER TABLE accounts ALTER COLUMN id TYPE bigint USING id + 1");
AlterExpression.ColumnDataType column = alter.getAlterExpressions().get(0)
.getColDataTypeList().get(0);
Expression conversion = column.getUsingExpression();
column.setUsingExpression(CCJSqlParserUtil.parseExpression("id * 10"));
String updatedSql = alter.toString();
Identity alterations are directly accessible through AlterExpression.getColumnIdentityActions();
the existing ColumnDataType.getIdentityAlterations() API remains available.
Sequence ownership is shared by CreateSequence and AlterSequence through Sequence.getOwnership(): null means omitted, isNone() means explicit OWNED BY NONE, and getColumn() identifies an owner. TablesNamesFinder includes LIKE sources and sequence owners without treating sequence or type names as tables. See ALTER TABLE and ALTER SEQUENCE.
For identity columns, RESTART 20 and RESTART WITH 20 produce the same
IdentityAlteration with kind RESTART and getRestartWith() == 20L.
The renderer consistently uses RESTART WITH 20. A null restart value means
bare RESTART, which uses the sequence’s configured start value. Call
setRestartWith to edit a parsed action or construct one with
new IdentityAlteration(Kind.RESTART).withRestartWith(20L).
Edit column identity actions¶
getColumnIdentityActions() returns active ALTER COLUMN identity actions
as ColumnIdentityAction entries. Each entry exposes the editable column name
and its ordered IdentityAlteration list, without requiring a data-type cast:
Alter alter = (Alter) CCJSqlParserUtil.parse(
"ALTER TABLE accounts ALTER COLUMN id "
+ "ADD GENERATED BY DEFAULT AS IDENTITY (CACHE 10)",
parser -> parser.withDialect(Dialect.POSTGRESQL));
ColumnIdentityAction column = alter.getAlterExpressions().get(0)
.getColumnIdentityActions().get(0);
column.setColumnName("account_id");
IdentityAlteration identity = column.getIdentityAlterations().get(0);
identity.setGenerationMode(IdentityDefinition.GenerationMode.ALWAYS);
identity.getParameters().get(0).setValue(20L);
System.out.println(alter);
// ALTER TABLE accounts ALTER COLUMN account_id
// ADD GENERATED ALWAYS AS IDENTITY (CACHE 20)
The outer list has unmodifiable membership; its entries are the original AST
nodes. Request a new list after changing action membership or switching the
outer ALTER operation. The inner alteration list is mutable: replace an entry
to change its kind, or use setIdentityAlterations to replace the ordered
actions for a column. Unrelated actions and inactive inherited payloads are
excluded. An inline identity declaration in ADD COLUMN remains a
ColumnOption rather than an identity alteration.
getGenerationMode/setGenerationMode work for both ADD and
SET_GENERATED. getParameters/setParameters work for ADD and
SET_PARAMETER. ADD access delegates to its existing IdentityDefinition;
replacing or clearing that definition immediately changes both views. Parameters
are ordered mutable Sequence.Parameter values, with null meaning omitted.
RESTART and DROP retain their distinct getRestartWith and isIfExists
properties. A sequence such as SET CACHE 10 RESTART WITH 20 remains two
ordered alterations.
For a newly constructed ADD action without a definition, mode and parameter getters return null. Non-null setters create the definition; setting parameters first uses BY DEFAULT. Null setters do not create one. This does not change the SQL grammar or validate whether a column already has an identity in a database.
Column defaults on tables and views¶
AlterExpression.getColumnDefaults() exposes active SET DEFAULT actions
through ColumnDefaultAction for both tables and views. The returned list is
read-only, but its entries are the original mutable AST objects:
for (String objectType : List.of("TABLE", "VIEW")) {
Statement statement = CCJSqlParserUtil.parse(
"ALTER " + objectType + " t ALTER COLUMN a SET DEFAULT 1",
parser -> parser.withDialect(Dialect.POSTGRESQL));
List<? extends AlterExpression> actions = statement instanceof Alter
? ((Alter) statement).getAlterExpressions()
: ((AlterRelation) statement).getActions();
for (AlterExpression action : actions) {
for (ColumnDefaultAction column : action.getColumnDefaults()) {
column.setDefaultExpression(new LongValue(42));
}
}
System.out.println(statement); // SET DEFAULT 42 for either object type
}
getColumnName() and setColumnName() access the affected column.
getDefaultExpression() exposes parsed expressions; getDefaultValue()
returns their SQL text. Existing ColumnSetDefault constructors still accept
opaque strings, for which getDefaultExpression() returns null.
Replacing an expression also replaces any opaque value. Unrelated actions
return an empty list. This lookup follows the action’s current rendering path:
changing an operation or retaining unused metadata does not make a fixed-body
action expose SET DEFAULT entries it will not render.
hasActiveColumnDefinitions() reports whether the action currently renders
its column definitions through the general ALTER body. getColDataTypeList()
continues to expose stored metadata even when another body takes precedence.
AlterDeParser uses the active path, so expression visitors rewrite the
selected default or column expressions without rendering inactive metadata.
TableDefinitionTraversal and the table-name finder visit the expressions
and tables belonging to the selected body. The metadata validator uses that
same selection. For example, adding a column definition to an action that still
stores an earlier foreign key renders the column and does not require the
inactive foreign table or its columns to exist. getConstraint() and
getIndex() still expose the stored objects for direct inspection.
getActiveTableDefinition() returns the original definition contributing to
the current body, or null when another payload takes precedence. A legacy
ADD CONSTRAINT header contributes only its keys; its unused storage options
are not traversed. Column options likewise visit only the payload selected by
their Kind, including after changing an option to FOREIGN_OPTIONS.
Custom AlterExpression subclasses overriding appendBody should also
reflect their dispatch in usesColumnActionBody(), usesGeneralBody() and
hasActiveConstraintHeader(). Subclasses rendering a separate definition can
override getActiveTableDefinition(). The general-body hook also determines
hasActiveColumnDefinitions().
Structured column attributes¶
ColumnDefinition.getColumnOptions() exposes column nullability, COLLATE,
COMMENT, ON UPDATE, AUTO_INCREMENT, visibility, inline PRIMARY KEY
and parenthesized generated expressions alongside existing defaults, references
and identity declarations. CREATE and ALTER column definitions use the same model.
ColumnOption.Kind selects the relevant accessor: getNullable(),
getCollation(), getComment(), getOnUpdateExpression(),
getVisible(), getConstraint() or getGeneratedDefinition().
The AUTO_INCREMENT kind has no additional payload. A missing option does not
imply a server default. Option order and unknown raw extensions are preserved.
GeneratedColumnDefinition contains an Expression, an explicit
GENERATED ALWAYS flag, and nullable Storage (STORED/VIRTUAL). Identity
columns continue to use IdentityDefinition. Visitors, validators and custom
expression deparsers traverse generation and ON UPDATE expressions and comment
literals. This is syntax modeling; server restrictions on permissible generation
expressions are not evaluated.
Charset remains in ColDataType.getCharacterSet(); column collation is in its
COLLATE option. Consumers can map both directly without reconstructing tokens.
StringValue comment bodies retain their SQL escape representation.
A column DEFAULT value ends before a COLLATE at its top level, which becomes the
column’s COLLATE option: MySQL’s DEFAULT literal and PostgreSQL’s DEFAULT b_expr
do not include COLLATE. A COLLATE inside parentheses, function arguments or a CASE
expression stays in the default expression. SET DEFAULT actions and CREATE
DOMAIN keep an expression COLLATE.
Migration: DEFAULT 'N' COLLATE utf8mb4_bin in a column definition previously
produced one DEFAULT option holding a CollateExpression. It now produces a
DEFAULT option with 'N' and a COLLATE option with getCollation();
getColumnSpecs() returns [DEFAULT, 'N', COLLATE, utf8mb4_bin]. SQL output
is unchanged.
MySQL column attributes have their own kinds: SRID (getSrid()),
COLUMN_FORMAT (getColumnFormat(): FIXED, DYNAMIC or DEFAULT), and
ENGINE_ATTRIBUTE/SECONDARY_ENGINE_ATTRIBUTE (getEngineAttribute()
and isUseEquals() for the optional equals sign). Engine attribute literals
are visited and rendered like comment literals. Inline UNIQUE KEY is one
KeyConstraint of type UNIQUE KEY. A bare inline KEY, MySQL’s column
synonym for PRIMARY KEY, keeps its KEY spelling and reports
ConstraintKind.PRIMARY_KEY. Other values, such as Percona Server’s
COLUMN_FORMAT COMPRESSED, remain opaque options.
CreateTable table = (CreateTable) CCJSqlParserUtil.parse(
"CREATE TABLE t (id INT KEY, g POINT SRID 4326 COLUMN_FORMAT FIXED)");
List<ColumnDefinition> columns = table.getColumnDefinitions();
columns.get(0).getColumnOptions().get(0).getConstraintKind(); // PRIMARY_KEY
ColumnOption srid = columns.get(1).getColumnOptions().get(0);
srid.getSrid(); // 4326
srid.setSrid(0);
columns.get(1).addColumnOptions(
ColumnOption.engineAttribute(new StringValue("{}")));
// CREATE TABLE t (id INT KEY, g POINT SRID 0 COLUMN_FORMAT FIXED ENGINE_ATTRIBUTE '{}')
Migration: these attributes were previously one opaque option per token, and
UNIQUE KEY was a UNIQUE constraint followed by an opaque KEY. Inspect the
kinds instead of the opaque nodes. getColumnSpecs() and getTokens() keep
the previous token split, for example [SRID, 4326] and [UNIQUE, KEY].
When constructing the KEY spelling, set the kind after the type:
new KeyConstraint().withType("KEY").withKind(ConstraintKind.PRIMARY_KEY).
CHAR(n) BINARY remains an opaque option: MySQL treats BINARY as part of
the data type’s character set clause, not as a column attribute.
ColumnDefinition stores all options in one ordered list, including opaque
ColumnOption.Kind.OTHER nodes. getColumnOptions() returns that mutable
list. getColumnSpecs() returns an unmodifiable snapshot for both raw and
structured options; expression fragments may occupy one token and structured
keywords use canonical capitalization. Unset options return null; an explicitly
empty list returns an empty snapshot.
CreateTable table = (CreateTable) CCJSqlParserUtil.parse(
"CREATE TABLE t (id INT DEFAULT 1)");
ColumnDefinition column = table.getColumnDefinitions().get(0);
column.getColumnOptions().get(0).setDefaultExpression(new LongValue(2));
column.addColumnSpecs("NOT", "NULL"); // keeps the structured DEFAULT node
// CREATE TABLE t (id INT DEFAULT 2 NOT NULL)
Migration: calls such as getColumnSpecs().add(...) or .set(...) now throw
UnsupportedOperationException instead of sometimes changing the AST and
sometimes editing a discarded copy. Use addColumnSpecs to append raw tokens,
edit the option nodes directly, or pass a modified token-list copy to
setColumnSpecs to replace every option with opaque text. Raw tokens are never
automatically parsed. Consequently getColumnOptions() is also non-null for
raw specifications; inspect Kind to distinguish them.
The constructor and setColumnSpecs copy supplied tokens. setColumnOptions
copies list membership but retains the mutable child nodes; later changes to the
caller’s list do not change the column. Both getters return null after either
setter receives null. Appending preserves the current nodes and accepts an
immutable input list. An empty append preserves existing options and creates an
empty option list when none was set.
ColumnOption.getTokens() and GeneratedColumnDefinition.getTokens() also
return unmodifiable, detached snapshots. Reading tokens does not detach the
underlying expressions or other child nodes: their setters and direct edits
remain visible in SQL output, visitors and newly requested snapshots.
For opaque Kind.OTHER options, copy the tokens, edit the copy, then call
setTokens. The setter and both ColumnOption.raw factories copy their
inputs; subsequent changes to the supplied list or array do not edit the AST.
Null raw tokens remain unset and return null; an empty list returns an empty
snapshot. Raw tokens are never parsed or classified.
ColumnOption raw = ColumnOption.raw("SELECTIVITY", "10");
List<String> tokens = new ArrayList<>(raw.getTokens());
tokens.set(1, "20");
raw.setTokens(tokens); // SELECTIVITY 20
Migration: raw token getter edits now throw UnsupportedOperationException,
as do edits to structured token snapshots that previously changed a discarded
copy. setTokens only accepts opaque options; it throws
IllegalStateException for structured options, including SERIAL DEFAULT
VALUE. Use typed setters for structured edits. To explicitly replace a
structured option with raw text, replace its node in the column’s option list:
column.getColumnOptions().set(0, ColumnOption.raw("DEFAULT", "8"));
The trailing FIRST or AFTER column of an ALTER TABLE ADD, CHANGE or
MODIFY column definition is a ColumnPosition on the
AlterExpression.ColumnDataType, not a column option. getColumnSpecs()
still ends with its tokens, and setColumnSpecs replaces the options and the
position. Migration: getColumnOptions() no longer contains the two opaque
AFTER and column-name nodes. As in MySQL, the position must follow the
column attributes; a position followed by further attributes is now rejected.
Alter alter = (Alter) CCJSqlParserUtil.parse(
"ALTER TABLE t ADD COLUMN c INT NOT NULL AFTER id");
AlterExpression.ColumnDataType column =
alter.getAlterExpressions().get(0).getColDataTypeList().get(0);
column.getPosition().getColumnName(); // id
column.getColumnSpecs(); // [NOT, NULL, AFTER, id]
column.setPosition(ColumnPosition.first());
// ALTER TABLE t ADD COLUMN c INT NOT NULL FIRST
Inspect logical replication statements¶
PostgreSQL publications and subscriptions have separate statement and option models. No database connection is opened when these statements are parsed.
CreatePublication publication = (CreatePublication) CCJSqlParserUtil.parse(
"CREATE PUBLICATION changes FOR TABLE accounts (id) WHERE (active = true)");
PublicationTable target = publication.getTargets().get(0).getTables().get(0);
Table table = target.getTable();
List<Column> columns = target.getColumns();
Expression filter = target.getWhere();
A PublicationTarget distinguishes a group of explicit tables from TABLES IN SCHEMA. Target order, repeated TABLE groups, ONLY and an explicit descendant * are preserved. CreatePublication.isAllTables() represents FOR ALL TABLES; an empty target list without that flag means no target clause was specified. AlterPublication.getAction() distinguishes adding, replacing or removing targets from option, owner and name changes.
PublicationOption exposes typed operation sets, partition-root booleans and generated-column modes. SubscriptionOption has a separate key enum and typed streaming, origin, synchronous-commit and boolean accessors. The ordered option lists contain only explicitly written options; server defaults, which can differ across PostgreSQL versions, are not injected into the AST. A missing value on a boolean option represents its explicit short form, equivalent to = true.
CreateSubscription subscription = (CreateSubscription) CCJSqlParserUtil.parse(
"CREATE SUBSCRIPTION changes_sub CONNECTION 'dbname=app' "
+ "PUBLICATION changes WITH (connect = false)");
StringValue connection = subscription.getConnection();
List<String> publications = subscription.getPublications();
Boolean connect = subscription.getOptions().get(0).getBooleanValue();
Connection strings remain string literals for lossless SQL regeneration. They can contain credentials and should not be logged without redaction. SubscriptionOption.isSlotNameNone() distinguishes the unquoted NONE keyword from a literal slot named 'NONE'. AlterSubscription covers connection changes, publication lists and refresh, enable/disable, options, skip LSN, ownership and renaming.
Publication table columns and row filters participate in visitors and custom expression deparsers. TablesNamesFinder reports explicitly named publication tables, but cannot enumerate ALL TABLES or schema-wide targets without a catalog. Publication and subscription names are not table names. Feature classification reports schema modification; a subscription that may start asynchronous replication can additionally report possible data modification.
See CREATE PUBLICATION, ALTER PUBLICATION, CREATE SUBSCRIPTION and ALTER SUBSCRIPTION.
Inspect type, domain and extension statements¶
PostgreSQL type DDL uses the existing column data-type model. A CreateType exposes its qualified name and a TypeDefinition: EnumTypeDefinition, CompositeTypeDefinition, RangeTypeDefinition or BaseTypeDefinition. A null definition denotes a shell type. Enum labels are ordered StringValue nodes, composite attributes carry a name, ColDataType and optional collation, and range options distinguish the subtype from names of support functions, collations and operator classes.
CreateType type = (CreateType) CCJSqlParserUtil.parse(
"CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy')");
EnumTypeDefinition definition = (EnumTypeDefinition) type.getDefinition();
String firstLabel = definition.getLabels().get(0).getValue();
AlterType alteration = (AlterType) CCJSqlParserUtil.parse(
"ALTER TYPE mood ADD VALUE IF NOT EXISTS 'fine' AFTER 'ok'");
alteration.getPosition(); // AlterType.Position.AFTER
alteration.getNeighborValue(); // StringValue containing 'ok'
AlterType also models type ownership, renaming, schema moves and ordered composite-attribute changes.
A base type, CREATE TYPE name (INPUT = fn, OUTPUT = fn, ...), has a BaseTypeDefinition, and ALTER TYPE name SET (property = value, ...) has the action SET_PROPERTIES with getProperties(). Both hold DefinitionElement items in source order: a name with its spelling and quoting, and a DefinitionArgument, which is null for an element without a value such as PASSEDBYVALUE. The argument follows PostgreSQL’s grammar: its Kind is TYPE (a ColDataType, with isSetOf() and isTypeReference() for SETOF and %TYPE), KEYWORD (a reserved keyword such as true), OPERATOR, NUMBER, STRING or NONE. PostgreSQL reads a function name or a word such as plain as a type name, so these are TYPE arguments whose name is getDataType().getDataType(). The property names and the kind of value each property takes are not checked, because PostgreSQL checks them when it runs the statement.
CreateType type = (CreateType) CCJSqlParserUtil.parse(
"CREATE TYPE box3 (INPUT = box3_in, OUTPUT = box3_out, INTERNALLENGTH = 16, PASSEDBYVALUE)");
BaseTypeDefinition definition = (BaseTypeDefinition) type.getDefinition();
definition.getElement("INPUT").getArgument().getDataType().getDataType(); // box3_in
definition.getElement("INTERNALLENGTH").getArgument().getNumber(); // LongValue 16
definition.getElement("PASSEDBYVALUE").getArgument(); // null
AlterType alteration = (AlterType) CCJSqlParserUtil.parse(
"ALTER TYPE box3 SET (SEND = box3_send, RECEIVE = NONE)");
alteration.getAction(); // AlterType.Action.SET_PROPERTIES
alteration.getProperties().get(1).getArgument().getKind(); // DefinitionArgument.Kind.NONE
See CREATE TYPE and ALTER TYPE.
CreateDomain stores a ColDataType, default expression and ordered DomainConstraint nodes. Each constraint distinguishes nullability from a check expression and preserves its optional name. AlterDomain models default/nullability changes, constraint actions, ownership, renaming and schema moves. Its isNotValid() flag describes the newly added check constraint. Domain expressions participate in statement visitors, expression deparsers and validation.
CreateExtension preserves IF NOT EXISTS, the optional WITH, and ordered schema/version/cascade options. Version identifiers and quoted versions are represented as Column and StringValue, respectively. AlterExtension distinguishes update, schema change, and member addition/removal. An ExtensionObject identifies the object kind and target; function/procedure/aggregate members expose a RoutineReference with typed signature arguments, not call expressions. A null argument list means the signature was omitted, whereas an empty list means explicit ().
Type names, domain names and routine signatures are not reported as tables by TablesNamesFinder; relation members of an extension are included. Extension scripts themselves are not inspected or executed. The obsolete CREATE EXTENSION ... FROM form is not supported.
See PostgreSQL’s documentation for CREATE TYPE, ALTER TYPE, CREATE DOMAIN and ALTER EXTENSION.
Inspect operator and aggregate definitions¶
PostgreSQL’s operator and aggregate DDL parses to typed statements that reuse the models of
DROP OPERATOR, DROP AGGREGATE and CREATE TYPE: an operator is an ExclusionOperator
(schema and symbol) or an OperatorSignature (with its operand types, null for NONE), an
aggregate is a RoutineReference, and a definition list holds DefinitionElement items with a
DefinitionArgument. PostgreSQL checks the names and values of a definition when it runs the
statement, so they are not restricted.
Statement |
Model |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getNewName() is the new name, the role or the schema of the action. An OperatorClassItem
is one member of an operator class or family: its Kind is OPERATOR, FUNCTION or
STORAGE, with the strategy or support number, the operator and its optional operand types, the
Purpose (FOR SEARCH or FOR ORDER BY with getSortFamily()), the function as a
RoutineReference, or the storage type. A member that ALTER OPERATOR FAMILY ... DROP removes
has a kind, a number and operand types only.
CreateOperator operator = (CreateOperator) CCJSqlParserUtil.parse(
"CREATE OPERATOR app.=== (FUNCTION = app.eq, LEFTARG = int, RIGHTARG = int, "
+ "NEGATOR = OPERATOR(app.!==), HASHES)");
operator.getOperator().getSchemaName(); // app
operator.getOperator().getName(); // ===
operator.getDefinitionElement("NEGATOR").getArgument().getOperator(); // app.!==
operator.getDefinitionElement("HASHES").getArgument(); // null
CreateOperatorClass operatorClass = (CreateOperatorClass) CCJSqlParserUtil.parse(
"CREATE OPERATOR CLASS oc DEFAULT FOR TYPE int4 USING btree AS "
+ "OPERATOR 1 < (int4, int4), FUNCTION 1 btint4cmp(int4, int4)");
OperatorClassItem item = operatorClass.getItems().get(0);
item.getNumber(); // 1
item.getOperator().getName(); // <
item.getOperandTypes(); // [int4, int4]
CreateAggregate aggregate = (CreateAggregate) CCJSqlParserUtil.parse(
"CREATE AGGREGATE app.total (numeric ORDER BY int) (SFUNC = app.add, STYPE = numeric)");
aggregate.getAggregate().getName(); // app.total
aggregate.getAggregate().getOrderByArguments(); // [int]
aggregate.getDefinitionElement("STYPE").getArgument().getDataType(); // numeric
An operator name is one name with its exact text, such as ===, <==> or @#@, wherever
PostgreSQL DDL names an operator. It follows the rules of PostgreSQL’s lexer: the parts must touch
each other, a name of several characters ends in + or - only if it contains one of
~ ! @ # % ^ & | ?, => alone is not an operator, and a name has at most 63 characters. A
database may precede the schema (db.schema.+); getSchemaName() then returns both. An
operator that contains a backtick is not supported, and without Dialect.POSTGRESQL the lexer
reads # before #, @ or a letter as the start of a name. An aggregate signature is
* or has at least one argument, so agg() is rejected wherever an aggregate is named.
Without a dialect, CREATE OPERATOR and ALTER OPERATOR are read this way when an operator
symbol follows, so Oracle’s CREATE OPERATOR name BINDING ... stays an
UnsupportedStatement, and so do MySQL’s CREATE AGGREGATE FUNCTION and SQL Server’s
CREATE AGGREGATE name (@parameter type) .... With Dialect.POSTGRESQL they are parse
errors. Validators report createOperator, alterOperator, createOperatorClass,
alterOperatorClass, createOperatorFamily, alterOperatorFamily, createAggregate
and alterAggregate, which PostgreSQL enables.
See CREATE OPERATOR, ALTER OPERATOR, CREATE OPERATOR CLASS, ALTER OPERATOR CLASS, CREATE OPERATOR FAMILY, ALTER OPERATOR FAMILY, CREATE AGGREGATE and ALTER AGGREGATE.
Classify a Statement¶
Every Statement can tell you what it does — whether it reads, writes, changes the schema, or sends rows back — without a second parse and without writing a visitor:
StatementFeatures features = CCJSqlParserUtil.parse(sqlStr).getFeatures();
if (features.returnsResultSet()) {
statement.executeQuery(sqlStr);
} else {
statement.executeUpdate(sqlStr);
}
Why bother¶
Two jobs come up constantly, and both are traps if you approach them with string matching:
Safeguarding a read-only client. Reporting tools, BI front-ends, LLM-generated SQL, user-supplied filters — plenty of code paths need to reject anything that writes, before the statement reaches the database. Checking whether the text starts with SELECT is not a safeguard.
Dispatching correctly. JDBC wants executeQuery() for row-returning statements and executeUpdate() for the rest. Get it backwards and you get an exception, not a wrong answer, but you still have to decide.
The reason a keyword check fails is that SQL is not organised into tidy Query/DML/DDL buckets. RETURNING turns a DELETE into a row source. A data-modifying CTE hides that DELETE inside something that begins with WITH. An INSERT can contain a whole SELECT and still return nothing:
SQL |
returns rows |
reads |
writes |
|---|---|---|---|
|
yes |
yes |
no |
|
no |
yes |
yes |
|
yes |
no |
yes |
|
yes |
no |
yes |
|
no |
no |
yes |
|
no |
yes |
no (schema) |
|
no |
yes |
yes |
Note the last two rows of the fourth and fifth entries: RETURNING appearing somewhere in the statement is not the question. What matters is whether rows reach the client, and that is a property of the statement’s own result position, not of any nested one.
The features¶
|
Meaning |
Typical statements |
|---|---|---|
|
reads persistent rows |
|
|
rows are sent back to the client |
|
|
rows are written or destroyed |
|
|
the catalogue changes |
|
|
session state changes |
|
|
transaction state or locks change |
|
|
nothing further can be known statically |
|
They are not mutually exclusive. INSERT .. RETURNING * carries MODIFIES_DATA and RETURNS_RESULT_SET; TRUNCATE carries MODIFIES_SCHEMA and MODIFIES_DATA, so that a guard looking only for data changes still stops it.
Proven, possible, excluded¶
Each feature is three-valued, because some questions cannot be answered from syntax alone. SELECT nextval('s') writes; SELECT upper(name) does not; the parser cannot tell them apart, because volatility lives in the database catalogue, not in the SQL text.
So a feature is either proven, not excludable, or ruled out, and you pick which side you want to be wrong on:
StatementFeatures features = statement.getFeatures();
features.is(StmtFeature.MODIFIES_DATA); // the grammar proves it
features.may(StmtFeature.MODIFIES_DATA); // proven, or could not be excluded
Caller |
Uses |
Because |
|---|---|---|
read-only guard |
|
a false negative lets a write through |
JDBC dispatcher |
|
a false positive picks |
Convenience methods wrap the common combinations:
features.returnsResultSet(); // is(RETURNS_RESULT_SET)
features.modifiesData(); // is(MODIFIES_DATA)
features.mayModifyData(); // may(MODIFIES_DATA)
features.modifiesSchema(); // is(MODIFIES_SCHEMA)
features.isOpaque(); // CALL, EXECUTE, dynamic SQL
When the analysis identifies an unresolved function, procedure or dynamic SQL marker, it records the reference so you can consult your own catalogue or allow-list:
StatementFeatures features = CCJSqlParserUtil.parse(sqlStr).getFeatures();
if (connection.isReadOnly() && features.mayModifyData()) {
throw new SQLException(
"rejected, unresolved: " + features.getUnresolvedReferences());
// e.g. [nextval]
}
The generic fallback for an unclassified statement preserves OPAQUE and the possible effects READS_DATA, RETURNS_RESULT_SET, MODIFIES_DATA and MODIFIES_SCHEMA, but does not add a Java statement or container class name to getUnresolvedReferences(). The reference set can therefore be empty while the effects remain unknown. Check isOpaque() and may(..); an empty reference set alone does not establish safety.
Existing explicit references and markers remain, including procedure and function names, do, unsupported and explain. Stored routine declarations also retain their existing statement-type markers, such as createfunction; these are independent of the generic fallback.
If you can prove some functions side-effect free, hand in a predicate and the uncertainty collapses:
Set<String> pure = Set.of("upper", "lower", "coalesce");
StatementFeatures features = statement.getFeatures(pure::contains);
// SELECT upper(name) FROM t
features.mayModifyData(); // false
features.getUnresolvedReferences(); // empty
Warning
The verdict is a syntactic claim, not a semantic guarantee. A user-defined function, a trigger on the target table or a CALL can do anything. Use this to reject obviously dangerous SQL early; it does not replace database-side permissions.
Scripts¶
Statements is an ArrayList<Statement> and not a Statement, so it has no getFeatures() of its own. Two entry points, for two different questions:
Statements script = CCJSqlParserUtil.parseStatements(
"UPDATE t SET a = 1; SELECT a FROM t;");
// one union verdict — for guards
StatementFeatures all = StatementFeatureVisitor.analyse(script);
all.modifiesData(); // true
all.returnsResultSet(); // true
// one verdict per statement, in order — for dispatchers
List<StatementFeatures> each = StatementFeatureVisitor.analyseEach(script);
each.get(0).returnsResultSet(); // false, the UPDATE
each.get(1).returnsResultSet(); // true, the SELECT
The union answers “may this script write anything?”. It cannot answer “executeQuery or executeUpdate?”, because it never says which statement returns the rows.
Note
Nothing is cached. The tree is mutable and you may build statements by hand, so the verdict is recomputed on every call — microseconds against a millisecond-scale parse.
Find Table Names¶
net.sf.jsqlparser.util.TablesNamesFinder returns every table name in a statement or an expression, including the ones buried in sub-selects.
// find in Statements
String sqlStr = "select * from A left join B on A.id=B.id and A.age = (select age from C)";
Set<String> tableNames = TablesNamesFinder.findTables(sqlStr);
assertThat( tableNames ).containsExactlyInAnyOrder("A", "B", "C");
// find in Expressions
String exprStr = "A.id=B.id and A.age = (select age from C)";
tableNames = TablesNamesFinder.findTablesInExpression(exprStr);
assertThat( tableNames ).containsExactlyInAnyOrder("A", "B", "C");
A name is left out only where it resolves to a WITH item in scope. An alias hides no table, and without RECURSIVE a WITH item does not see itself or the items after it. The target of a DML statement is always reported. So the result may hold names that are no tables on some databases, such as a WITH item that recurses without RECURSIVE: check it against the metadata of the database if that matters.
Use the Visitor Patterns¶
Casting your way down the tree works for one known shape. For anything general — every column in a query, every table in a script — use a visitor: you override only the node types you care about and the adapters walk the rest.
There is one visitor interface per layer of the model, and an ..Adapter base class for each that already implements the full traversal:
Adapter |
Reacts to |
|---|---|
|
statements: |
|
query bodies: |
|
expressions: |
|
FROM items: |
// Define an Expression Visitor reacting on any Expression
// Overwrite the visit() methods for each Expression Class
ExpressionVisitorAdapter<Void> expressionVisitorAdapter = new ExpressionVisitorAdapter<>() {
public <S> Void visit(EqualsTo equalsTo, S context) {
equalsTo.getLeftExpression().accept(this, context);
equalsTo.getRightExpression().accept(this, context);
return null;
}
public <S> Void visit(Column column, S context) {
System.out.println("Found a Column " + column.getColumnName());
return null;
}
};
// Define a Select Visitor reacting on a Plain Select invoking the Expression Visitor on the Where Clause
SelectVisitorAdapter<Void> selectVisitorAdapter = new SelectVisitorAdapter<>() {
@Override
public <S> Void visit(PlainSelect plainSelect, S context) {
return plainSelect.getWhere().accept(expressionVisitorAdapter, context);
}
};
// Define a Statement Visitor for dispatching the Statements
StatementVisitorAdapter<Void> statementVisitor = new StatementVisitorAdapter<>() {
public <S> Void visit(Select select, S context) {
return select.getSelectBody().accept(selectVisitorAdapter, context);
}
};
String sqlStr="select 1 from dual where a=b";
Statement stmt = CCJSqlParserUtil.parse(sqlStr);
// Invoke the Statement Visitor without a context
stmt.accept(statementVisitor, null);
Tip
The second parameter of every visit() is a free-form context object of your choosing, threaded through the traversal. Pass null when you do not need it.
Build a SQL Statement¶
The object model works in both directions. Build the tree from Java and print it as SQL:
String expectedSQLStr = "SELECT 1 FROM dual t WHERE a = b";
// Step 1: generate the Java Object Hierarchy for
Table table = new Table().withName("dual").withAlias(new Alias("t", false));
Column columnA = new Column().withColumnName("a");
Column columnB = new Column().withColumnName("b");
Expression whereExpression =
new EqualsTo().withLeftExpression(columnA).withRightExpression(columnB);
PlainSelect select = new PlainSelect().addSelectItem(new LongValue(1))
.withFromItem(table).withWhere(whereExpression);
// Step 2a: Print into a SQL Statement
Assertions.assertEquals(expectedSQLStr, select.toString());
// Step 2b: De-Parse into a SQL Statement
StringBuilder builder = new StringBuilder();
StatementDeParser deParser = new StatementDeParser(builder);
deParser.visit(select);
Assertions.assertEquals(expectedSQLStr, builder.toString());
The same visitor can render an entire statement list:
Statements statements = CCJSqlParserUtil.parseStatements("SELECT 1; SELECT 2;");
StringBuilder script = new StringBuilder();
statements.accept(new StatementDeParser(script), null);
Assertions.assertEquals("SELECT 1;\nSELECT 2;\n", script.toString());
Statement lists and nested blocks share the separator policy used by
Statements.toString(). Blocks and IF/ELSE statements retain their own
semicolon settings. Custom deparsers receive each child statement and the
IF/ELSE condition through the visitor API, with the supplied context.
Customize JSON expression output¶
ExpressionDeParser visits structured children of JsonFunction and
JsonTableFunction while rendering them. A custom expression printer can
rewrite inputs, paths, PASSING values and DEFAULT expressions, including nested
JSON_TABLE columns, without changing the AST. JSON object keys and values are
visited when they are Expression objects; opaque objects and additional
query-path strings retain their existing rendering.
Statement statement = CCJSqlParserUtil.parse(
"SELECT JSON_VALUE((SELECT doc FROM source_table), '$.a')");
ExpressionDeParser expressions = new ExpressionDeParser() {
@Override
public <S> StringBuilder visit(Column column, S context) {
return getBuilder().append("payload");
}
};
statement.accept(new StatementDeParser(expressions, new SelectDeParser(),
expressions.getBuilder()), null);
// SELECT JSON_VALUE((SELECT payload FROM source_table), '$.a')
StatementDeParser connects the select and expression printers, so subqueries
inside JSON expressions use the same printer. For expression-only use with
subqueries, connect an appropriate SelectDeParser explicitly.
JSON nodes also expose appendTo(StringBuilder, Consumer<Expression>) for
direct rendering. The consumer prints each child into the same builder;
ExpressionDeParser forwards the visitor context through that callback.
JsonFunction additionally has consumer overloads of appendObject,
appendArray, appendValue, appendQuery and appendExists.
Subclass migration: override these consumer overloads or appendTo to
customize both default and visitor output. The deparser enters through appendTo;
overriding only a legacy single-argument append* or toString() method does
not customize that path. JsonFunction also uses the consumer overloads for
type-specific default rendering. Subclasses of concrete JSON_TABLE column
definitions should move toString() customization
to appendTo. Direct subclasses of the abstract JsonTableColumnDefinition
retain their opaque toString() fallback until they override appendTo.
ODBC timestamp intervals¶
In ODBC escapes such as {fn TIMESTAMPADD(SQL_TSI_YEAR, 2, travel_date)} and
{fn TIMESTAMPDIFF(SQL_TSI_DAY, start_date, end_date)}, the first argument is a
DateUnitExpression for the nine standard SQL_TSI_* interval keywords.
The original ODBC keyword is preserved on output and is not visited as a column.
This applies only to unqualified, escaped calls with three arguments and a bare
interval keyword. Ordinary calls, qualified names, quoted identifiers and other
arguments keep their existing expression interpretation.
Handle Parse Errors¶
CCJSqlParserUtil.parse(String, ...) requires a statement: null and empty string
inputs throw JSQLParserException, matching the default behavior for whitespace-only
and comment-only input. CCJSqlParserUtil.parseStatements(String, ...) returns a new,
mutable empty Statements list for null or empty input, as it already does for
whitespace-only and comment-only input. This applies to the overloads with parser
configuration callbacks and caller-provided executors; caller-provided executors remain
open. These empty-input results replace the previous null returns of these methods.
By default a syntax error aborts the whole parse. Two features let a script survive one bad statement:
parser.withErrorRecovery(true)skips to the next statement separator and returns an empty statement.parser.withUnsupportedStatements(true)captures an unrecognised statement root asUnsupportedStatement; it may be the first statement. Failures in recognised statement grammar still throw, or usewithErrorRecovery(true)and record the error. ThusSELECT * FROM,INSERT INTO t (aandUPDATE t SETdo not become opaque statements. The two options serve different purposes; enabling unsupported capture does not suppress recognised syntax errors.
CCJSqlParser parser = new CCJSqlParser(
"select * from mytable; select from; select * from mytable2" );
Statements statements = parser.withErrorRecovery().Statements();
// 3 statements, the failing one set to NULL
assertEquals(3, statements.size());
assertNull(statements.get(1));
// errors are recorded
assertEquals(1, parser.getParseErrors().size());
Statements statements = CCJSqlParserUtil.parseStatements(
"select * from mytable; shutdown defrag; select * from mytable2; select 4;"
, parser -> parser.withUnsupportedStatements() );
// 4 statements with one Unsupported Statement holding the content
assertEquals(4, statements.size());
assertInstanceOf(UnsupportedStatement.class, statements.get(1));
assertEquals("shutdown defrag", statements.get(1).toString());
// no errors records, because a statement has been returned
assertEquals(0, parser.getParseErrors().size());
Note
Opaque capture is not SQL validation. An unrecognised root or an existing opaque
grammar branch cannot establish validity in an unknown dialect. Callers that need
validity must validate against that dialect. Recognised syntax failures formerly
captured by the generic fallback now throw or produce an error-recovery placeholder;
partial IF results and typed prefixes with invalid trailing clauses are not published
as successful statements. SingleStatement() remains an incremental entry point;
use Statement() or Statements() to check complete statement boundaries.
An UnsupportedStatement is reported as OPAQUE by Classify a Statement — nothing about its effects is knowable.
Choose a Dialect¶
One grammar covers every supported RDBMS, but a few pieces of syntax mean different things in different products. Those are switched with parser features, and a Dialect preset turns on the right set for you.
// MySQL: backslash escapes, hash line comments, double-quoted strings
Statement stmt = CCJSqlParserUtil.parse(
"SELECT `col` FROM t WHERE a = 'x\\'yz' AND b = 42#24"
, parser -> parser.withDialect(Dialect.MYSQL) );
|
Turns on |
|---|---|
|
|
|
|
|
tagged dollar strings, the newline rule for ordinary string literals, literal-local |
|
the newline rule for adjacent string literals |
|
|
|
|
|
|
|
Informix |
|
GoogleSQL |
|
|
|
|
|
|
|
|
Features set explicitly after the preset win over it.
With Dialect.MYSQL, -- starts a line comment only when followed by
whitespace or a control character. Thus SELECT 1--2 contains subtraction
and a negative operand. SQL inside an unversioned executable comment is parsed
normally: SELECT 1 /*! + 2 */ becomes SELECT 1 + 2. Optimizer hints
(/*+ ... */) remain separate from executable comments.
Conditional executable comments require an explicit target server version:
Statement stmt = CCJSqlParserUtil.parse(
"SELECT 1 /*!90702 + 2 */",
parser -> parser.withDialect(Dialect.MYSQL).withMySqlServerVersion(90702));
The value is major * 10000 + minor * 100 + patch; for example, 90702
means 9.7.2 and 260700 means 26.7.0. Five-digit and six-digit comment version
prefixes follow MySQL’s whitespace rules. SQL for a newer server is ignored;
SQL for the configured version or an older one participates in the AST.
Both SQL renderers produce the resolved SQL for that target, without the
conditional wrappers. Without a target, a conditional executable comment
raises an error instead of silently discarding its SQL. This replaces the
previous treatment of all executable comments as ordinary discarded comments.
The same configuration applies to parsers constructed with a StreamProvider
or FeatureConfiguration. Other dialects retain their comment behavior.
Nested executable comments are rejected; nested-comment acceptance in other
dialects is not a guarantee that the same SQL is valid on MySQL.
Non-reserved names such as comment work unquoted in ADD, MODIFY,
CHANGE, DROP and RENAME column actions. The COLUMN keyword does
not change how the name is interpreted. Column definitions remain editable:
Alter alter = (Alter) CCJSqlParserUtil.parse(
"ALTER TABLE t MODIFY COLUMN comment TEXT",
parser -> parser.withDialect(Dialect.MYSQL));
AlterExpression.ColumnDataType column = alter.getAlterExpressions().get(0)
.getColDataTypeList().get(0);
column.setColumnName("notes");
column.getColDataType().setDataType("LONGTEXT");
// ALTER TABLE t MODIFY COLUMN notes LONGTEXT
String sql = alter.toString();
Select Dialect.POSTGRESQL when parsing PostgreSQL SQL. For example,
DROP INDEX "a.b" has the single name "a.b" and no schema; the dot
inside the quotes is not a separator. This applies to table and column
references throughout DDL and DML. The default configuration retains its
historical name-splitting behavior for compatibility with BigQuery names.
COMMENT ON TABLE t IS $tag$body$tag$ accepts tagged dollar strings with
the PostgreSQL preset, or with withDollarQuotedStringTags(true). Tagged
strings remain disabled by default. Table, column and view comments preserve
their dollar delimiter and literal body. Ordinary single-quoted strings
separated by a newline concatenate; dollar-quoted strings do not.
E'...' enables backslash escapes for that literal without changing
the treatment of ordinary strings elsewhere in the statement.
The shared type grammar accepts negative scales, including
numeric(2, -3), in type fragments, DDL and casts. getPrecision()
returns 2 and getScale() returns -3; an omitted scale returns
null. PostgreSQL’s precision and scale range checks remain the database’s
responsibility. When constructing a ColDataType directly,
ColDataType.fromNumericParameters("numeric", 2, -3) accepts negative scales;
use null for omitted parameters. The legacy int
constructor continues to treat negative arguments as omitted parameters.
StringValue.getValue() returns the literal body as written, and toString()
renders it unchanged. getNotExcapedValue() returns the characters it denotes.
A doubled quote of the literal’s own quote character is resolved, so
"it""s" yields it"s. getEscaping() records how the parser read
backslashes:
MYSQL_BACKSLASHwithout a dialect or withDialect.MYSQL/MARIADBwhen backslash escapes are enabled:'it\'s'yieldsit's,\n,\t,\0and\Zbecome their characters, and\%and\_keep their backslash.POSTGRESQL_ESCAPE_STRINGforE'...'withDialect.POSTGRESQL, including\x, octal and\uescapes; octal and hexadecimal escapes are UTF-8 bytes.POSTGRESQL_UNICODE_ESCAPEforU&'...', see `PostgreSQL Unicode escape strings`_.STANDARDotherwise, including dollar-quoted strings and constructed values; backslashes stay literal. Other dialects with backslash escapes keep this reading.
setEscaping() selects the reading for a constructed value. Escaping is not
part of equals(), like the quote character.
With Dialect.POSTGRESQL, U&'...' and an optional UESCAPE 'c' clause form one
StringValue. Its prefix is U&, getValue() returns the body as written, and
toString() renders the literal with its clause. getUnicodeEscapeCharacter() returns
the UESCAPE character, or null without the clause, when the escape character is a
backslash. getNotExcapedValue() resolves the escapes as PostgreSQL does:
the escape character followed by four hexadecimal digits, or by
+and six, is that code point. A character above U+FFFF can also be written as a UTF-16 surrogate pair of two escapes;a doubled escape character and a doubled quote stand for themselves.
Comment comment = (Comment) CCJSqlParserUtil.parse(
"COMMENT ON TABLE t IS U&'d!0061t!+000061' UESCAPE '!'",
parser -> parser.withDialect(Dialect.POSTGRESQL));
comment.getComment().getNotExcapedValue(); // "data"
comment.toString(); // COMMENT ON TABLE t IS U&'d!0061t!+000061' UESCAPE '!'
The parser rejects what PostgreSQL rejects: an escape without its digits, code point 0 or one above U+10FFFF, an unpaired surrogate, and an escape character that is a hexadecimal digit,
+, a quote, whitespace or not ASCII.getNotExcapedValue()throwsIllegalArgumentExceptionfor such a constructed value.Without
Dialect.POSTGRESQL,U&'x'keeps its previous reading: the columnU, the&operator and the string'x'.UESCAPEmay follow the literal after spaces, tabs or newlines, but not after a comment or a continuation line.U&"..."quoted identifiers are not supported.The
UESCAPEcharacter is not part ofequals(), like the escaping.
MySQL user-variable targets in SELECT ... INTO @variable require
Dialect.MYSQL or Dialect.MARIADB. They are stored in
PlainSelect.getMySqlSelectIntoClause().getVariables() as UserVariable
expressions, with the clause position preserved before FROM or at the end
of the query. They are not table targets in getIntoTables().
Doris distribution hints require parser.withDialect(Dialect.DORIS).
Join.getJoinHint() exposes the keyword and Position.AFTER_JOIN;
the existing SQL Server hints use Position.BEFORE_JOIN. Rendering preserves
both the position and the brackets around a Doris hint.
CockroachDB primary-key changes require parser.withDialect(Dialect.COCKROACHDB).
Their action is an AlterExpressionPrimaryKey with key elements and storage
parameters in getConstraint(). isUsingHash() preserves USING HASH, while
getBucketCount() holds the legacy WITH BUCKET_COUNT = expression value.
The newer WITH (bucket_count = expression) form uses the index storage parameters.
With Dialect.TERADATA, UPDATE a FROM target a, source b SET a.id = b.id
uses the existing Update model’s fromItem and joins properties.
isFromBeforeSet() preserves the clause position in both SQL renderers.
Table discovery and metadata validation recognize a target alias declared in
that FROM clause. Other dialects retain the existing FROM-after-SET syntax.
Dialect.SQLSERVER supports methods on expression results, including
(SELECT ... FOR XML PATH(''), TYPE).value('.', 'varchar(max)').
MethodCallExpression exposes the receiver expression and a Function
containing the method name and arguments. Field access and method calls share
the navigation grammar; expression visitors and deparsers traverse both the
receiver and method arguments. XQuery strings remain string literals.
Dialect.POSTGRESQL enables DO [LANGUAGE name] code [LANGUAGE name],
with the language clause allowed once, before or after the body.
DoStatement.getCode() is a StringValue that preserves the literal’s
quotes, dollar tag and body text. The optional language and its position have
separate properties; an omitted language remains unspecified in the AST.
The body is language-specific source, not a parsed PL/pgSQL statement tree.
Expression visitors can inspect or replace the body literal. Feature analysis
reports OPAQUE; table discovery rejects this statement because the body’s
table accesses are unknown. Validation checks the doStatement capability,
without validating the procedural language inside the literal.
Enable the PostgreSQL dialect when parsing a script containing a DO block:
Statements statements = CCJSqlParserUtil.parseStatements(
"DO $$BEGIN RAISE NOTICE 'hello'; END$$; SELECT 1;",
parser -> parser.withDialect(Dialect.POSTGRESQL));
DoStatement block = (DoStatement) statements.get(0);
String body = block.getCode().getValue();
// body: BEGIN RAISE NOTICE 'hello'; END
// statements.get(1) is the following SELECT.
Semicolons and SQL statements inside the body remain part of its string literal; they do not split the surrounding script into additional statements.
PostgreSQL CREATE FUNCTION and CREATE PROCEDURE declarations also preserve
AS '...', AS E'...' and dollar-quoted bodies as opaque text. Their
getFunctionDeclarationParts() list retains the body and trailing options,
while a following statement is parsed separately. Newline-separated body string
continuations retain the newline needed when the declaration is rendered again.
Use Dialect.POSTGRESQL for tagged dollar quotes and PostgreSQL-specific DDL
such as schema-qualified index collations and operator classes. This support
does not validate PL/pgSQL source or build an AST for statements inside the body.
With Dialect.POSTGRESQL, # terminates an unquoted identifier, so JSON
operators such as js#>>'{a}' and js#>'{a}' work without surrounding
spaces. Quote identifiers containing #, for example "js#". Other
dialects retain their existing identifier and hash-comment rules.
With Dialect.SQLSERVER, SET NOCOUNT ON and grouped boolean options such as
SET QUOTED_IDENTIFIER, ANSI_NULLS OFF use SetStatement.getOnOffOptions().
The ordered OnOffOption list and shared isOn() value are editable;
setOnOffOptions() replaces generic assignments and their scope. Both SQL
renderers share statement punctuation while generic assignments retain expression
visitor support. Parsing a SET directive records it without changing lexer settings.
Dialect.SQLSERVER enables INSERT BULK table (name type, ...) WITH (...).
InsertBulk exposes the target table, existing ColumnDefinition models,
and ordered typed options, including ROWS_PER_BATCH and ORDER keys.
The SQL declaration is preserved; the following binary bulk-load data stream
is outside the SQL parser. Visitors and deparsers traverse option values and
ordering expressions. Validation uses the insertBulk capability.
With Dialect.SQLSERVER, PRIMARY KEY NONCLUSTERED (id) and
UNIQUE CLUSTERED (id) store their clustering option in Index.getClustering()
for both CREATE TABLE and ALTER TABLE. Without that dialect, these words
retain their existing interpretation as optional index names.
SQL Server CREATE TABLE also accepts a trailing comma after the final column
or table constraint. SQL output normalizes the definition by omitting that comma.
CREATE UNIQUE NONCLUSTERED INDEX ix ON t (id) also requires
Dialect.SQLSERVER. Uniqueness remains in Index.getType() and clustering
is stored separately in Index.getClustering(). With Dialect.SPANNER,
CREATE UNIQUE NULL_FILTERED INDEX ix ON t (id) stores null filtering in
CreateIndex.isNullFiltered(). An omitted clustering or null-filtering option
is not supplied from database defaults. The Spanner preset currently selects
this index syntax; it does not configure GoogleSQL string-literal rules.
With Dialect.POSTGRESQL, index keys accept schema-qualified collation and
operator-class names, for example name COLLATE pg_catalog."C"
pg_catalog.text_ops ASC NULLS LAST. Function keys such as lower(name) are
stored as expressions. Key attributes are available through getCollation(),
getOperatorClass(), getOperatorClassParameters(), getSortOrder() and
getNullOrdering() on KeyElement. Under this dialect these
attributes are not duplicated in the legacy getParams() list, so changing
or removing them is reflected when rendering SQL. Other dialects retain the
legacy parameter representation, including MySQL prefix lengths.
CreateIndexDeParser and StatementDeParser pass key expressions,
structured option values and the partial-index predicate to their expression
visitor. StatementVisitorAdapter, TablesNamesFinder and index validation
traverse the same structured expressions.
Informix’s constraint form requires an explicit dialect selection:
Statement stmt = CCJSqlParserUtil.parse(
"ALTER TABLE child ADD CONSTRAINT FOREIGN KEY (id) "
+ "REFERENCES parent(id) CONSTRAINT fk_child",
parser -> parser.withDialect(Dialect.INFORMIX));
Row pattern matching (MATCH_RECOGNIZE)¶
MatchRecognize is a FromItem wrapping the input relation. Its input
alias and output alias are independent. getMeasures() and
getDefinitions() expose ordinary SQL expressions; getPattern() exposes
an editable RowPattern tree with variables, groups, ordered alternatives,
sequences, anchors and quantifiers. Oracle and Snowflake additionally support
PERMUTE and exclusion nodes. Permutations remain compact in the AST.
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
"SELECT * FROM events MATCH_RECOGNIZE ("
+ "ORDER BY seq MEASURES SUM(A.price) AS total "
+ "PATTERN (A+) DEFINE A AS price > 0 "
+ "OPTIONS (use_longest_match = TRUE))",
parser -> parser.withDialect(Dialect.BIGQUERY));
MatchRecognize match = (MatchRecognize) select.getFromItem();
RowPattern.Quantified repetition = (RowPattern.Quantified) match.getPattern();
repetition.setReluctant(true);
System.out.println(select); // PATTERN (A+?)
FromItemVisitorAdapter traverses the input and clause expressions.
RowPatternVisitorAdapter traverses pattern nodes and optionally visits
expressions in quantifier bounds. RowPatternFunction represents an explicit
RUNNING or FINAL prefix while exposing the underlying Function to
existing expression visitors. Both toString() and the deparsers render the
current AST, including edited nested STRUCT arguments.
Select the source dialect explicitly:
BIGQUERYrequiresORDER BY,MEASURES,PATTERNandDEFINE. It supports empty alternatives, literal or query-parameter bounds, adjacent anchors such as^A+$, andOPTIONS (use_longest_match = TRUE/FALSE).ORACLEsupports optional ordering/measures, row output modes, targetedAFTER MATCH SKIP,SUBSET, permutations, exclusions and function modes.SNOWFLAKEsupports the corresponding documented forms exceptSUBSETand BigQuery options. Its documented alternation-before-concatenation precedence is used for the pattern AST. Other presets use concatenation-before-alternation. Rendering adds explicit parentheses at mixed operator boundaries; it does not translate expression syntax between DBs.
Validation checks the matchRecognize and matchRecognizeOptions features.
With an explicit dialect, it also checks selected static rules: pattern variable
names, bounds, subset/skip targets and incompatible clause options. Table
finding does not treat pattern qualifiers such as A.price as table names.
Metadata validation does not bind these columns to the input relation.
Function eligibility, aggregation/type rules, data-dependent skip failures and
query execution remain the database’s responsibility.
The test fixtures were compared before and after deparsing using GoogleSQL’s reference evaluator and Oracle 26ai Free. The reference evaluator is not the BigQuery service. Snowflake coverage follows its documentation and parser round-trip tests; it has not been executed against a Snowflake account.
See the official BigQuery MATCH_RECOGNIZE syntax, Oracle row pattern matching guide and Snowflake MATCH_RECOGNIZE reference.
The individual features¶
Feature |
What it changes |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
adjacent string literals concatenate: |
|
permits deeply nested expressions, at a significant performance cost |
|
aborts parsing after N milliseconds |
String sqlStr="select 1 from [sample_table] where [a]=[b]";
// T-SQL Square Bracket Quotation
Statement stmt = CCJSqlParserUtil.parse(
sqlStr
, parser -> parser
.withSquareBracketQuotation(true)
);
// Set Parser Timeout to 6000 ms
Statement stmt1 = CCJSqlParserUtil.parse(
sqlStr
, parser -> parser
.withSquareBracketQuotation(true)
.withTimeOut(6000)
);
// Allow Complex Parsing (which allows nested Expressions, but is much slower)
Statement stmt2 = CCJSqlParserUtil.parse(
sqlStr
, parser -> parser
.withSquareBracketQuotation(true)
.withAllowComplexParsing(true)
.withTimeOut(6000)
);
// Allow Back-slash escaping
sqlStr="SELECT ('\\'Clark\\'', 'Kent')";
Statement stmt2 = CCJSqlParserUtil.parse(
sqlStr
, parser -> parser
.withBackslashEscapeCharacter(true)
);
Things that trip people up¶
Hint
Quoting: Double quotes
".."quote identifiers. Square brackets[..]are arrays unless you turn onwithSquareBracketQuotation.Reserved keywords: JSQLParser uses a more restrictive list than most databases, and such keywords need to be quoted.
Escaping: standard single-quote
'..escaping is always on. Backslash escaping is not — setwithBackslashEscapeCharacter.Oracle alternative quoting is partially supported, for common brackets:
q'{...}',q'[...]',q'(...)'andq''...''.
Compile from Source Code¶
You need JDK 8 or JDK 11. JSQLParser-4.9 is the last JDK 8 compatible release; everything after depends on JDK 11. Building JSQLParser-5.1 and newer with Gradle needs a JDK 17 toolchain, because of the plugins used.
git clone --depth 1 https://github.com/JSQLParser/JSqlParser.git
cd JSqlParser
mvn install
git clone --depth 1 https://github.com/JSQLParser/JSqlParser.git
cd JSqlParser
gradle publishToMavenLocal
PostgreSQL roles, privileges and triggers¶
CreateRole and AlterRole model role attributes and configuration changes,
including PostgreSQL’s USER/GROUP aliases. Role options are ordered and typed;
passwords and configuration values are expressions. Omitted options remain
omitted. CREATE USER name without attributes retains the existing MySQL
CreateUser AST by default. Select Dialect.POSTGRESQL explicitly to obtain
the PostgreSQL CreateRole AST for this ambiguous form:
CreateRole user = (CreateRole) CCJSqlParserUtil.parse(
"CREATE USER app",
parser -> parser.withDialect(AbstractJSqlParser.Dialect.POSTGRESQL));
MySQL account names (user[@host]) of CREATE USER and of view, trigger and
routine DEFINER clauses share one grammar rule and are read into separate user
and host StringValue parts (UserAccount and MySqlDefiner). Every MySQL
spelling is accepted, including app@localhost, app@'%' and
'app'@localhost. A part quoted with ', " or a backquote keeps its quote
character in getQuoteStr() and its text in getValue():
CreateUser createUser = (CreateUser) CCJSqlParserUtil.parse(
"CREATE USER `app`@'%' IDENTIFIED BY 'secret'");
UserAccount account = createUser.getAccounts().get(0);
account.getUser().getValue(); // app
account.getUser().getQuoteStr(); // `
account.getHost().getValue(); // %
Grant retains its string-based getters, setters and fluent methods. Its
PrivilegeClause exposes typed Privilege items, column lists,
PrivilegeTarget kinds, multiple role memberships and grantor information.
getPrivileges() is a mutable string view of the typed privilege list.
getRole() and getObjectName() expose the first role or named target for
legacy callers; use getRoleItems() and getTarget() for the complete lists.
Names in PrivilegeTarget are multipart identifiers, not SQL clause text.
Routine targets use RoutineReference data-type signatures, with null
arguments for an omitted signature and an empty list for explicit ().
Revoke shares the privilege payload and adds the revoked option and
CASCADE/RESTRICT behavior. AlterDefaultPrivileges has separate role/schema
scope and one nested GRANT or REVOKE. Table-name discovery reports explicit
table targets, not schema-wide targets, routine/type names or role names.
CreateTrigger supports both its existing MySQL statement body and a distinct
PostgreSQL routine invocation. PostgreSQL fields include multiple events,
UPDATE OF columns, constraint attributes, transition relations, row/statement
orientation and a WHEN expression. The absence of FOR ROW/STATEMENT is retained.
Expression visitors and StatementDeParser traverse privilege columns,
role values, trigger conditions and invocation arguments. Declaring a trigger
is classified as a schema change, not execution of its body.
Capability validation covers these statement families, not every server-version or catalog-dependent restriction. Event triggers use separate statements, described below. Serialized role SQL can contain passwords; avoid logging real credentials.
References: CREATE ROLE, ALTER ROLE, GRANT, REVOKE, ALTER DEFAULT PRIVILEGES, CREATE TRIGGER.
MySQL privileges and accounts in GRANT and REVOKE¶
Grant and Revoke also parse the MySQL forms, in every dialect:
Privilege levels:
PrivilegeTarget.getWildcard()isDEFAULT_DATABASEfor*,GLOBALfor*.*andDATABASEfordb.*, whose database is the single entry ofgetNames(). Named levels such asdb.tblkeep a null wildcard. Only named table targets are reported by table-name discovery.Privileges: MySQL static privileges are
Privilege.Kindconstants, for exampleCREATE_TEMPORARY_TABLESorREPLICATION_SLAVE;getName()returns the SQL spelling. Another identifier, such asBACKUP_ADMIN, is a dynamic privilege of kindDYNAMICwithgetDynamicName().Grantees:
PrivilegeClause.getGranteeItems()returnsGranteevalues, which MySQL and PostgreSQL share.ACCOUNTholds aUserAccount: a MySQLuser[@host], read like theCREATE USERaccount names above, or a PostgreSQL role name without host. The other kinds are PostgreSQLGROUP name,PUBLIC,CURRENT_USER(MySQL alsoCURRENT_USER()),CURRENT_ROLEandSESSION_USER.getGrantor()typesGRANTED BYthe same way.Role memberships:
getRoleItems()holds the granted roles asUserAccountvalues, so'reader'and'writer'@'%'keep their quoting and host. A membership has no privileges and no target.MySQL clauses:
PROXY ON useris aPROXYprivilege on anACCOUNTtarget (PrivilegeTarget.getAccount()).REVOKE ALL [PRIVILEGES], GRANT OPTION FROM ...has the privilegesALLandGRANT_OPTIONand no target.RevokerecordsIF EXISTSandIGNORE UNKNOWN USER;Grant.getAsUser()andgetWithRole()holdAS user [WITH ROLE ...]; the role selection is aRoleSelectionfromnet.sf.jsqlparser.statement.create.user.
Grant grant = (Grant) CCJSqlParserUtil.parse(
"GRANT SELECT, BACKUP_ADMIN ON db1.* TO 'app'@'%', CURRENT_USER");
grant.getTarget().getWildcard(); // DATABASE
grant.getPrivilegeItems().get(1).getDynamicName(); // BACKUP_ADMIN
UserAccount app = grant.getClause().getGranteeItems().get(0).getAccount();
app.getHost().getValue(); // %
Grant.getOptions() lists every WITH option, including PostgreSQL membership
options such as WITH INHERIT TRUE, SET FALSE. Privileges take only GRANT OPTION;
memberships take distinct ADMIN, INHERIT and SET options.
The string accessors are kept and derived from the typed state: getUsers(),
getGrantees() and getRoles() are mutable string views that write through,
getOption() is the first option and getGrantedBy() renders the grantor.
getObjectName() returns a wildcard level as written, for example db1.*, and
setObjectName() replaces it with a named target. Assigned text is converted with
Grantee.valueOf(): special role names are recognized and other text is kept as the
account name. Parsed memberships have no target; new Grant() still starts with an
empty table target, and setRole() clears it.
The MySQL and MariaDB validation capabilities now include revoke.
References: GRANT, REVOKE, Specifying Account Names.
PostgreSQL event triggers, REINDEX, SECURITY LABEL and schema elements¶
CREATE EVENT TRIGGER parses to CreateEventTrigger. getEvent() keeps the event
name as written and getFilters() the WHEN … IN (…) conditions in source order. The
invoked function reuses CreateTrigger.ExecuteKeyword and a Function without
arguments. ALTER EVENT TRIGGER parses to AlterEventTrigger; its states reuse
RelationAlterAction.EnableState, and each non-null setter selects the STATE,
OWNER or RENAME action. A MySQL event named trigger still parses to
CreateEvent or AlterEvent.
CreateEventTrigger trigger = (CreateEventTrigger) CCJSqlParserUtil.parse(
"CREATE EVENT TRIGGER etg ON ddl_command_start "
+ "WHEN TAG IN ('CREATE TABLE') EXECUTE FUNCTION abort_any_command()");
trigger.getEvent(); // ddl_command_start
trigger.getFilters().get(0).getVariable(); // TAG
trigger.getFunction().getName(); // abort_any_command
REINDEX parses to Reindex: a Target (INDEX, TABLE, SCHEMA, DATABASE or SYSTEM),
the CONCURRENTLY keyword, the parenthesized CONCURRENTLY, TABLESPACE and
VERBOSE options with their values as written, and the object name. Only DATABASE and
SYSTEM may omit the name. getTable() returns the name only for REINDEX TABLE, which
table visitors report. REINDEX is a non-reserved keyword.
SECURITY LABEL parses to SecurityLabel with the provider as written and one target,
told apart by getObjectType(): a named object (getObjectName()), a qualified column
(getColumn()), a FUNCTION, PROCEDURE, ROUTINE or AGGREGATE signature (getRoutine())
or a large object OID. A null label stands for IS NULL. Table visitors report tables,
views, materialized views, foreign tables and a column’s table.
CREATE SCHEMA keeps its schema elements in getStatements() and renders them after the
AUTHORIZATION and PATH clauses. The element kinds are PostgreSQL’s: CREATE TABLE,
CREATE VIEW, CREATE INDEX, CREATE SEQUENCE, CREATE TRIGGER and GRANT;
CREATE SCHEMA IF NOT EXISTS cannot include them. Elements keep their names as written,
and unqualified names refer to the new schema. The parser no longer copies the schema name
into CREATE TABLE elements. Table visitors, validation and StatementDeParser traverse
the elements.
Validators report createEventTrigger, alterEventTrigger, reindex,
reindexConcurrently (PostgreSQL 12) and securityLabel. Feature analysis classifies
these statements as schema changes; REINDEX also reads data.
References: CREATE EVENT TRIGGER, ALTER EVENT TRIGGER, REINDEX, SECURITY LABEL, CREATE SCHEMA.
Oracle anonymous blocks¶
With Dialect.ORACLE, OracleBlock extends Block with variable declarations
and exception handlers. Initializers and OracleAssignment values are expressions;
nested blocks and handler bodies contain statements. Calls without CALL use
Execute.ExecType.IMPLICIT, preserving qualified names, parentheses and bind arguments.
OracleNullStatement represents the PL/SQL NULL statement. Implicit calls are
recognized inside Oracle blocks, so application procedure names need no keyword registration.
Shared traversal and rendering include declaration initializers, assignments and exception handler bodies. Procedure side effects remain unknown; table discovery reports unsupported procedure calls, and feature analysis remains conservative. This covers anonymous blocks with variable declarations, SQL statements, assignments, calls, nesting and handlers, not all PL/SQL declarations, loops, packages or procedure definitions.
SQL Server routine declarations¶
Dialect.SQLSERVER uses a shared declaration path for CREATE, ALTER and
CREATE OR ALTER FUNCTION/PROCEDURE. CreateFunctionalStatement.getOperation()
identifies the operation. For functions, getReturnType() exposes scalar types,
inline RETURNS TABLE, and a return variable with ordered TableElement column
and constraint definitions. Table elements reuse the existing definition traversal
and deparser, including custom expression visitors.
With a structured return type, getFunctionDeclarationParts() contains the name
and parameter tokens; getRoutineBodyParts() contains the following options and
body. These remain opaque tokens, so this does not implement a T-SQL body AST or
resolve tables used inside a routine. Other dialects retain the existing token-list
representation. New operations have separate validation capabilities.
Parse procedure definitions one SQL Server batch at a time: a procedure consumes the
remaining batch, including SQL after an END. Client-side GO batch splitting is
not performed by this routine declaration parser.
PostgreSQL table-level NOT NULL constraints¶
Dialect.POSTGRESQL supports PostgreSQL 18’s table-level
CONSTRAINT nn NOT NULL id, in both CREATE TABLE and ALTER TABLE ADD.
NotNullConstraint exposes the constraint name, target Column and
noInherit flag; getConstraintAttributes().isNotValid() represents an
ALTER NOT VALID clause when present.
Alter alter = (Alter) CCJSqlParserUtil.parse(
"ALTER TABLE t ADD CONSTRAINT nn NOT NULL id NOT VALID",
parser -> parser.withDialect(Dialect.POSTGRESQL));
NotNullConstraint constraint = (NotNullConstraint)
alter.getAlterExpressions().get(0).getConstraint();
constraint.getColumn().setColumnName("other_id");
constraint.setName("other_nn");
The target column participates in expression visitors and deparsers. New nodes
can be built with new NotNullConstraint().withName("nn")
.withColumn(new Column("id")). This is distinct from column definitions and
ALTER COLUMN ... SET NOT NULL; those retain their existing APIs.
MySQL and PostgreSQL tablespaces¶
CREATE TABLESPACE parses to CreateTablespace and ALTER TABLESPACE to
AlterTablespace in every dialect; isUndo() records the MySQL UNDO keyword.
A PostgreSQL definition exposes getOwner(), getLocation() and the WITH
parameters as IndexOption values in getParameters(). A MySQL definition exposes
getDataFile() and keeps its options in source order in getOptions(). They reuse the
TableOption kinds ENGINE, ENCRYPTION, AUTOEXTEND_SIZE and
ENGINE_ATTRIBUTE; CREATE also accepts FILE_BLOCK_SIZE.
CreateTablespace create = (CreateTablespace) CCJSqlParserUtil.parse(
"CREATE TABLESPACE ts1 ADD DATAFILE 'ts1.ibd' FILE_BLOCK_SIZE = 8192 ENGINE = InnoDB");
create.getDataFile().getValue(); // ts1.ibd
create.getOption(TableOption.Kind.ENGINE).get().getValue(); // InnoDB
AlterTablespace alter = (AlterTablespace) CCJSqlParserUtil.parse(
"ALTER TABLESPACE fast SET (random_page_cost = 1.1)");
alter.getAction(); // SET_PARAMETERS
alter.getParameters(); // [random_page_cost = 1.1]
getAction() is RENAME, OWNER, SET_PARAMETERS or RESET_PARAMETERS,
SET_STATE for ALTER UNDO TABLESPACE … SET ACTIVE|INACTIVE, or OPTIONS for MySQL
options. Undo tablespaces accept only ENGINE, CREATE UNDO TABLESPACE requires
ADD DATAFILE, and ALTER rejects FILE_BLOCK_SIZE. Other ALTER TABLESPACE forms,
such as Oracle and NDB data file clauses, still parse as UnsupportedStatement, and
ALTER TABLE ALL IN TABLESPACE remains AlterTablespaceMove. Validators report the
createTablespace and alterTablespace features, which the MySQL and PostgreSQL
capabilities enable; feature analysis reports MODIFIES_SCHEMA.
MySQL spatial reference systems and ALTER INSTANCE¶
MySQL CREATE SPATIAL REFERENCE SYSTEM parses to
CreateSpatialReferenceSystem. isOrReplace() and isIfNotExists() exclude each
other, getSrid() is the number as written (a LongValue, or a HexValue for
0x... and X'...'), and getAttributes() keeps the NAME, DEFINITION,
ORGANIZATION … IDENTIFIED BY and DESCRIPTION attributes in source order as
SpatialReferenceSystemAttribute values. getName(), getDefinition(),
getOrganization(), getOrganizationId() and getDescription() return the first
attribute of their kind. The parser follows MySQL’s grammar, which takes the attributes in
any order and any number: the server reports a repeated attribute, a missing NAME or
DEFINITION and an SRID above 4294967295 when it runs the statement, not as syntax
errors. DROP SPATIAL REFERENCE SYSTEM parses to
Drop with the object type SPATIAL_REFERENCE_SYSTEM.
CreateSpatialReferenceSystem srs = (CreateSpatialReferenceSystem) CCJSqlParserUtil.parse(
"CREATE SPATIAL REFERENCE SYSTEM 4120 NAME 'Greek' ORGANIZATION 'EPSG' "
+ "IDENTIFIED BY 4120 DEFINITION 'GEOGCS[\"Greek\"]'");
srs.getSrid(); // 4120
srs.getName().getValue(); // Greek
srs.getOrganizationId(); // 4120
MySQL ALTER INSTANCE parses
to AlterInstance. getAction() is ENABLE_INNODB_REDO_LOG,
DISABLE_INNODB_REDO_LOG, ROTATE_INNODB_MASTER_KEY, ROTATE_BINLOG_MASTER_KEY,
RELOAD_TLS or RELOAD_KEYRING. For RELOAD TLS, getChannel() is the identifier
after FOR CHANNEL as written, or null, and isNoRollbackOnError() records
NO ROLLBACK ON ERROR. MySQL reads INNODB, BINLOG and REDO_LOG as
identifiers, so they may be written in backticks, and the engine of ROTATE also as a
string; the statement prints them as plain words. The grammar takes any identifier as the
channel; the server accepts mysql_main and mysql_admin.
These statements are read with Dialect.MYSQL and without a dialect. With
Dialect.MYSQL, a CREATE SPATIAL statement that is neither this one nor
CREATE SPATIAL INDEX, and an ALTER INSTANCE with another action, are errors;
without a dialect they stay UnsupportedStatement, as in the other dialects, where
DROP SPATIAL REFERENCE SYSTEM is still rejected. REFERENCE, INSTANCE,
ROTATE, RELOAD and the other words remain ordinary identifiers. Validators report
the createSpatialReferenceSystem and alterInstance features, which the MySQL
capabilities enable, and drop. Feature analysis reports MODIFIES_SCHEMA for
CREATE SPATIAL REFERENCE SYSTEM and MODIFIES_SESSION for ALTER INSTANCE, a
server operation like ALTER SYSTEM.
SQL Server identity inserts¶
With Dialect.SQLSERVER, SET IDENTITY_INSERT dbo.actor ON uses
SetIdentityInsertStatement. getTable() reuses the qualified Table AST;
isOn() and setOn() expose the session setting. Table names may include a
database and schema, including SQL Server bracket-quoted identifiers. Table
visitors, both SQL renderers and metadata validation use this structured target.
Feature analysis reports MODIFIES_SESSION; the directive itself inserts no rows.
The dedicated validation capability is setIdentityInsert.
Database options and configuration parameters¶
CreateDatabase, MySQL’s CREATE SCHEMA and AlterDatabase share the
DatabaseOption model. MySQL options are CHARACTER_SET (getName() keeps
the CHARACTER SET, CHAR SET or CHARSET spelling), COLLATE,
ENCRYPTION and, for ALTER only, READ_ONLY. PostgreSQL options have one kind
each, from OWNER and TEMPLATE to CONNECTION_LIMIT, IS_TEMPLATE and
OID; isUseWith() records the optional WITH. Values keep their SQL text,
such as 'UTF8' or -1. Other option text remains one raw OTHER option.
AlterDatabase.getAction() selects what is rendered: OPTIONS (the default),
RENAME, OWNER, SET_TABLESPACE, REFRESH_COLLATION_VERSION or
CONFIGURATION_PARAMETER. Payloads of the other actions are kept. SET and
RESET of a configuration parameter use ConfigurationParameterChange, which
AlterRole shares:
AlterDatabase alter = (AlterDatabase) CCJSqlParserUtil.parse(
"ALTER DATABASE app SET search_path TO app, public",
parser -> parser.withDialect(Dialect.POSTGRESQL));
ConfigurationParameterChange change = alter.getParameterChange();
change.getParameter(); // search_path
change.getValues(); // [app, public], a mutable ExpressionList
change.setValueKind(ConfigurationParameterChange.ValueKind.DEFAULT);
// ALTER DATABASE app SET search_path TO DEFAULT
getValueKind() is VALUES, DEFAULT or FROM_CURRENT; RESET ALL uses
the parameter name ALL. AlterRole keeps getParameter(), isUseEquals(),
isFromCurrent(), isUseDefault(), getValues() and their setters; they read
and edit getParameterChange(). FROM CURRENT and DEFAULT are now mutually
exclusive: setFromCurrent(false) and setUseDefault(false) return to the values.
Without the MySQL dialect, ALTER DATABASE forms that are neither MySQL nor
PostgreSQL syntax, such as ALTER DATABASE d SET RECOVERY SIMPLE, remain an
UnsupportedStatement; Dialect.MYSQL reports them as errors. ALTER SCHEMA
followed by MySQL options is an AlterDatabase in every dialect, while PostgreSQL’s
ALTER SCHEMA ... RENAME TO/OWNER TO remains an AlterSchema. Validation
reports the createDatabase and alterDatabase features, which the MySQL,
MariaDB and PostgreSQL capabilities enable.
CreateDatabase.getOptions() is now a live list that is never null, and
setOptions copies its argument. getDatabaseOptions() is an unmodifiable
token snapshot derived from the options, including WITH, and is null without
options. setDatabaseOptions replaces all options with one raw option, and
addDatabaseOptions appends one while keeping the typed options. Parsed
statements no longer end with an empty raw option.
References: MySQL CREATE DATABASE, ALTER DATABASE; PostgreSQL CREATE DATABASE, ALTER DATABASE.
Routine and trigger alterations¶
PostgreSQL ALTER FUNCTION, ALTER PROCEDURE and ALTER ROUTINE and MySQL
ALTER FUNCTION and ALTER PROCEDURE parse to AlterRoutine. getRoutineType()
names the keyword and getRoutine() is a RoutineReference with the optional
PostgreSQL argument types. getAction() is CHARACTERISTICS, RENAME, OWNER,
SET_SCHEMA or DEPENDS_ON_EXTENSION (isNoDependency() for NO DEPENDS);
payloads of the other actions are kept. getCharacteristics() is a mutable list of
RoutineCharacteristic in source order, shared by both dialects. Each Kind renders
its own keywords, such as IMMUTABLE, CALLED_ON_NULL_INPUT, PARALLEL_SAFE,
COST or MySQL’s READS_SQL_DATA. SECURITY_DEFINER and SECURITY_INVOKER keep
the [EXTERNAL] SECURITY or SQL SECURITY spelling in getSecurityClause().
COST, ROWS, SUPPORT and COMMENT keep their value as SQL text in getValue().
getComment() returns the COMMENT literal as a StringValue read with the dialect’s
escaping, so getNotExcapedValue() is the text MySQL stores. SET and RESET reuse
ConfigurationParameterChange:
AlterRoutine alter = (AlterRoutine) CCJSqlParserUtil.parse(
"ALTER FUNCTION f(int) IMMUTABLE SET search_path = public",
parser -> parser.withDialect(Dialect.POSTGRESQL));
alter.getRoutine().getArguments(); // [int]
alter.getCharacteristics().get(0).getKind(); // IMMUTABLE
alter.getCharacteristics().get(1).getParameterChange().getParameter(); // search_path
The characteristics of one statement belong to one dialect, and MySQL’s take no argument
list. Other forms after ALTER FUNCTION or ALTER PROCEDURE, such as Oracle’s
COMPILE or SQL Server routine definitions without the SQL Server dialect, remain an
UnsupportedStatement. PostgreSQL’s ALTER TRIGGER name ON table RENAME TO and
[NO] DEPENDS ON EXTENSION parse to AlterTrigger; table visitors report its table.
Validators report alterFunction, alterProcedure, alterRoutine and
alterTrigger; PostgreSQL enables all of them, MySQL and MariaDB the first two.
PostgreSQL’s ALTER RULE name ON table RENAME TO new_name
(ALTER RULE) parses to
AlterRule with getName(), getTable() and getNewName(); table visitors report
its table and validators report alterRule, which PostgreSQL enables. It is the only
ALTER RULE form, so any other text after ALTER RULE is a parse error.
PostgreSQL’s cast, transform and conversion definitions are typed statements:
CREATE CAST (source_type AS target_type) {WITH FUNCTION function [(argument_type [, ...])] | WITHOUT FUNCTION | WITH INOUT} [AS ASSIGNMENT | AS IMPLICIT](CREATE CAST) parses toCreateCast.getCast()is theCastSignaturethatDROP CASTuses,getMethod()isWITH_FUNCTION,WITHOUT_FUNCTIONorWITH_INOUT,getFunction()is theRoutineReferenceofWITH FUNCTION, andgetContext()isASSIGNMENT,IMPLICITornullfor a cast that applies only when written.CREATE [OR REPLACE] TRANSFORM FOR type_name LANGUAGE lang_name (FROM SQL WITH FUNCTION function, TO SQL WITH FUNCTION function)(CREATE TRANSFORM) parses toCreateTransform.getTransform()is theTransformSignaturethatDROP TRANSFORMuses.getFromSqlFunction()andgetToSqlFunction()areRoutineReferenceobjects, one of themnullwhen the statement has only the other item, andisToSqlFirst()keeps the order of the two items.CREATE [DEFAULT] CONVERSION name FOR 'source_encoding' TO 'dest_encoding' FROM function_name(CREATE CONVERSION) parses toCreateConversionwithisDefaultConversion(),getName(),getSourceEncoding()andgetDestinationEncoding()asStringValueandgetFunction().ALTER CONVERSION name {RENAME TO new_name | OWNER TO role | SET SCHEMA schema}(ALTER CONVERSION) parses toAlterConversionwithgetAction()andgetNewName(),getOwner()orgetSchemaName().
CreateCast cast = (CreateCast) CCJSqlParserUtil.parse(
"CREATE CAST (bigint AS int4) WITH FUNCTION int4(bigint) AS ASSIGNMENT");
cast.getCast().getSourceType(); // ColDataType bigint
cast.getMethod(); // CreateCast.Method.WITH_FUNCTION
cast.getFunction().getName(); // int4
cast.getContext(); // CreateCast.Context.ASSIGNMENT
CreateTransform transform = (CreateTransform) CCJSqlParserUtil.parse(
"CREATE TRANSFORM FOR hstore LANGUAGE plpython3u ("
+ "FROM SQL WITH FUNCTION hstore_to_plpython(internal), "
+ "TO SQL WITH FUNCTION plpython_to_hstore(internal))");
transform.getTransform().getLanguage(); // plpython3u
transform.getToSqlFunction().getName(); // plpython_to_hstore
Validators report createCast, createTransform, createConversion and
alterConversion, which PostgreSQL enables. With Dialect.POSTGRESQL these are the only
statements that CREATE CAST, CREATE TRANSFORM, CREATE [DEFAULT] CONVERSION and
ALTER CONVERSION start, so any other text after them is a parse error. In another dialect
and without one, a statement is read this way when it has PostgreSQL’s form
(CAST (...) WITH FUNCTION, WITH INOUT or WITHOUT FUNCTION,
TRANSFORM FOR type LANGUAGE, CONVERSION name FOR, and ALTER CONVERSION name
followed by RENAME, OWNER or SET); a statement of another product that starts with
the same words, such as CREATE CAST (a AS b) WITH SPECIFIC FUNCTION f, remains an
UnsupportedStatement.
CreateFunctionalStatement.getDefiner() returns the MySQL DEFINER of
CREATE DEFINER = ... FUNCTION|PROCEDURE as the MySqlDefiner that views and
triggers use. With Dialect.MYSQL or Dialect.MARIADB,
CreateFunctionalStatement.getCharacteristics() lists the characteristics between the
routine header and its body, such as COMMENT, DETERMINISTIC, READS SQL DATA or
SQL SECURITY INVOKER, with the same RoutineCharacteristic kinds; DETERMINISTIC
and NOT_DETERMINISTIC occur only here. getFunctionDeclarationParts() still holds
every token and renders the statement unchanged, so the list is read-only. Replacing the
tokens with setFunctionDeclarationParts(parts) clears it, while
setFunctionDeclarationParts(parts, characteristics) sets both, and appending tokens
keeps it. Without a MySQL dialect the list stays empty. MySQL CREATE TRIGGER accepts IF NOT EXISTS (isIfNotExists())
and keeps MariaDB’s OR REPLACE. A BEGIN ... END trigger body with only SQL
statements remains a Block; a compound body with procedural statements such as
IF, DECLARE or loops is an UnsupportedStatement with its tokens, captured with
the boundary of routine bodies. In both cases the semicolon after END separates the
next statement.
References: PostgreSQL ALTER FUNCTION, ALTER PROCEDURE, ALTER ROUTINE, ALTER TRIGGER; MySQL ALTER FUNCTION, ALTER PROCEDURE, CREATE TRIGGER.
MySQL table maintenance statements¶
CHECK TABLE, CHECKSUM TABLE, OPTIMIZE TABLE and REPAIR TABLE parse to
TableMaintenanceStatement in every dialect; ANALYZE TABLE remains Analyze.
getOperation() identifies the statement, getModifier() returns
NO_WRITE_TO_BINLOG or LOCAL for OPTIMIZE and REPAIR, and isTablesKeyword()
records the TABLES spelling. getOptions() keeps the trailing options in source
order. CHECK accepts FOR UPGRADE, QUICK, FAST, MEDIUM, EXTENDED and
CHANGED; CHECKSUM accepts one of QUICK and EXTENDED; REPAIR accepts QUICK,
EXTENDED and USE_FRM. Other combinations are rejected, and the option words and
CHECKSUM remain ordinary identifiers elsewhere. With unsupported statements enabled, these
statements are parsed rather than captured as unsupported statements.
TableMaintenanceStatement repair = (TableMaintenanceStatement) CCJSqlParserUtil.parse(
"REPAIR NO_WRITE_TO_BINLOG TABLE t1, t2 QUICK");
repair.getTables(); // [t1, t2]
repair.getOptions(); // [QUICK]
Table visitors and metadata validation use the table list. Validators report the
tableMaintenance feature, which the MySQL and MariaDB capabilities enable. Feature
analysis reports READS_DATA and RETURNS_RESULT_SET; OPTIMIZE and REPAIR also
report MODIFIES_SCHEMA, and REPAIR may report MODIFIES_DATA.
ANALYZE TABLE ... UPDATE HISTOGRAM ON keeps WITH n BUCKETS
(getHistogramBucketCount()), MySQL 8.4’s AUTO UPDATE or MANUAL UPDATE
(getHistogramUpdateMode()) and USING DATA 'json' (getHistogramData(), a
StringValue). USING DATA excludes the other two options, and DROP HISTOGRAM
takes none of them.
PostgreSQL maintenance statements¶
With Dialect.POSTGRESQL and without a dialect, PostgreSQL’s maintenance statements parse
to typed statements:
VACUUM [(option [value], ...)] [target, ...]andVACUUM [FULL] [FREEZE] [VERBOSE] [ANALYZE] [target, ...]parse toVacuumStatement.ANALYZE [(option [value], ...)] [target, ...]andANALYZE [VERBOSE] [target, ...]extendAnalyze, which keeps the MySQLANALYZE TABLEform and its histogram clause. A statement takes one form: options, or targets withONLY,*or columns, cannot be combined withNO_WRITE_TO_BINLOG,LOCAL,TABLEorUPDATE/DROP HISTOGRAM. The PostgreSQL form can be spelledANALYSE, whichgetCommand()keeps.CLUSTER [(option [value], ...)] [table [USING index]],CLUSTER [VERBOSE] [table [USING index]]and the pre-8.3CLUSTER [VERBOSE] index ON tableparse toClusterStatement;getIndexSyntax()tellsUSINGandONapart.CHECKPOINTparses toCheckpointStatement.
The three option lists share UtilityOption: getName() keeps the name as written, and
getValueType() tells whether the value is a Boolean word, a number, a string or another word.
getBooleanValue(), getLongValue(), getStringValue() and getEnumValue(Class) read
it, and getKind(Class) maps the name to VacuumStatement.OptionKind,
Analyze.OptionKind or ClusterStatement.OptionKind. The unparenthesized keywords are
options too: VACUUM FULL t holds the option FULL like VACUUM (FULL) t, and
isParenthesizedOptions() tells the forms apart. As in PostgreSQL’s grammar, an option list
takes any name and any Boolean word, number, string or word as its value, as long as an unquoted
name or word is not one that PostgreSQL reserves (ANALYZE and ANALYSE are names);
PostgreSQL checks the names and values when it runs the statement. getKind(Class) is null for a name the statement does not know,
such as VACUUM (FOO) t, and the typed readers are null for a value of another type, such as
PARALLEL 1.5. A double-quoted name keeps its case, as in PostgreSQL: "verbose" is
VERBOSE, and "VERBOSE" is an unknown option. EXPLAIN (...) reads its options
through the same production but keeps ExplainStatement.Option, and accepts only the names
and values it accepted before.
A MaintenanceTarget of VACUUM and ANALYZE is [ONLY] table [*] [(column, ...)]: its
getReference() is the TableReference of LOCK and TRUNCATE, and
getColumns() lists the columns. Analyze.getTargets() holds the tables of both forms;
getTable() and getTables() are derived from it, and getTables() now returns a
read-only snapshot.
VacuumStatement vacuum = (VacuumStatement) CCJSqlParserUtil.parse(
"VACUUM (PARALLEL 2, INDEX_CLEANUP OFF) t (a)",
parser -> parser.withDialect(Dialect.POSTGRESQL));
vacuum.getOptions().get(0).getLongValue(); // 2
vacuum.getOptions().get(1).getBooleanValue(); // false
vacuum.getTargets().get(0).getColumns(); // [a]
VACUUM, CLUSTER, FREEZE, CHECKPOINT and ANALYSE remain identifiers elsewhere. As in
PostgreSQL, FULL, FREEZE, VERBOSE and ANALYZE directly after VACUUM are
options, never table names. With Dialect.POSTGRESQL, ANALYZE verbose now means the
VERBOSE option; without a dialect it is still a table named verbose unless a table
name follows, and other dialects keep that reading. Without a dialect, a statement starting
with one of these words is a PostgreSQL statement only when its next tokens take the
PostgreSQL form, so other products’ variants such as VACUUM INTO 'file' remain
unsupported statements. Other dialects do not parse these statements, nor ANALYSE.
Table visitors report the tables of the targets and the CLUSTER table, but not the column
lists or the index. Validators report the analyze, vacuum, cluster and
checkpoint features, which the PostgreSQL capabilities enable; the MySQL and MariaDB
capabilities now enable analyze as well. Feature analysis reports MODIFIES_SCHEMA and
READS_DATA for VACUUM and CLUSTER, as for ANALYZE, and MODIFIES_SESSION for
CHECKPOINT, a server operation like ALTER SYSTEM CHECKPOINT.
MySQL table and backup locks
—————————-
MySQL and MariaDB LOCK TABLES
parses to the LockStatement of PostgreSQL and Oracle LOCK TABLE. Each entry of
getTargets() keeps its table, the alias as getTable().getAlias(), and
getLockType(): READ, READ_LOCAL, WRITE, LOW_PRIORITY_WRITE or MariaDB’s
WRITE_CONCURRENT. MySQL 8.4 has only READ [LOCAL] and WRITE;
LOW_PRIORITY WRITE is accepted because MySQL 8.0 (deprecated, without effect)
and MariaDB accept it. The lock type is null for a PostgreSQL or Oracle target, and
isTablesKeyword() records the TABLES spelling. MariaDB’s trailing WAIT n and
NOWAIT use getWaitSeconds() and isNoWait(), like PostgreSQL and Oracle.
UNLOCK TABLE and UNLOCK TABLES parse to UnlockStatement, and
LOCK INSTANCE FOR BACKUP and UNLOCK INSTANCE to
BackupLockStatement, whose getAction() is LOCK or UNLOCK.
LockStatement lock = (LockStatement) CCJSqlParserUtil.parse(
"LOCK TABLES t AS a READ LOCAL, u WRITE");
lock.getTargets().get(0).getTable().getAlias().getName(); // a
lock.getTargets().get(0).getLockType(); // READ_LOCAL
The MySQL and MariaDB dialects read these forms, and so does the parser without a dialect:
an alias or a lock type after the first table name tells MySQL’s form from PostgreSQL’s
LOCK [TABLE] name, so LOCK tables NOWAIT still locks a table named tables.
WRITE CONCURRENT, WAIT and NOWAIT after a MySQL target list require
Dialect.MARIADB, and Dialect.MARIADB rejects the backup lock statements, which
MariaDB does not have. PostgreSQL and the other dialects keep rejecting the MySQL forms.
UNLOCK, INSTANCE and BACKUP remain ordinary identifiers.
Table visitors report the locked tables but not their aliases. Validators report the
lock, unlockTables and backupLock features; the MySQL capabilities enable all
three, MariaDB the first two, and PostgreSQL and Oracle lock. Feature analysis reports
MODIFIES_TRANSACTION.
MySQL servers, plugins and components¶
MySQL CREATE SERVER and
ALTER SERVER parse to the
CreateServer and AlterServer of the PostgreSQL statements. A ForeignDataOption
keeps its value in getValueExpression(): a StringValue, or for MySQL’s PORT the
number as written (LongValue, DoubleValue or HexValue). getValue() still
returns the string value and is null for a number. A server or wrapper name written as a
string literal, as in FOREIGN DATA WRAPPER 'mysql', is kept with its quotes.
CreateServer server = (CreateServer) CCJSqlParserUtil.parse(
"CREATE SERVER s FOREIGN DATA WRAPPER mysql OPTIONS (HOST 'h', PORT 3306)");
ForeignDataOption port = server.getOptions().get(1);
port.getValueExpression(); // LongValue 3306
port.getValue(); // null
Numeric values and quoted names are read with the MySQL and MariaDB dialects and without a
dialect, and only in these two statements; PostgreSQL and the other option lists keep
requiring string constants. Dialect.MYSQL follows the server grammar: the options are
USER, HOST, DATABASE, OWNER, PASSWORD and SOCKET with a string and
PORT with a number, OPTIONS is required, and the PostgreSQL clauses (IF NOT
EXISTS, TYPE, VERSION, ADD/SET/DROP options, RENAME TO,
OWNER TO) are rejected. DROP SERVER stays a Drop.
INSTALL PLUGIN,
UNINSTALL PLUGIN,
INSTALL COMPONENT and
UNINSTALL COMPONENT
parse to one PluginStatement with an Action (INSTALL, UNINSTALL) and an
ObjectType (PLUGIN, COMPONENT). A plugin statement has getPluginName() and,
for INSTALL, the getLibrary() after SONAME. A component statement lists its URNs
in getComponents(), and INSTALL COMPONENT ... SET its assignments in
getVariables(): each ComponentVariable has a SetScope (NONE, GLOBAL or
PERSIST), the name as written, the operator (= or :=) and the value expression;
the word ON is read as a column. As in MySQL, a value is not DEFAULT or a subquery.
PluginStatement install = (PluginStatement) CCJSqlParserUtil.parse(
"INSTALL COMPONENT 'file://component1' SET GLOBAL component1.var1 = 12 + 3",
parser -> parser.withDialect(Dialect.MYSQL));
ComponentVariable variable = install.getVariables().get(0);
// GLOBAL, component1.var1, 12 + 3
String parts = variable.getScope() + ", " + variable.getName() + ", " + variable.getValue();
The statements are read with the MySQL and MariaDB dialects and without a dialect; MariaDB
has no components. With the MySQL and MariaDB dialects every INSTALL is this statement.
Without a dialect DuckDB’s INSTALL extension keeps producing an ExtensionStatement,
also for extensions named plugin or component: SONAME after the plugin name or a
component URN selects the MySQL statement. UNINSTALL, PLUGIN, COMPONENT and
SONAME remain ordinary identifiers. The validator checks installPlugin (MySQL and
MariaDB) and installComponent (MySQL), and createServer and alterServer are now
part of the MySQL and MariaDB capabilities. Feature analysis reports MODIFIES_SESSION.
PostgreSQL session statements and COPY options¶
With Dialect.POSTGRESQL and without a dialect, PostgreSQL’s session statements parse to
typed statements:
DISCARD
{ALL | PLANS | SEQUENCES | TEMP | TEMPORARY}parses toDiscardStatement.getTarget()keeps the target as written;TEMPandTEMPORARYname the same state.LISTEN
channeland UNLISTEN{channel | *}parse toListenStatement, next toNotifyStatement.getAction()isLISTENorUNLISTEN, andgetChannel()is null forUNLISTEN *(isAllChannels()).SHOW
TIME ZONE,SHOW TRANSACTION ISOLATION LEVELandSHOW SESSION AUTHORIZATIONparse to theShowStatementof otherSHOW namestatements instead ofUnsupportedStatement, and RESET takes the same names.SET,RESETandSHOWread them through one rule:getName()isTime Zone, as before forSETandRESET,TRANSACTION ISOLATION LEVELorSESSION AUTHORIZATION.
COPY also accepts the option syntax that
PostgreSQL used before 9.0 and still accepts: keywords without parentheses or commas after the file
name and the optional WITH, in any order (BINARY, FREEZE, DELIMITER [AS] 'x',
NULL [AS] 'x', CSV, HEADER, QUOTE [AS] 'x', ESCAPE [AS] 'x',
FORCE QUOTE {column, ... | *}, FORCE NOT NULL {column, ... | *},
FORCE NULL {column, ... | *} and ENCODING 'x'), and COPY BINARY table.
CopyStatement.getLegacyOptions() holds a CopyLegacyOption per option, in source order,
with its Kind, its string (getValue()) and whether AS preceded it, or the columns of a
FORCE option (isAllColumns() for *). isBinary() records COPY BINARY. The
options in parentheses keep their reading through getOptions(); a statement takes one of
the two forms.
CopyStatement copy = (CopyStatement) CCJSqlParserUtil.parse(
"COPY t TO STDOUT WITH CSV HEADER FORCE QUOTE a, b");
copy.getLegacyOptions().get(0).getKind(); // CSV
copy.getLegacyOptions().get(2).getColumns(); // [a, b]
Other dialects keep rejecting these forms, and SHOW TIME ZONE remains an
UnsupportedStatement there. LISTEN and UNLISTEN become non-reserved keywords and,
like DISCARD, remain usable as names. As in PostgreSQL’s grammar, a channel or a FORCE
column cannot be an unquoted word that PostgreSQL reserves, such as ALL.
Table visitors report the copied table and no table for the session statements. Validators
report the discard and listen features, which the PostgreSQL capabilities enable
together with show. Feature analysis reports MODIFIES_SESSION for DISCARD,
LISTEN and UNLISTEN, and a possible MODIFIES_SCHEMA for DISCARD ALL, TEMP
and TEMPORARY, which drop the temporary tables of the session.
MySQL server administration statements¶
With Dialect.MYSQL and without a dialect, these MySQL 8.4 statements parse to typed
statements:
FLUSH parses to
FlushStatement.getModifier()returnsNO_WRITE_TO_BINLOGorLOCAL, theTableMaintenanceStatement.Modifierof OPTIMIZE, REPAIR and the partition operations.getOptions()keeps the flush options in source order. Each is aFlushOptionwhosegetKind()is one of MySQL 8.4’s options, fromBINARY_LOGStoUSER_RESOURCES, and whosegetChannel()holds the string literal ofRELAY LOGS FOR CHANNEL 'name'. Without options the statement isFLUSH {TABLE | TABLES} [tbl_name, ...] [WITH READ LOCK | FOR EXPORT](isFlushTables()), withgetTables(),isTablesKeyword()andgetTableLock(). As in MySQL, that form takes no flush option,FOR EXPORTrequires a table list, onlyRELAY LOGSnames a channel, andFLUSH HOSTS, which MySQL 8.4 removed, is rejected; with unsupported statements enabled it remains anUnsupportedStatement.KILL [CONNECTION | QUERY] processlist_id parses to
KillStatement.getKind()isCONNECTION,QUERYor null when neither is given, andgetProcesslistId()is an expression, since MySQL accepts@idor1 + 2there.RESET PERSIST [[IF EXISTS] system_var_name] extends
ResetStatementwithisPersist()andisIfExists();getName()is the variable as written, with a component orDEFAULTprefix such asvalidate_password.length, or null when the statement names none. Without a dialect only a variable name orIF EXISTSafterPERSISTselects this form, so a bareRESET PERSISTstill resets a parameter namedpersist, as it does in the other dialects.HANDLER parses to
HandlerStatement, whosegetAction()isOPEN,READorCLOSE.getTable()is the table that OPEN opens, with the handler name as its alias, or the unqualified handler name that READ and CLOSE take;getHandlerName()returns the name in both cases. A READ hasgetIndexName()and eithergetKeyComparison()(=,<=,>=,<or>) withgetKeyValues(), orgetDirection()(FIRST,NEXT,PREVorLAST, and onlyFIRSTorNEXTwithout an index), followed bygetWhere(),getLimit()andgetOffset(). As in MySQL, a READ takes no subquery.
FlushStatement flush = (FlushStatement) CCJSqlParserUtil.parse(
"FLUSH LOCAL TABLES t1, t2 WITH READ LOCK");
flush.getTables(); // [t1, t2]
flush.getTableLock(); // WITH_READ_LOCK
HandlerStatement read = (HandlerStatement) CCJSqlParserUtil.parse(
"HANDLER h READ i1 >= (1, 2) WHERE a > 0 LIMIT 3");
read.getKeyComparison(); // GREATER_THAN_OR_EQUALS
read.getKeyValues(); // (1, 2)
Other dialects keep rejecting these statements. HANDLER becomes a non-reserved keyword
and remains usable as a name, like FLUSH, KILL and the option words.
Table visitors report the tables of FLUSH TABLES and the table that HANDLER ... OPEN
opens, but not the handler name of READ and CLOSE, which may be an alias. They find the
tables of subqueries in the KILL expression, and they now return no table for every
RESET statement instead of throwing. Validators report the
flush, kill, handler and resetPersist features, which the MySQL capabilities
enable; PostgreSQL’s RESET keeps reset. Feature analysis reports
MODIFIES_SESSION for FLUSH, KILL and HANDLER OPEN and CLOSE, adds
MODIFIES_TRANSACTION for the table locks of FLUSH TABLES, and reports READS_DATA and
RETURNS_RESULT_SET for HANDLER READ.
PostgreSQL IMPORT FOREIGN SCHEMA¶
With Dialect.POSTGRESQL and without a dialect,
IMPORT FOREIGN SCHEMA
remote_schema [{LIMIT TO | EXCEPT} (table [, ...])] FROM SERVER server INTO local_schema
[OPTIONS (option 'value' [, ...])] parses to ImportForeignSchema, a
ForeignDataStatement like CreateServer and CreateUserMapping. getRemoteSchema(),
getServer() and getLocalSchema() hold the names. getQualification() is LIMIT_TO,
EXCEPT or null when the whole schema is imported, and getTables() lists the tables as
TableReference values, the relation targets of LOCK and TRUNCATE, because
PostgreSQL’s grammar reads the same relation syntax there. setTables(qualification, tables)
changes both together. getOptions() holds the ForeignDataOption values that the other
foreign-data statements use; ADD, SET and DROP are rejected as in their CREATE
forms.
ImportForeignSchema imported = (ImportForeignSchema) CCJSqlParserUtil.parse(
"IMPORT FOREIGN SCHEMA remote LIMIT TO (t1, t2) FROM SERVER srv INTO local "
+ "OPTIONS (import_default 'true')");
imported.getQualification(); // LIMIT_TO
imported.getTables(); // [t1, t2]
imported.getOptions().get(0).getName(); // import_default
Other dialects keep rejecting the statement, and the EXASOL dialect keeps IMPORT for its
own statement. Table visitors report no table: the listed names belong to the remote schema.
Validators report the importForeignSchema feature, which the PostgreSQL capabilities and
FeaturesAllowed.DDL enable. Feature analysis reports MODIFIES_SCHEMA.
MySQL replication statements
—————————-
The MySQL 8.4 replication statements parse with Dialect.MYSQL and without a dialect, into
the classes of net.sf.jsqlparser.statement.replication:
CHANGE REPLICATION SOURCE TO is a
ChangeReplicationSourceStatement. EachReplicationSourceOptionkeeps its spelling andgetKind(), andKind.getValueType()says what the option takes: a string, a number,NULL,0or1, a keyword such asOFF(held as aColumn, likeONin PostgreSQL publication options), or theParenthesedExpressionListofIGNORE_SERVER_IDS. A value of another type is a parse error, as in MySQL.PRIVILEGE_CHECKS_USERkeeps its account ingetAccount(), andgetPrimaryKeyCheck()andgetGtidAssignment()read the keyword options.CHANGE REPLICATION FILTER is a
ChangeReplicationFilterStatementofReplicationFilterrules. A rule’s list, which may be empty, holds database names,db.tablenames, quoted patterns or(from, to)pairs, asgetDatabases(),getTables(),getPatterns()andgetRewrites().START REPLICA and STOP REPLICA are
StartReplicaStatementandStopReplicaStatement, withgetThreads()(IO_THREAD,SQL_THREAD). START keeps itsUNTILconditions asReplicaUntilOptionand theUSER,PASSWORD,DEFAULT_AUTHandPLUGIN_DIRstrings.RESET BINARY LOGS AND GTIDS [TO n] and RESET REPLICA [ALL], also as the comma list of the RESET statement, are a
ResetReplicationStatementofResetReplicationStatement.Optionentries.PURGE BINARY LOGS extends Oracle’s
PurgeStatement:getPurgeObjectType()isBINARY_LOGS,getLogBound()isTOorBEFORE, andgetLogExpression()is the log name literal or the point in time. As in MySQL, theBEFOREexpression takes no subquery at any depth.Dialect.MARIADBreads it as well, and MariaDB’sPURGE MASTER LOGSasMASTER_LOGS.
FOR CHANNEL 'name' takes a string, which getChannel() returns as a StringValue.
ChangeReplicationSourceStatement change = (ChangeReplicationSourceStatement)
CCJSqlParserUtil.parse("CHANGE REPLICATION SOURCE TO SOURCE_HOST = 'h', "
+ "IGNORE_SERVER_IDS = (1, 2) FOR CHANNEL 'c1'");
change.getOptions().get(0).getKind(); // SOURCE_HOST
change.getChannel().getValue(); // c1
MySQL 8.4 removed CHANGE MASTER TO, START SLAVE, RESET MASTER and the MASTER_
options, so they stay unsupported. Without a dialect a bare RESET REPLICA remains the
PostgreSQL ResetStatement of a parameter named replica; Dialect.MYSQL, or ALL,
FOR CHANNEL or a further option after it, makes it MySQL’s. PostgreSQL, Oracle and MariaDB
keep their readings of RESET, PURGE and START. STOP becomes a non-reserved
keyword and remains usable as a name.
Table visitors report the tables of REPLICATE_DO_TABLE and REPLICATE_IGNORE_TABLE.
Validators report the changeReplicationSource, changeReplicationFilter,
startReplica, stopReplica, resetReplication and purgeBinaryLogs features; the
MySQL capabilities enable all of them and MariaDB purgeBinaryLogs. Feature analysis reports
MODIFIES_SESSION, as for ALTER SYSTEM.
MySQL LOAD DATA and LOAD XML¶
MySQL and MariaDB LOAD DATA and
LOAD XML parse to
MySqlLoadDataStatement; BigQuery’s LOAD DATA {INTO | OVERWRITE} remains
LoadDataStatement. getFormat() is DATA or XML. As in the server’s grammar,
both forms take every clause: getLockOption() (LOW_PRIORITY or CONCURRENT),
isLocal(), getFileName(), getDuplicateHandling() (REPLACE or IGNORE, the
enum of CREATE TABLE … SELECT), getTable(), getPartitions(),
getCharacterSet() with isUseCharsetKeyword() for the CHARSET spelling,
getRowsIdentifiedBy(), getFileFormat(), getIgnoreCount() with getIgnoreUnit()
(LINES or ROWS), getColumns() (columns and UserVariable targets, empty for
()) and getUpdateSets() for the SET clause.
MySqlLoadDataStatement load = (MySqlLoadDataStatement) CCJSqlParserUtil.parse(
"LOAD DATA LOCAL INFILE 'a.csv' INTO TABLE t FIELDS TERMINATED BY ',' "
+ "IGNORE 1 LINES (a, @b) SET c = @b + 1");
load.getFileFormat().getFieldsTerminatedBy().getValue(); // ,
load.getUpdateSets().get(0).getValue(0); // @b + 1
MySqlFileFormat holds the {FIELDS | COLUMNS} and LINES clauses, which
SELECT … INTO OUTFILE shares: MySqlSelectIntoClause.getFileFormat() returns it, and the
clause’s earlier getters and setters, such as getFieldsTerminatedBy(), read and write it.
Each clause needs at least one subclause, so a bare FIELDS or LINES, which MySQL
rejects, is now a parse error. The subclauses may come in any order and repeat, as MySQL
allows; like the server, the format keeps the last value of each and prints them in the
documented order. INTO OUTFILE also takes CHARSET and a quoted character set name.
The MySQL and MariaDB dialects read these forms, and so does the parser without a dialect:
INFILE after LOAD DATA or LOAD XML and the optional LOW_PRIORITY,
CONCURRENT and LOCAL tells them from BigQuery’s LOAD DATA INTO and from DuckDB’s
and PostgreSQL’s LOAD name, so LOAD xml still loads an extension named xml. The
other dialects keep rejecting the MySQL forms. INFILE and CONCURRENT remain ordinary
identifiers.
Table visitors report the target table and the tables of subqueries in the SET clause.
Validators report the loadData feature, which the MySQL and MariaDB capabilities now
enable. Feature analysis reports MODIFIES_DATA.
PostgreSQL cursors¶
PostgreSQL’s cursor statements parse to the net.sf.jsqlparser.statement.cursor package:
DECLARE name [BINARY] [ASENSITIVE | INSENSITIVE] [[NO] SCROLL] CURSOR [{WITH | WITHOUT} HOLD] FOR query parses to
DeclareCursorStatement.getOptions()lists theCursorOptionvalues in their order; as in PostgreSQL’s grammar they may come in any order and repeat, and PostgreSQL rejects contradicting ones such asSCROLL NO SCROLLwhen it runs the statement.getHoldability()isWITH_HOLD,WITHOUT_HOLDor null, andgetQuery()is theSelect: a SELECT,VALUES,TABLE tor a query withWITH.FETCH and MOVE
[direction] [FROM | IN] cursorparse toFetchCursorStatement, whosegetKind()isFETCHorMOVE. The direction is an optionalFetchDirectionkeyword (NEXT,PRIOR,FIRST,LAST,ABSOLUTE,RELATIVE,FORWARD,BACKWARD) withgetCount()orisAll():ABSOLUTEandRELATIVEtake a count,FORWARDandBACKWARDan optional count orALL, and a count orALLcan stand alone. A count is a signed 32-bit integer, as in PostgreSQL.getFromKeyword()isFROM,INor null.CLOSE {name | ALL} parses to
CloseCursorStatement;isAll()is true when it has no cursor name.
FetchCursorStatement fetch = (FetchCursorStatement) CCJSqlParserUtil.parse(
"FETCH ABSOLUTE -1 FROM c", parser -> parser.withDialect(Dialect.POSTGRESQL));
fetch.getDirection(); // ABSOLUTE
fetch.getCount(); // -1
fetch.getCursorName(); // c
A cursor name is any name PostgreSQL takes as a column identifier, so its reserved words and
type or function name keywords such as table or binary are rejected. As in PostgreSQL,
a direction word that ends the statement names the cursor: FETCH next fetches from a
cursor named next.
Dialect.POSTGRESQL reads FETCH, MOVE, CLOSE and DECLARE followed by a
name as cursor statements. Without a dialect they parse when they take the PostgreSQL form:
DECLARE needs a name, the cursor options and CURSOR, and FETCH, MOVE and
CLOSE must end at a semicolon or the end of the input, so other products’ variants such
as FETCH NEXT FROM c INTO @x or CLOSE GLOBAL c remain unsupported statements. Other
dialects keep their readings, and DECLARE @variable still parses to the SQL Server
DeclareStatement in every dialect that read it before. MOVE becomes a non-reserved
keyword; it remains usable as a name, as do CLOSE, CURSOR and the option and
direction words. SELECT ... FETCH FIRST is unchanged.
Table visitors report the tables of the query of DECLARE and none for the other
statements, whose cursor name is not a table. Validators report the declareCursor,
fetchCursor (FETCH and MOVE) and closeCursor features, which the PostgreSQL
capabilities enable, and validate the query of DECLARE like a SELECT. Feature analysis
reports MODIFIES_SESSION for all of them, READS_DATA for a DECLARE whose query
reads a table, RETURNS_RESULT_SET for FETCH, and a possible READS_DATA for
FETCH and MOVE, which run the query of the cursor.
MySQL account management statements¶
CREATE USER and ALTER USER share UserOptions (getOptions()) in MySQL’s
clause order: the DEFAULT ROLE roles, the REQUIRE options (TlsOption), the
WITH resource limits (ResourceLimit), the password and locking options
(PasswordLockOption) in source order, and COMMENT or ATTRIBUTE. Omitted
clauses stay omitted. UserAuthentication adds ALTER USER’s REPLACE 'current'
(getCurrentCredential()), RETAIN CURRENT PASSWORD and the
DISCARD_OLD_PASSWORD mode.
AlterUser alter = (AlterUser) CCJSqlParserUtil.parse(
"ALTER USER 'app'@'%' IDENTIFIED BY 'new' RETAIN CURRENT PASSWORD "
+ "PASSWORD EXPIRE INTERVAL 90 DAY ACCOUNT LOCK");
alter.getAccounts().get(0).getAuthentication().isRetainCurrentPassword(); // true
alter.getOptions().getPasswordLockOptions(); // [PASSWORD EXPIRE INTERVAL 90 DAY, ACCOUNT LOCK]
MySQL multi-factor authentication is part of the same model. CREATE USER keeps the
second and third factor of IDENTIFIED ... AND IDENTIFIED ... [AND IDENTIFIED ...] in
UserAccount.getAdditionalAuthentications(), and the temporary first factor of a
passwordless plugin, IDENTIFIED WITH plugin INITIAL AUTHENTICATION IDENTIFIED ..., in
UserAuthentication.getInitialAuthentication(). ALTER USER keeps
{ADD | MODIFY} n FACTOR IDENTIFIED ..., DROP n FACTOR and the registration steps
n FACTOR INITIATE REGISTRATION, n FACTOR FINISH REGISTRATION SET CHALLENGE_RESPONSE AS
'...' and n FACTOR UNREGISTER as UserFactorOperation objects (operation, factor
number, authentication or challenge response) in UserAccount.getFactorOperations(). As
on the server, an account takes one or two changes of the same kind, and the factor number
is checked only when the statement runs.
AlterUser factors = (AlterUser) CCJSqlParserUtil.parse(
"ALTER USER app ADD 2 FACTOR IDENTIFIED WITH authentication_ldap_sasl AS 'uid=app' "
+ "ADD 3 FACTOR IDENTIFIED WITH authentication_webauthn");
UserFactorOperation second = factors.getAccounts().get(0).getFactorOperations().get(0);
second.getOperation(); // ADD
second.getFactor(); // 2
second.getAuthentication().getPlugin(); // authentication_ldap_sasl
ALTER USER USER() ... keeps USER() as the unquoted account name, as
DEFINER = CURRENT_USER does; isUserFunction() identifies it.
ALTER USER acct DEFAULT ROLE {NONE | ALL | role, ...} stores a RoleSelection in
getOptions().getDefaultRoles(). PostgreSQL’s ALTER USER, an alias of ALTER ROLE
(WITH LOGIN, PASSWORD 'x', SET, RENAME TO …), still parses to
AlterRole. MySQL syntax is recognized by IF EXISTS, USER(), quoted names,
hosts, account lists, a missing action or a MySQL account clause.
RENAME USER parses to RenameUser with ordered from/to pairs. MySQL
CREATE ROLE [IF NOT EXISTS] role, ... with quoted names, hosts or several roles keeps
them in CreateRole.getRoles(), and getName() is null. A single unquoted name, which
both databases accept, keeps the PostgreSQL getName(); so does an unquoted r@h, which
the lexer reads as one identifier, unless the MySQL or MariaDB dialect is selected. Role names
use the shared account spelling and are UserAccount values without authentication.
SET DEFAULT ROLE (MySQL’s TO, MariaDB’s FOR) and SET PASSWORD FOR parse to
SetDefaultRoleStatement and SetPasswordStatement in every dialect. SET ROLE and
SET PASSWORD without FOR share the variable assignment shape that PostgreSQL uses
for SET ROLE name and SET ROLE NONE, so they remain a SetStatement unless
Dialect.MYSQL or Dialect.MARIADB is selected. With these dialects every form parses
to SetRoleStatement (DEFAULT, NONE, ALL [EXCEPT ...] or a role list) or
SetPasswordStatement (= password or TO RANDOM, then REPLACE and
RETAIN CURRENT PASSWORD). The MySQL-only SET ROLE ALL EXCEPT ... and role names
with a quoted user or host, such as 'r'@'%', parse to SetRoleStatement in every
dialect.
SetRoleStatement setRole = (SetRoleStatement) CCJSqlParserUtil.parse(
"SET ROLE ALL EXCEPT 'admin'@'%'",
parser -> parser.withDialect(Dialect.MYSQL));
setRole.getRoles().getKind(); // ALL_EXCEPT
setRole.getRoles().getRoles(); // ['admin'@'%']
Validators report alterUser, renameUser, setRole, setDefaultRole and
setPassword; the MySQL and MariaDB capabilities enable them and createRole.
Feature analysis reports MODIFIES_SCHEMA, or MODIFIES_SESSION for SET ROLE.
Multi-factor authentication clauses are not modeled. Serialized account SQL can contain
passwords; avoid logging real credentials.
Legacy MySQL GROUP BY ordering¶
MySQL before 8.0.13 accepted ASC and DESC on individual GROUP BY items.
Select the existing MYSQL dialect and explicitly enable this legacy syntax:
Statement statement = CCJSqlParserUtil.parse(
"SELECT a FROM t GROUP BY a DESC",
parser -> parser.withDialect(Dialect.MYSQL).withLegacyMySqlGroupBy(true));
The option is disabled by default and does not enable this syntax in other dialects.
GroupByElement keeps its existing expression list; getGroupBySortDirection(index)
returns each explicit direction, or null when omitted. Directions follow list positions;
replacing the expression list clears them. Validators report the separate
selectGroupByOrdering feature, which is not enabled in the MySQL 8.0 capability.
PostgreSQL COMMENT targets¶
COMMENT ON supports these PostgreSQL object kinds in addition to the existing
TABLE, COLUMN and VIEW forms:
INDEX,SCHEMA,SEQUENCE,DOMAIN,TYPE,MATERIALIZED VIEW,FOREIGN TABLE,COLLATION,STATISTICS,CONVERSIONandTEXT SEARCH {CONFIGURATION | DICTIONARY | PARSER | TEMPLATE}ACCESS METHOD,DATABASE,EVENT TRIGGER,EXTENSION,FOREIGN DATA WRAPPER,[PROCEDURAL] LANGUAGE,PUBLICATION,ROLE,SERVER,SUBSCRIPTIONandTABLESPACEFUNCTION,PROCEDURE,ROUTINEandAGGREGATEsignaturesCONSTRAINT,TRIGGER,POLICYandRULEwith their owning tableCAST (source_type AS target_type)OPERATOR [schema.]op (left_type, right_type), whereNONEmarks the missing operand of a unary operatorOPERATOR CLASSandOPERATOR FAMILYwithUSING access_methodLARGE OBJECT oidandTRANSFORM FOR type_name LANGUAGE lang_name
These unambiguous target forms also parse without a dialect preset. Kind words such as
ROLE, SERVER, RULE, OPERATOR or CONVERSION are not reserved and
remain ordinary identifiers.
Tagged dollar strings still require Dialect.POSTGRESQL or
withDollarQuotedStringTags(true).
The additional targets are exposed through Comment.getTarget() as a
CommentTarget. Its Kind identifies the object, and getName() preserves
the individual identifier components. Table is used as the name container;
an index or type name does not thereby represent a table dependency.
isProcedural() records PROCEDURAL LANGUAGE.
The original Comment.getTable(), getColumn() and getView() accessors
continue to describe their respective existing forms.
COMMENT ON FUNCTION app.f(IN value integer) IS 'description' uses the shared
RoutineReference in getTarget().getRoutine(); Kind.isRoutine() identifies
these kinds and getName() is null. Argument mode, optional name
and ColDataType are preserved. An omitted signature has null arguments;
f() has an empty argument list. The signature identifies a routine and is not
a function call. An AGGREGATE requires a signature and also accepts (*) and
ordered-set signatures such as (float8 ORDER BY integer) or (ORDER BY integer).
For COMMENT ON CONSTRAINT ck ON app.t IS NULL, getName() is the constraint
name and getRelation() is its owning table. TRIGGER, POLICY and RULE
targets use the same fields; Kind.isOwnedByRelation() identifies these kinds.
Only a constraint accepts ON DOMAIN app.d, which sets isOnDomain() and stores
the domain as the owner instead. A CAST target exposes getSourceType() and
getTargetType(). NULL removes the comment and remains represented by a null
Comment.getComment().
COMMENT ON OPERATOR app.-> (jsonb, NONE) reads the operator with the reference
shared by exclusion constraints and ALTER EXTENSION: getOperator() returns an
ExclusionOperator with getSchemaName() and the operator symbol in getName().
getLeftOperandType() and getRightOperandType() hold the operand types, and null
stands for NONE; at least one operand type is required. The lexer reads some
PostgreSQL operators, such as <<= or -|-, as several tokens. Parts that touch
each other are joined into one operator, while whitespace or a comment separates two
operators as in PostgreSQL. Exclusion constraints and ALTER EXTENSION accept the same
operators. Operators that the lexer reads as part of an identifier, such as ##
without Dialect.POSTGRESQL, are not supported.
OPERATOR CLASS and OPERATOR FAMILY targets keep their name in getName()
and the index access method in getAccessMethod(); Kind.isOwnedByAccessMethod()
identifies these kinds. LARGE OBJECT exposes getLargeObjectOid(), which
accepts an unsigned 32-bit integer. TRANSFORM exposes getTransformType() and
getLanguage(). Each TEXT SEARCH object type has its own Kind, for example
TEXT_SEARCH_DICTIONARY.
Table discovery visits tables, column owners, views, materialized views, foreign
tables and the tables owning constraints, triggers, policies and rules. It does not
infer an index’s table or treat domains, sequences, types, routines or other catalog
objects as tables. Statement visitors visit comment literals, and custom SQL deparsers
can replace the explicit relation or literal. Feature analysis reports a schema
modification. Validation exposes a separate commentOn... capability for each
additional target kind.
Current date/time expression metadata¶
TemporalExpressionInfo.from(expression, dialect) provides a common read-only
view of MySQL and PostgreSQL current-date/time expressions. It recognizes the
existing TimeKeyExpression, Function and Column representations without
replacing nodes or changing their SQL rendering:
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
"SELECT CURRENT_TIMESTAMP(6)", p -> p.withDialect(Dialect.POSTGRESQL));
TemporalExpressionInfo info = TemporalExpressionInfo.from(
select.getSelectItem(0).getExpression(), Dialect.POSTGRESQL).orElseThrow();
// info.getKind() == TemporalExpressionInfo.Kind.CURRENT_TIMESTAMP
// info.getPrecision() == 6
Import TemporalExpressionInfo from net.sf.jsqlparser.util. Precision is
null when omitted, and explicit zero is preserved. getName() retains the
original keyword or function name; hasParentheses() distinguishes bare and
call forms. Results are snapshots: call from again after editing an AST.
The dialect is significant. MySQL LOCALTIME and LOCALTIMESTAMP are aliases
of CURRENT_TIMESTAMP; PostgreSQL exposes LOCAL_TIME and
LOCAL_TIMESTAMP separately. MySQL NOW, CURDATE and CURTIME aliases
are recognized in their function-call forms. PostgreSQL now() is recognized
without precision arguments. Quoted or qualified identifiers, unrelated
expressions and unsupported dialects return Optional.empty().
This API identifies expression metadata rather than performing database validation. Precision reports the requested value, without applying defaults, server range checks or clamping. For example, PostgreSQL accepts precision 7 with a warning and clamps it to 6, whereas MySQL rejects it.
With Dialect.POSTGRESQL, ALTER TABLE ... ADD UNIQUE USING INDEX and
ADD PRIMARY KEY USING INDEX expose a ConstraintUsingIndex through
AlterExpression.getConstraint(). Its getName() is the optional new constraint
name, while getExistingIndexName() identifies the existing index. This is
separate from an index declaration’s name, columns and access method.
Alter alter = (Alter) CCJSqlParserUtil.parse(
"ALTER TABLE t ADD CONSTRAINT uq UNIQUE USING INDEX i",
parser -> parser.withDialect(Dialect.POSTGRESQL));
ConstraintUsingIndex constraint = (ConstraintUsingIndex)
alter.getAlterExpressions().get(0).getConstraint();
constraint.setExistingIndexName("replacement_index");
constraint.setName("replacement_constraint");
getConstraintAttributes() exposes deferrability and initial timing when
present. The same AST can be constructed with new ConstraintUsingIndex()
and its fluent withName, withType and withExistingIndexName methods.
Transaction statements¶
START TRANSACTION produces a StartTransaction with an ordered list of typed
TransactionMode values. PostgreSQL isolation levels, READ ONLY / READ WRITE, and
[NOT] DEFERRABLE are supported, as is MySQL WITH CONSISTENT SNAPSHOT.
PostgreSQL accepts mode lists with or without commas; output uses commas.
The explicit MySQL dialect rejects a transaction containing both READ ONLY and
READ WRITE, while retaining repeated occurrences of the same mode.
Choose the dialect explicitly to parse BEGIN as a transaction command:
StartTransaction transaction = (StartTransaction) CCJSqlParserUtil.parse(
"BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY",
parser -> parser.withDialect(Dialect.POSTGRESQL));
transaction.getModes().set(1, TransactionMode.READ_WRITE);
// BEGIN ISOLATION LEVEL SERIALIZABLE, READ WRITE
String sql = transaction.toString();
Without a dialect, BEGIN retains the existing procedural block interpretation.
MySQL and PostgreSQL dialects distinguish BEGIN transactions from
BEGIN SELECT ...; END blocks by the following tokens. Oracle blocks and opaque
routine bodies retain their existing handling.
Commit and RollbackStatement share TransactionCompletionStatement, which
exposes the optional TransactionKeyword (WORK or PostgreSQL TRANSACTION),
TransactionChain (CHAIN or NO_CHAIN) and MySQL TransactionRelease
(RELEASE or NO_RELEASE). Null fields preserve the server’s default behavior;
AND CHAIN cannot be combined with RELEASE. The PostgreSQL spellings END and
ABORT produce a Commit and a RollbackStatement whose getCommand() is
END or ABORT, so consumers handle one model per completion:
Commit end = (Commit) CCJSqlParserUtil.parse("END WORK AND CHAIN",
parser -> parser.withDialect(Dialect.POSTGRESQL));
// END, WORK, CHAIN
String sql = end.getCommand() + ", " + end.getKeyword() + ", " + end.getChain();
END is a statement only with the PostgreSQL dialect, at the start of a statement and
outside BEGIN ... END blocks; ABORT is also read without a dialect. The existing
rollback work/savepoint/force accessors remain available. RELEASE [SAVEPOINT]
produces ReleaseSavepointStatement; the MySQL dialect requires SAVEPOINT.
Savepoint names retain identifier quoting.
SET [scope] TRANSACTION, PostgreSQL SET [scope] SESSION CHARACTERISTICS AS
TRANSACTION and SET [scope] TRANSACTION SNAPSHOT 'id' produce a
SetTransactionStatement. It reuses TransactionMode for the characteristics and
the SetScope of SetStatement for the scope keyword written after SET;
isSessionCharacteristics() marks the PostgreSQL form and getSnapshotId() returns
the snapshot as a StringValue. Each dialect accepts the scopes of its server: MySQL
GLOBAL, SESSION, LOCAL, PERSIST and PERSIST_ONLY, MariaDB GLOBAL,
SESSION and LOCAL, and PostgreSQL SESSION and LOCAL, also before
SESSION CHARACTERISTICS and SNAPSHOT. The MySQL dialect requires commas and
accepts one isolation level and one access mode. Variable assignments such as
SET transaction_isolation = 'READ-COMMITTED' remain SetStatement, whose
getScope() now types the getEffectParameter() keyword.
SET CONSTRAINTS {ALL | name [, ...]} {DEFERRED | IMMEDIATE} produces a
SetConstraintsStatement. An empty getConstraintNames() list means ALL, and
getTiming() reuses ConstraintAttributes.Initially. The MySQL dialect rejects it.
SetTransactionStatement set = (SetTransactionStatement) CCJSqlParserUtil.parse(
"SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY",
parser -> parser.withDialect(Dialect.MYSQL));
set.setScope(SetScope.GLOBAL);
// SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY
String sql = set.toString();
These statements are traversed by statement visitors, deparsers and transaction
feature analysis. The validator checks rollback, savepoint, setTransaction,
setConstraints and the clause features (transactionChain,
transactionRelease, transactionEndAbort, setTransactionGlobal,
setTransactionPersist, setTransactionSessionCharacteristics,
setTransactionSnapshot) against the MySQL and PostgreSQL capability sets.
PostgreSQL AND [NO] CHAIN is enabled beginning with its version 12 capability set.
Two-phase commit¶
The two-phase commit statements share one TransactionId: a global transaction
identifier, and for MySQL’s xid an optional branch qualifier and format ID
(gtrid [, bqual [, formatID]]). The identifier parts are the literals as written, a
StringValue or a HexValue.
PostgreSQL PREPARE TRANSACTION 'id' produces a PrepareTransactionStatement; it is
read before PREPARE name AS statement, which keeps producing PrepareStatement.
COMMIT PREPARED 'id' and ROLLBACK PREPARED 'id' produce a Commit and a
RollbackStatement whose getPreparedTransactionKeyword() is PREPARED and whose
getPreparedTransactionId() names the transaction. Oracle’s ROLLBACK FORCE 'id'
uses the same pair with the keyword FORCE; getForceDistributedTransactionIdentifier()
still returns the identifier as written. The PostgreSQL forms are read with the PostgreSQL
dialect or without one, and take a PostgreSQL string constant.
MySQL XA {START | BEGIN} xid [JOIN | RESUME], XA END xid [SUSPEND [FOR MIGRATE]],
XA PREPARE xid, XA COMMIT xid [ONE PHASE], XA ROLLBACK xid and
XA RECOVER [CONVERT XID] produce an XaStatement with an Action and an
optional Option. They are read with the MySQL and MariaDB dialects or without one;
MariaDB does not accept CONVERT XID. The gtrid and bqual are string, hexadecimal or
bit-value literals, and the formatID is an unsigned integer.
XaStatement xa = (XaStatement) CCJSqlParserUtil.parse("XA START X'0a', 'b', 1 JOIN",
parser -> parser.withDialect(Dialect.MYSQL));
TransactionId xid = xa.getTransactionId();
// START, X'0a', 'b', 1, JOIN
String parts = xa.getAction() + ", " + xid.getGlobalTransactionId() + ", "
+ xid.getBranchQualifier() + ", " + xid.getFormatId() + ", " + xa.getOption();
The validator checks twoPhaseCommit (PostgreSQL) and xa (MySQL and MariaDB).
Feature analysis reports XA RECOVER as returning a result set and the other
statements as transaction control.
MySQL resource groups¶
MySQL CREATE RESOURCE GROUP name TYPE [=] {SYSTEM | USER} [VCPU [=] vcpu_spec [, ...]]
[THREAD_PRIORITY [=] N] [ENABLE | DISABLE] produces a CreateResourceGroup and
ALTER RESOURCE GROUP name [VCPU ...] [THREAD_PRIORITY ...] [ENABLE | DISABLE] [FORCE]
an AlterResourceGroup. Both extend ResourceGroupDefinition, which holds the name,
the VCPU list as VcpuRange elements (a single CPU or start-end), the thread
priority and the State. Each option remembers whether it was written with =, :=
or nothing (OptionAssignment); the optional commas of the VCPU list are always printed.
SET RESOURCE GROUP name [FOR thread_id [, ...]] produces a SetResourceGroup whose
thread IDs are the numbers as written (LongValue or HexValue);
getThreadIds() is null without FOR.
DROP RESOURCE GROUP name [FORCE] is a Drop with the object type RESOURCE_GROUP
and isForce().
CreateResourceGroup group = (CreateResourceGroup) CCJSqlParserUtil.parse(
"CREATE RESOURCE GROUP rg1 TYPE = USER VCPU = 0-3, 5 THREAD_PRIORITY = 10 ENABLE",
parser -> parser.withDialect(Dialect.MYSQL));
// USER, [0-3, 5], 10, ENABLE
String parts = group.getType() + ", " + group.getVcpus() + ", "
+ group.getThreadPriority() + ", " + group.getState();
The statements are read with the MySQL dialect or without one and follow MySQL’s grammar:
the options come in the order above, and CPU numbers and the priority are plain integers.
What MySQL checks when it runs the statement, such as the CPU numbers of the machine or the
priority range of the group type, is not checked. In other dialects CREATE and ALTER
RESOURCE GROUP stay an UnsupportedStatement and DROP RESOURCE GROUP rg keeps the
generic Drop reading. The validator checks createResourceGroup,
alterResourceGroup and setResourceGroup (MySQL); feature analysis reports
SET RESOURCE GROUP as a session change and the others as schema changes.
MySQL loadable functions¶
MySQL CREATE [AGGREGATE] FUNCTION [IF NOT EXISTS] name RETURNS {STRING | INTEGER | REAL |
DECIMAL} SONAME 'library' produces a CreateFunction whose getLoadableFunction()
holds the name, the ReturnType, the library as a StringValue and isIfNotExists();
isAggregate() tells the two forms apart. The synonyms INT, INT4 and DEC keep
their spelling in getReturnTypeKeyword(). getFunctionDeclarationParts() is derived from
the definition, and setting declaration parts removes it.
CreateFunction function = (CreateFunction) CCJSqlParserUtil.parse(
"CREATE AGGREGATE FUNCTION IF NOT EXISTS f RETURNS REAL SONAME 'udf.so'",
parser -> parser.withDialect(Dialect.MYSQL));
LoadableFunction definition = function.getLoadableFunction();
// true, f, REAL, udf.so
String parts = function.isAggregate() + ", " + definition.getName() + ", "
+ definition.getReturnType() + ", " + definition.getLibrary().getValue();
With the MySQL dialect the statement follows MySQL’s grammar: AGGREGATE, or a function name
directly followed by RETURNS, must continue as a loadable function, without OR REPLACE.
With the MariaDB dialect or without one, a statement of exactly this shape gets the definition
(OR REPLACE included) and anything else keeps its previous reading. Other dialects are
unchanged: CREATE AGGREGATE FUNCTION stays an UnsupportedStatement there.
TablesNamesFinder finds no tables in a loadable function.
MySQL ALTER TABLE validation modifier¶
MySQL ALTER TABLE ... {WITH | WITHOUT} VALIDATION is an AlterExpressionTableOption
with the operation VALIDATION and getValidationMode(), like the ALGORITHM and
LOCK modifiers it may be mixed with in the action list. The mode written directly after
EXCHANGE PARTITION ... WITH TABLE t still belongs to that action
(AlterExpressionPartition.getExchangeValidationMode()).
Alter alter = (Alter) CCJSqlParserUtil.parse(
"ALTER TABLE t1 WITHOUT VALIDATION, EXCHANGE PARTITION p0 WITH TABLE t2",
parser -> parser.withDialect(Dialect.MYSQL));
AlterExpressionTableOption modifier =
(AlterExpressionTableOption) alter.getAlterExpressions().get(0);
// VALIDATION, WITHOUT, EXCHANGE_PARTITION
String parts = modifier.getOperation() + ", " + modifier.getValidationMode() + ", "
+ alter.getAlterExpressions().get(1).getOperation();
With the MySQL dialect, WITH or WITHOUT at the start of an action must be this
modifier, and it is rejected after a comma that follows a command MySQL accepts only at the
end of the list (a partition management command, DISCARD or IMPORT TABLESPACE,
PARTITION BY, REMOVE PARTITIONING). With the MariaDB dialect or without one, the
modifier is read where it is a whole item of the list; other dialects keep the raw-text action.
MySQL SHOW statements¶
The MySQL statements that read table, column and object definitions parse to typed statements in every dialect:
SHOW [EXTENDED] [FULL] TABLES [{FROM | IN} db] [LIKE 'p' | WHERE expr]parses toShowTablesStatement, as before.SHOW [EXTENDED] [FULL] {COLUMNS | FIELDS} {FROM | IN} t [{FROM | IN} db] [LIKE 'p' | WHERE expr]extendsShowColumnsStatement.getTable()returns theTable;getTableName()andsetTableName(String)remain and are derived from it.isExtended(),isFull()andisFieldsKeyword()record the modifiers and theFIELDSspelling, andgetTableSelectionMode()theFROM/INspelling before the table.SHOW [EXTENDED] {INDEX | INDEXES | KEYS} {FROM | IN} t [{FROM | IN} db] [WHERE expr]extendsShowIndexStatementin the same way:getTable(),isExtended(),getKeyword()(INDEX,INDEXESorKEYS),getTableSelectionMode(),getDbSelection()andgetWhereCondition(). TheStringconstructor,getTableName()andsetTableName(String)remain.SHOW INDEX FROM t FROM dbused to fail.SHOW TABLE STATUS [{FROM | IN} db] [LIKE 'p' | WHERE expr]parses toShowTableStatusStatement.SHOW CREATEparses toShowCreateStatement.getObjectType()isTABLE,VIEW,PROCEDURE,FUNCTION,TRIGGER,EVENT,DATABASEorUSER. The first six name the object withgetName(), aTable.DATABASEusesgetDatabaseName(),isSchemaKeyword()andisIfNotExists().USERusesgetAccount(), aUserAccountwithout authentication;isCurrentUser()andisCurrentUserParentheses()describeCURRENT_USERandCURRENT_USER().
The {FROM | IN} db clause is a ShowDbSelection (getSelectionMode() and
getName()) and the LIKE 'p' | WHERE expr filter a ShowFilter (getKind() and
getExpression()). ShowTablesStatement, ShowColumnsStatement,
ShowTableStatusStatement and MySqlShowStatement return them from
getDbSelection() and getFilter(); ShowIndexStatement has the database clause
only. The separate accessors remain and read and write these values:
getSelectionMode()/getDbSelectionMode(), getDbName(), getLikeExpression()
and getWhereCondition(). A selection mode set before the database name is kept for it.
A statement holds one filter, so setWhereCondition replaces a LIKE filter and
setLikeExpression a WHERE filter; ShowTablesStatement used to keep and render
both. ShowTablesStatement.toString() no longer fails without modifiers or without a
selection mode.
The other MySQL 8.4 SHOW statements
parse to MySqlShowStatement. getKind() tells which one it is and getKeyword()
how its keywords were spelled:
VARIABLESandSTATUStake a scope and a filter;PROCESSLISTtakesFULL;DATABASES(alsoSCHEMAS),PROCEDURE_STATUS,FUNCTION_STATUS,CHARACTER_SET(alsoCHARSETandCHAR SET) andCOLLATIONtake a filter;TRIGGERS(withFULL),EVENTSandOPEN_TABLEStake a database and a filter;PROCEDURE_CODEandFUNCTION_CODEtake the routine name, andENGINEthe engine name andSTATUS,MUTEXorLOGS;GRANTStakesFOR account [USING role, ...];WARNINGSandERRORStakeCOUNT(*)or a limit;BINLOG_EVENTStakesIN 'log',FROM posand a limit, andRELAYLOG_EVENTSalsoFOR CHANNEL 'c';REPLICA_STATUStakesFOR CHANNEL 'c';PROFILEtakes types such asCPUorBLOCK IO,FOR QUERY nand a limit;ENGINES(alsoSTORAGE ENGINES),PLUGINS,PRIVILEGES,PROFILES,BINARY_LOGS,BINARY_LOG_STATUSandREPLICAStake nothing.
The parts have typed accessors: getScope() (GLOBAL, SESSION or LOCAL),
isFull(), isCount(), getRoutineName() (a Table), getEngineName() and
getEngineInfo(), getDbSelection(), getFilter(), getAccount() and
getUsingRoles() (UserAccount values, read like the accounts of CREATE USER),
getLogName(), getPosition(), getLimit() (LIMIT [offset,] n) with
getOffset() (LIMIT n OFFSET offset), getChannel(), getProfileTypes() and
getProfileQuery(). Kind.allows(Part) tells which parts a kind takes; setting
another part throws IllegalArgumentException, and setKind drops the parts the new
kind does not take. setKeyword("SCHEMAS") selects the kind together with its spelling.
ShowColumnsStatement columns = (ShowColumnsStatement) CCJSqlParserUtil.parse(
"SHOW FULL COLUMNS FROM orders FROM shop LIKE 'c%'");
columns.getTableName(); // orders
columns.getDbName(); // shop
ShowCreateStatement create = (ShowCreateStatement) CCJSqlParserUtil.parse(
"SHOW CREATE USER 'app'@'%'");
create.getAccount().getHost().getNotExcapedValue(); // %
MySqlShowStatement grants = (MySqlShowStatement) CCJSqlParserUtil.parse(
"SHOW GRANTS FOR 'app'@'%' USING 'reader'");
grants.getKind(); // GRANTS
grants.getUsingRoles().size(); // 1
MySqlShowStatement events = (MySqlShowStatement) CCJSqlParserUtil.parse(
"SHOW BINLOG EVENTS IN 'binlog.000001' FROM 4 LIMIT 10",
parser -> parser.withDialect(Dialect.MYSQL));
events.getLogName().getValue(); // binlog.000001
events.getLimit().getRowCount(); // 10
MySqlShowStatement is read with Dialect.MYSQL and Dialect.MARIADB and without
a dialect; other dialects keep UnsupportedStatement. Without a dialect, a single word
such as SHOW VARIABLES or SHOW GRANTS remains a ShowStatement, as in every
dialect but MySQL and MariaDB. The spellings MySQL 8.4 removed, SHOW MASTER STATUS,
SHOW MASTER LOGS, SHOW SLAVE STATUS and SHOW SLAVE HOSTS, are read with
Dialect.MARIADB and without a dialect.
With Dialect.MYSQL or Dialect.MARIADB the LIKE pattern of every SHOW
statement is a string literal, as in the server, and parses to a StringValue;
without a dialect it is any simple expression, as before. Limit values are unsigned
integers, names or ? parameters, log names and channels are strings, and a log
position is an unsigned number.
A typed statement is used only when it covers the whole statement. Other forms, such as
Snowflake’s SHOW COLUMNS IN SCHEMA s, ClickHouse’s SHOW CREATE TABLE t FORMAT f or
SHOW WARNINGS LIKE 'a', remain UnsupportedStatement; SHOW INDEX forms with
such trailing clauses used to fail. A WHERE filter is read to its end, so
SHOW COLUMNS FROM t WHERE a = 1 AND b = 2 no longer becomes an
UnsupportedStatement, and text the expression does not take is a parse error.
StatementDeParser prints the expressions of the filters, limits and log positions with
its ExpressionDeParser. Table visitors report the table of SHOW COLUMNS and
SHOW INDEX (qualified by the FROM db clause) and the table or view of
SHOW CREATE TABLE and SHOW CREATE VIEW; until now SHOW INDEX raised
UnsupportedOperationException, as SHOW TABLES still does. SHOW TABLE STATUS
and MySqlShowStatement name no table. Validators report the showColumns,
showIndex, showTableStatus, showCreate and mySqlShow features, which the
MySQL and MariaDB capabilities enable, and look up the database of MySqlShowStatement
and the procedure of SHOW PROCEDURE CODE. Feature analysis
reports RETURNS_RESULT_SET.
MySQL EXPLAIN, prepared statements and DO¶
The MySQL forms of these utility statements extend the existing models. They are read with
Dialect.MYSQL and Dialect.MARIADB and, unless noted, without a dialect; other dialects
are unchanged.
EXPLAIN,
DESCRIBEandDESCtakeANALYZE [FORMAT = name]or[FORMAT = name] [INTO @variable], thenFOR CONNECTION idor[FOR {SCHEMA | DATABASE} name]and aSELECT,TABLE,VALUES,INSERT,REPLACE,UPDATEorDELETEstatement. They parse toExplainStatement, whosegetKeyword()keeps the spelling.ANALYZEandFORMATareOptionvalues like PostgreSQL’s options;Option.isUseEqual()marks MySQL’sFORMAT = name, andgetFormat()reads the format asExplainStatement.Format. A format other thanTRADITIONAL,JSONorTREEis rejected, as MySQL rejects it while it parses.getIntoVariable(),getConnectionId(),getSchemaName()andisSchemaKeyword()hold the other clauses, andgetTargetKind()tells a statement, a table and a connection apart.EXPLAIN SELECT,EXPLAIN ANALYZE SELECTandDESCRIBE SELECTkeep their readings, andDESCRIBE TABLE tneeds the MySQL dialect.{DESCRIBE | DESC | EXPLAIN} table {column | 'pattern'}keepsDescribeStatementandExplainStatement;getColumnPattern()is theColumnor theStringValue. It needs the MySQL dialect, sinceDESCRIBE x ynames an object kind in Snowflake.PREPARE name FROM {‘text’ | @variable} extends
PrepareStatement:getSource()is theStringValueorUserVariableandgetStatement()is null. The statement text is not parsed.EXECUTE name USING @a, @b fills
Execute.getUsingVariables().DROP PREPARE name parses to
DeallocateStatementwithgetKeyword()DROP.DO expr [, expr] … extends
DoStatement:getSelectItems()holds the expressions, which MySQL reads as a select list, andgetCode()is null. Without a dialect,DObefore a string orLANGUAGEremains PostgreSQL’s code block, which needsDialect.POSTGRESQL.
ExplainStatement explain = (ExplainStatement) CCJSqlParserUtil.parse(
"EXPLAIN ANALYZE FORMAT = TREE SELECT * FROM t",
parser -> parser.withDialect(Dialect.MYSQL));
explain.getFormat(); // TREE
explain.getOption(ExplainStatement.OptionType.ANALYZE); // present
Execute execute = (Execute) CCJSqlParserUtil.parse("EXECUTE s USING @a, @b");
execute.getUsingVariables().size(); // 2
Table visitors report the tables of the explained statement, of EXPLAIN table (until now
none) and of subqueries in MySQL’s DO. PREPARE ... FROM raises
UnsupportedOperationException, like EXECUTE and PostgreSQL’s DO, because the tables
of the statement text are unknown. Validators report explain, prepare,
executeUsing, deallocate and doStatement; the MySQL and MariaDB capabilities now
include prepare, executeExecute, executeUsing and doStatement. Feature analysis
reports MODIFIES_SESSION instead of RETURNS_RESULT_SET for EXPLAIN ... INTO, and reads
the expressions of MySQL’s DO instead of reporting OPAQUE.