diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2210dbe9d..b5f37735f 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -496,6 +496,27 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * Whether PRIOR starts Oracle's hierarchical operator, as in PRIOR id or PRIOR (a), rather + * than naming a column, as in prior = 1, prior IS NULL or ORDER BY prior DESC. + */ + private boolean isPriorOperatorAhead() { + if (getToken(1).kind != K_PRIOR) { + return false; + } + int kind = getToken(2).kind; + switch (kind) { + case OPENING_BRACKET: case S_IDENTIFIER: case S_QUOTED_IDENTIFIER: case DATA_TYPE: + case S_LONG: case S_DOUBLE: case S_CHAR_LITERAL: + return true; + case K_ASC: case K_DESC: case K_NULLS: + return false; + default: + return kind >= MIN_NON_RESERVED_WORD && kind <= MAX_NON_RESERVED_WORD + && !isConditionKeywordAhead(2); + } + } + /** * Scans ahead through a dotted identifier chain and checks if '(' follows. * Distinguishes function calls like func(), schema.func(), a.b.c.func() @@ -699,6 +720,15 @@ public class CCJSqlParser extends AbstractJSqlParser { && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)); } + /** + * Whether the ABSENT ON NULL or RETURNING clause of JSON_ARRAY or JSON_OBJECT starts here, + * rather than an element: ABSENT is a non-reserved word and RETURNING may name a column. + */ + private boolean isJsonConstructorClauseAhead() { + int kind = getToken(1).kind; + return kind == K_RETURNING || kind == K_ABSENT && getToken(2).kind == K_ON; + } + protected Expression prattArithRest(Expression left, int minPrec) throws ParseException { while (!interrupted) { Token t = getToken(1); @@ -863,12 +893,12 @@ public class CCJSqlParser extends AbstractJSqlParser { switch (t.kind) { case S_IDENTIFIER: case S_QUOTED_IDENTIFIER: case DATA_TYPE: case K_DATETIMELITERAL: case K_DATE_LITERAL: - case K_ALL: case K_ANY: case K_CASEWHEN: case K_CONNECT: case K_CREATE: - case K_DEFAULT: case K_GLOBAL: case K_GROUP: case K_GROUPING: case K_IF: + case K_ALL: case K_ANY: case K_BETWEEN: case K_CASEWHEN: case K_CONNECT: case K_CREATE: + case K_DEFAULT: case K_FULL: case K_GLOBAL: case K_GROUP: case K_GROUPING: case K_IF: case K_IIF: case K_IGNORE: case K_IN: case K_INTERVAL: case K_LEFT: - case K_LIMIT: case K_NEXTVAL: case K_OFFSET: case K_ON: case K_OPTIMIZE: - case K_ORDER: case K_PROCEDURE: case K_PUBLIC: case K_QUALIFY: case K_RIGHT: - case K_SET: case K_SOME: case K_START: case K_TABLES: + case K_LIMIT: case K_NEXTVAL: case K_OFFSET: case K_ON: case K_ONLY: case K_OPTIMIZE: + case K_ORDER: case K_PROCEDURE: case K_PUBLIC: case K_QUALIFY: case K_RETURNING: + case K_RIGHT: case K_SET: case K_SOME: case K_START: case K_TABLES: case K_TOP: case K_VALUE: case K_VALUES: return true; default: @@ -1243,7 +1273,7 @@ public class CCJSqlParser extends AbstractJSqlParser { switch (kind) { case K_JOIN: case K_FOR: case K_ORDER: case K_UNION: case K_EXCEPT: case K_INTERSECT: - case K_USE: case K_OPTIMIZE: case K_FORCE: case K_UNKNOWN: + case K_USE: case K_OPTIMIZE: return true; default: return false; @@ -1331,6 +1361,10 @@ public class CCJSqlParser extends AbstractJSqlParser { case K_RIGHT: return nextKind != K_JOIN && nextKind != K_OUTER && nextKind != K_SEMI && nextKind != K_ANTI && nextKind != K_ARRAY_LITERAL; + case K_FULL: return nextKind != K_JOIN && nextKind != K_OUTER + && nextKind != K_SEMI && nextKind != K_ANTI + && nextKind != K_ANY && nextKind != K_ALL + && nextKind != K_ARRAY_LITERAL; case K_ARRAY_LITERAL: return nextKind != K_JOIN; case K_ALL: return nextKind != K_JOIN; @@ -1396,8 +1430,12 @@ public class CCJSqlParser extends AbstractJSqlParser { return true; } + if (getToken(1).kind == K_ONLY && isRelationOnlyAhead(prev)) { + return false; + } + // ── 2. After delimiters and operators: expression position ───── - // With K_FROM/K_SELECT/K_CURRENT removed from RelObjectName's + // With K_FROM/K_SELECT removed from RelObjectName's // token list, accepting all remaining keywords after these // delimiters and operators is safe. if (!prev.image.isEmpty()) { @@ -1458,6 +1496,10 @@ public class CCJSqlParser extends AbstractJSqlParser { // Set operators case K_UNION: case K_EXCEPT: case K_INTERSECT: case K_MINUS: + // TABLE and COLUMN are always followed by the object name, as in + // CREATE TABLE value (...) or ADD COLUMN offset INT + case K_TABLE: case K_COLUMN: + // DDL / utility keywords (never identifiers) case K_FOREIGN: case K_CONSTRAINT: case K_UNIQUE: case K_CHECK: case K_FORCE: @@ -1474,6 +1516,24 @@ public class CCJSqlParser extends AbstractJSqlParser { return isReservedKeywordSafeByFollower(); } + /** + * Whether ONLY is PostgreSQL's prefix of a relation name, as in DELETE FROM ONLY t, + * JOIN ONLY t or FROM a, ONLY b, rather than a table or column named only. + */ + private boolean isRelationOnlyAhead(Token prev) { + int next = getToken(2).kind; + boolean name = next == S_IDENTIFIER || next == S_QUOTED_IDENTIFIER || next == OPENING_BRACKET; + switch (prev.kind) { + case K_FROM: case K_JOIN: case K_UPDATE: case K_INTO: case K_USING: + return name || next >= MIN_NON_RESERVED_WORD && next <= MAX_NON_RESERVED_WORD; + case K_COMMA: + // a column definition continues with its type instead, as in (a INT, only INT) + return name; + default: + return false; + } + } + /** * DuckDB's FROM-first query: a FROM clause followed by query clauses the pipe syntax cannot * consume. Scanning stops at a pipe operator or the end of the statement, and clause keywords @@ -1905,13 +1965,18 @@ public class CCJSqlParser extends AbstractJSqlParser { return false; } + // MySQL's FORCE INDEX|KEY (...) index hint, not an alias + if (kind == K_FORCE && (getToken(2).kind == K_INDEX || getToken(2).kind == K_KEY)) { + return false; + } + // For reserved keywords in alias position, skip the structural- // keyword whitelist and go directly to follower disambiguation. // Only check keywords that are actually in RelObjectName's token // alternatives — don't fire for brackets, operators, literals, etc. switch (kind) { case K_ALL: case K_ARRAY_LITERAL: case K_ANY: case K_CASEWHEN: case K_CONNECT: - case K_CREATE: case K_DEFAULT: + case K_CREATE: case K_DEFAULT: case K_FULL: case K_GLOBAL: case K_GROUP: case K_GROUPING: case K_IF: case K_IIF: case K_IGNORE: case K_IN: case K_INTERVAL: case K_LEFT: case K_LIMIT: case K_NEXTVAL: case K_OFFSET: @@ -2044,6 +2109,13 @@ public class CCJSqlParser extends AbstractJSqlParser { || "VALID".equalsIgnoreCase(getToken(offset + 1).image)); } + /** EXCLUDE starts an exclusion constraint only before USING, its elements or WHERE. */ + private boolean isExcludeConstraintAhead() { + int next = getToken(2).kind; + return getToken(1).kind == K_EXCLUDE + && (next == K_USING || next == OPENING_BRACKET || next == K_WHERE); + } + private boolean isMySqlStatisticsOptionAhead() { String name = getToken(1).image; return "STATS_AUTO_RECALC".equalsIgnoreCase(name) @@ -2290,7 +2362,7 @@ public class CCJSqlParser extends AbstractJSqlParser { Token token = getToken(1); // These existing modifiers remain available without a dialect preset. if (token.kind == K_STRAIGHT || token.kind == K_SQL_CACHE - || token.kind == K_SQL_NO_CACHE || token.kind == K_SQL_CALC_FOUND_ROWS) { + || token.kind == K_SQL_CALC_FOUND_ROWS) { return true; } // Keep qualified names, aliases and standalone identifier expressions intact. @@ -2300,6 +2372,10 @@ public class CCJSqlParser extends AbstractJSqlParser { || ")".equals(getToken(2).image)) { return false; } + // SQL_NO_CACHE is a non-reserved word, so it is a modifier only before a select item. + if (token.kind == K_SQL_NO_CACHE) { + return true; + } if (getAsString(Feature.dialect) != null && !isMySqlDialect()) { return false; } @@ -2907,12 +2983,24 @@ SKIP: * Non-reserved SQL keywords usable as unquoted identifiers. * Tokens are declared inline to get consecutive kind values between * MIN_NON_RESERVED_WORD and MAX_NON_RESERVED_WORD sentinels. + * + * The words are split over two productions: the lookahead routine JavaCC generates for one + * choice of all of them exceeds the JVM's 8000 byte limit for JIT compiled methods + * (-XX:HugeMethodLimit) and would always run interpreted. */ String NonReservedWord() : +{ String word; } +{ + ( word=NonReservedWordPart1() | word=NonReservedWordPart2() ) + { return word; } +} + +String NonReservedWordPart1() : { Token tk = null; } { ( - tk= + tk= + | tk= | tk= | tk= | tk= @@ -2989,6 +3077,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -3060,6 +3149,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -3076,6 +3166,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -3120,7 +3211,15 @@ String NonReservedWord() : | tk= | tk= | tk= - | tk= + ) + { return tk.image; } +} + +String NonReservedWordPart2() : +{ Token tk = null; } +{ + ( + tk= | tk= | tk= | tk= @@ -3194,6 +3293,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -3263,6 +3363,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -3303,6 +3404,8 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= + | tk= | tk= | tk= | tk= @@ -3331,6 +3434,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -3351,7 +3455,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */ { | -| | | | @@ -3366,7 +3469,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */ | | | -| | | | @@ -3379,7 +3481,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */ | | | -| | | | @@ -3391,7 +3492,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */ | | | -| | | /* Salesforce SOQL */ | @@ -3422,7 +3522,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */ | | | -| | | | @@ -3438,14 +3537,11 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */ | | | -| | | -| | | | -| | | | @@ -3455,7 +3551,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */ | | | -| | | | | @@ -4354,7 +4449,7 @@ TableReference RelationTarget(): TableReference target; } { - [ { only = true; } ] + [ LOOKAHEAD(2) { only = true; } ] ( "(" table=Table() ")" { parenthesized = true; } | table=Table() ) [ "*" { descendants = true; } ] { @@ -7328,11 +7423,11 @@ Column Column() #Column : * check isReservedKeywordAsIdentifier() — reserved keywords that can serve as * unquoted identifiers in the current parser position. * - * NOTE: K_FROM, K_SELECT, and K_CURRENT are deliberately NOT included here + * NOTE: K_FROM and K_SELECT are deliberately NOT included here * to avoid FIRST-set pollution in JavaCC's choice resolution. They are only * valid as identifiers in dotted-name continuations (after '.'), named * parameters (after ':'), index columns, constraint names, and similar - * restricted contexts. Those call sites add the three tokens inline. + * restricted contexts. Those call sites add the two tokens inline. */ String RelObjectName() : { Token tk = null; String result = null; } @@ -7352,13 +7447,13 @@ String RelObjectName() : /* ── Reserved keywords usable as identifiers (context-guarded) ─── */ | LOOKAHEAD({ isReservedKeywordAsIdentifier() }) - ( tk= | tk= | tk= | tk= - | tk= | tk= + ( tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= | tk= - | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= + | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= ) ) @@ -7366,10 +7461,10 @@ String RelObjectName() : } /* - * Extended version of RelObjectName that additionally accepts FROM, SELECT, - * and CURRENT as unquoted identifiers. + * Extended version of RelObjectName that additionally accepts FROM and SELECT + * as unquoted identifiers. * - * These three tokens are kept OUT of RelObjectName() to avoid polluting + * These two tokens are kept OUT of RelObjectName() to avoid polluting * JavaCC's FIRST-set computation (which would cause choice conflicts in * expression parsing, subselect detection, etc.). * @@ -7380,7 +7475,7 @@ String RelObjectName() : String RelObjectNameExt() : { Token tk = null; String result = null; } { - ( result = RelObjectName() | tk= | tk= | tk= ) + ( result = RelObjectName() | tk= | tk= ) { return tk != null ? tk.image : result; } } @@ -8192,7 +8287,8 @@ void MySqlSelectModifier(PlainSelect select): { ( LOOKAHEAD({ getToken(1).kind == K_HIGH_PRIORITY && isMySqlSelectModifierAhead() }) token= | token= - | token= | token= | token= + | token= | token= + | LOOKAHEAD({ getToken(1).kind == K_SQL_NO_CACHE && isMySqlSelectModifierAhead() }) token= | LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && isMySqlSelectModifierAhead() }) token= ) { modifier = MySqlSelectModifier.valueOf(token.image.toUpperCase(Locale.ROOT)); @@ -8958,7 +9054,7 @@ Expression SelectItemExpression() #ColumnsExpression(transformers != null): // @fixme: Oracle's SEQUENCE.nextval is parsed as COLUMN with a name part nextval // @todo: parse a proper SEQUENCE instead of a COLUMN ( - LOOKAHEAD(3) expression = ConnectByPriorOperator() + LOOKAHEAD({ isPriorOperatorAhead() }) expression = ConnectByPriorOperator() | ( expression = Condition() { expression = prattExpressionRest(expression, 2); } ) ) @@ -10688,7 +10784,7 @@ String OptionHintName(): ( tk= | word = NonReservedWord() | tk= | tk= | tk= | tk= | tk= | tk= - | tk= | tk= | tk= | tk= ) + | tk= | tk= ) { if (name.length() > 0) { name.append(' '); } name.append(tk != null ? tk.image : word); @@ -11048,7 +11144,8 @@ Expression Condition(): LOOKAHEAD(2, { !isExistsIdentifierAhead() }) result=ExistsExpression() | - [ LOOKAHEAD(2, { getToken(1).kind == K_PRIOR }) { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ] + // A choice rather than [...], so that enclosing syntactic lookaheads honour the guard + ( LOOKAHEAD({ isPriorOperatorAhead() }) { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } | {} ) left=SimpleExpression() { result = left; } // Consume Oracle (+) once, before dispatching @@ -11143,7 +11240,7 @@ Expression RegularConditionRHS(Expression leftExpression, int oracleJoinRight) # | { result = new CosineSimilarity(); } ) - ( LOOKAHEAD(2) rightExpression=ComparisonItem() { oraclePrior = EqualsTo.ORACLE_PRIOR_END; } + ( LOOKAHEAD({ isPriorOperatorAhead() }) rightExpression=ComparisonItem() { oraclePrior = EqualsTo.ORACLE_PRIOR_END; } | rightExpression=ComparisonItem() ) [ LOOKAHEAD(2) "(" "+" ")" { oracleJoin = EqualsTo.ORACLE_JOIN_LEFT; } ] @@ -12058,7 +12155,8 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(4 , {!interrupted}) retval=ExtractExpression() - | retval=XMLSerializeExpr() + | LOOKAHEAD({ getToken(1).kind == K_XMLSERIALIZE && getToken(2).kind == OPENING_BRACKET }) + retval=XMLSerializeExpr() | LOOKAHEAD(3, { !interrupted}) retval = JsonFunction() @@ -12093,8 +12191,9 @@ Expression PrimaryExpression() #PrimaryExpression: // there is a huge! performance deterioration from this production //| LOOKAHEAD(FunctionAllColumns()) retval=FunctionAllColumns() - // support timestamp expressions - | LOOKAHEAD(2, {!interrupted}) (token= | token=) { retval = new TimeKeyExpression(token.image); } + // support timestamp expressions; current.x is a qualified column + | LOOKAHEAD(2, {!interrupted && !(getToken(1).kind == K_CURRENT && ".".equals(getToken(2).image))}) + (token= | token=) { retval = new TimeKeyExpression(token.image); } | LOOKAHEAD(2, {!interrupted}) retval=DateTimeLiteralExpression() @@ -12110,7 +12209,7 @@ Expression PrimaryExpression() #PrimaryExpression: | retval=ConnectByRootOperator() - | retval=ConnectByPriorOperator() + | LOOKAHEAD({ isPriorOperatorAhead() }) retval=ConnectByPriorOperator() | LOOKAHEAD(2, { !interrupted && getToken(1).kind == K_KEY && getToken(2).kind != OPENING_BRACKET }) retval=KeyExpression() @@ -12698,7 +12797,8 @@ JsonFunction JsonObjectBody() : { ( "(" ( // First Element - LOOKAHEAD(2) keyValuePair = JsonKeyValuePair(true) { result.add(keyValuePair);} + LOOKAHEAD(2, { !isJsonConstructorClauseAhead() }) + keyValuePair = JsonKeyValuePair(true) { result.add(keyValuePair);} // Next Elements ( @@ -12744,6 +12844,7 @@ JsonFunction JsonArrayBody() : { { result.setOnNullType( JsonAggregateOnNullType.NULL ); } ) | + LOOKAHEAD(1, { !isJsonConstructorClauseAhead() }) expression=Expression() { functionExpression = new JsonFunctionExpression( expression ); result.add( functionExpression ); } [ @@ -13649,7 +13750,7 @@ WindowElement WindowElement(): ( { windowElement.setType(WindowElement.Type.ROWS); } | { windowElement.setType(WindowElement.Type.RANGE); } | { windowElement.setType(WindowElement.Type.GROUPS); }) - ( ( + ( LOOKAHEAD(1) ( { windowElement.setRange(range); } offset = WindowOffset() { range.setStart(offset); } offset = WindowOffset() { range.setEnd(offset); } @@ -13702,7 +13803,7 @@ WindowOffset WindowOffset(): } { ( - ( + LOOKAHEAD(2) ( ( { offset.setType(WindowOffset.Type.PRECEDING); } | { offset.setType(WindowOffset.Type.FOLLOWING); } ) ) @@ -14486,7 +14587,8 @@ JsonTableFunction.JsonTableColumnDefinition JsonTableColumnDefinition() : { LOOKAHEAD( ColDataType(), - { !(getToken(1).kind == S_IDENTIFIER && ( + { getToken(1).kind != K_PATH + && !(getToken(1).kind == S_IDENTIFIER && ( getToken(1).image.equalsIgnoreCase("ALLOW") || getToken(1).image.equalsIgnoreCase("DISALLOW"))) } ) dataType = ColDataType() { valueColumnDefinition.setDataType(dataType); } @@ -15194,7 +15296,7 @@ CreateIndex CreateIndex(): name=RelObjectNames() { index.setName(name.getNames()); } ] ( LOOKAHEAD(3)( - [ { createIndex.setOnly(true); } ] table=Table() + [ LOOKAHEAD(2) { createIndex.setOnly(true); } ] table=Table() [ indexType=UsingIndexType() { index.setUsing(indexType); } ] ) | @@ -15204,7 +15306,7 @@ CreateIndex CreateIndex(): createIndex.setIndexTypeBeforeOn(true); } ] - [ { createIndex.setOnly(true); } ] table=Table() + [ LOOKAHEAD(2) { createIndex.setOnly(true); } ] table=Table() ) ) colNames = IndexColumnsWithParamsList() @@ -16258,6 +16360,7 @@ TableElement TableConstraint(boolean createContext): index = checkConstraint; } | + LOOKAHEAD({ isExcludeConstraintAhead() }) namedConstraint=PostgreSqlExcludeConstraint() { namedConstraint.setName(constraintName); index = namedConstraint; } | LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) @@ -16269,7 +16372,7 @@ TableElement TableConstraint(boolean createContext): | checkConstraint=CheckConstraintSpec(null) { index = checkConstraint; } | - index=PostgreSqlExcludeConstraint() + LOOKAHEAD({ isExcludeConstraintAhead() }) index=PostgreSqlExcludeConstraint() | LOOKAHEAD({ Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) && getToken(1).kind == K_NOT }) @@ -17084,6 +17187,8 @@ ColDataType ColDataType(): | tk= | tk= | tk= + // PostgreSQL's geometric type + | tk= ) { schema = tk.image; @@ -19905,7 +20010,7 @@ Alter AlterTable(boolean foreign): { { alter.setForeignTable(foreign); } [ LOOKAHEAD(2) { alter.setUseTableIfExists(true); } ] - [ { alter.setUseOnly(true); } ] + [ LOOKAHEAD(2) { alter.setUseOnly(true); } ] [ LOOKAHEAD(2) { requireDdlSyntax(!alter.isUseTableIfExists(), "Duplicate IF EXISTS"); alter.setUseTableIfExists(true); @@ -21567,7 +21672,7 @@ List SubscriptionOptions(): PublicationTable PublicationTable(boolean drop): { PublicationTable result = new PublicationTable(); Table table; String name; Expression expression; ExpressionList columns; } { - [ { result.setOnly(true); } ] + [ LOOKAHEAD(2) { result.setOnly(true); } ] table=Table() { result.setTable(table); } [ "*" { result.setIncludeDescendants(true); } ] [ LOOKAHEAD(1) "(" { columns = new ExpressionList(); } diff --git a/src/site/sphinx/keywords.rst b/src/site/sphinx/keywords.rst index 473148b23..4fb9318da 100644 --- a/src/site/sphinx/keywords.rst +++ b/src/site/sphinx/keywords.rst @@ -7,8 +7,6 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | **Keyword** | +---------------------------+ -| ABSENT | -+---------------------------+ | ALL | +---------------------------+ | AND | @@ -37,8 +35,6 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | CROSS | +---------------------------+ -| CURRENT | -+---------------------------+ | DISTINCT | +---------------------------+ | DISTINCTROW | @@ -49,10 +45,6 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | EXCLUDES | +---------------------------+ -| EXISTS | -+---------------------------+ -| EXTEND | -+---------------------------+ | FALSE | +---------------------------+ | FETCH | @@ -81,8 +73,6 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | ILIKE | +---------------------------+ -| IMPORT | -+---------------------------+ | IN | +---------------------------+ | INCLUDES | @@ -137,8 +127,6 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | PREFERRING | +---------------------------+ -| PRIOR | -+---------------------------+ | PROCEDURE | +---------------------------+ | PUBLIC | @@ -149,8 +137,6 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | RIGHT | +---------------------------+ -| SAMPLE | -+---------------------------+ | SET | +---------------------------+ | SOME | @@ -159,12 +145,8 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | SQL_CALC_FOUND_ROWS | +---------------------------+ -| SQL_NO_CACHE | -+---------------------------+ | START | +---------------------------+ -| STATEMENT | -+---------------------------+ | STRAIGHT_JOIN | +---------------------------+ | TABLES | @@ -177,14 +159,10 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | TRUE | +---------------------------+ -| UNBOUNDED | -+---------------------------+ | UNION | +---------------------------+ | UNIQUE | +---------------------------+ -| UNKNOWN | -+---------------------------+ | UNPIVOT | +---------------------------+ | USE | @@ -203,7 +181,5 @@ The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and m +---------------------------+ | WITH | +---------------------------+ -| XMLSERIALIZE | -+---------------------------+ | XOR | +---------------------------+ diff --git a/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java b/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java index d5775d124..d07d818aa 100644 --- a/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java @@ -9,10 +9,18 @@ */ package net.sf.jsqlparser.statement; +import java.util.function.Consumer; import java.util.stream.Stream; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.AnalyticExpression; +import net.sf.jsqlparser.expression.ConnectByPriorOperator; +import net.sf.jsqlparser.expression.JsonAggregateOnNullType; +import net.sf.jsqlparser.expression.JsonFunction; import net.sf.jsqlparser.expression.TernaryExpression; import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.WindowElement; +import net.sf.jsqlparser.expression.WindowOffset; +import net.sf.jsqlparser.expression.XMLSerializeExpr; import net.sf.jsqlparser.statement.piped.FromQuery; import net.sf.jsqlparser.statement.piped.SelectPipeOperator; import net.sf.jsqlparser.expression.NotExpression; @@ -21,16 +29,29 @@ import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; +import net.sf.jsqlparser.expression.operators.relational.IsUnknownExpression; import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.parser.ParserKeywordsUtils; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.index.CreateIndex; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.ExcludeConstraint; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.select.Join; +import net.sf.jsqlparser.statement.select.MySqlSelectModifier; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.SampleClause; import net.sf.jsqlparser.statement.select.WithItem; +import net.sf.jsqlparser.util.TablesNamesFinder; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; @@ -338,4 +359,199 @@ public void testClickHouseClausesAfterTable() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SELECT a FROM mytable PREWHERE b = 1 WHERE c = 2", true); assertSqlCanBeParsedAndDeparsed("SELECT a FROM mytable SETTINGS max_threads = 8", true); } + + // MySQL 8.4 and PostgreSQL 18 accept these keywords as unquoted column and table names. + public static Stream serverKeywordNames() { + return Stream.of("absent", "any", "between", "current", "exclude", "force", "full", + "grouping", "if", "import", "offset", "only", "prior", "returning", "set", "some", + "sql_no_cache", "unbounded", "unknown", "value", "values", "xmlserialize") + .flatMap(keyword -> Stream.of(Arguments.of(keyword, null), + Arguments.of(keyword, Dialect.MYSQL), + Arguments.of(keyword, Dialect.POSTGRESQL))); + } + + private static Statement assertRoundTrip(String sql, Dialect dialect) + throws JSQLParserException { + Consumer consumer = parser -> { + if (dialect != null) { + parser.withDialect(dialect); + } + }; + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true, consumer); + assertEquals(statement.toString(), + CCJSqlParserUtil.parse(statement.toString(), consumer).toString()); + return statement; + } + + @ParameterizedTest(name = "{0} ({1})") + @MethodSource("serverKeywordNames") + void testServerKeywordAsColumnAndTableName(String keyword, Dialect dialect) + throws JSQLParserException { + CreateTable createTable = (CreateTable) assertRoundTrip( + "CREATE TABLE mytable (id INT, " + keyword + " INT)", dialect); + assertEquals(keyword, createTable.getColumnDefinitions().get(1).getColumnName()); + + Alter alter = (Alter) assertRoundTrip( + "ALTER TABLE mytable ADD COLUMN " + keyword + " INT", dialect); + assertEquals(keyword, + alter.getAlterExpressions().get(0).getColDataTypeList().get(0).getColumnName()); + + // A keyword table name made CREATE TABLE fall back to an UnsupportedStatement + CreateTable namedTable = (CreateTable) assertRoundTrip( + "CREATE TABLE " + keyword + " (a INT)", dialect); + assertEquals(keyword, namedTable.getTable().getName()); + + assertRoundTrip("SELECT " + keyword + ", t." + keyword + " AS " + keyword + + " FROM mytable t WHERE " + keyword + " = 1 ORDER BY " + keyword, dialect); + assertRoundTrip("SELECT a FROM " + keyword + " WHERE a = 1", dialect); + assertRoundTrip("UPDATE mytable SET " + keyword + " = 1", dialect); + assertRoundTrip("INSERT INTO mytable (" + keyword + ") VALUES (1)", dialect); + } + + @Test + void testTableAndColumnKeepTheirOptionalClauses() throws JSQLParserException { + CreateTable createTable = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE IF NOT EXISTS if (a INT)", true); + assertTrue(createTable.isIfNotExists()); + assertEquals("if", createTable.getTable().getName()); + + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE IF EXISTS value ADD COLUMN IF NOT EXISTS set INT", true); + assertTrue(alter.isUseTableIfExists()); + assertEquals("value", alter.getTable().getName()); + assertEquals("set", + alter.getAlterExpressions().get(0).getColDataTypeList().get(0).getColumnName()); + } + + @Test + void testJoinAndIndexHintKeywordsStayClauses() throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM a FULL JOIN b ON a.id = b.id", true); + assertNull(((Table) select.getFromItem()).getAlias()); + assertTrue(select.getJoins().get(0).isFull()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM a full FULL OUTER JOIN b ON full.id = b.id", true); + assertEquals("full", select.getFromItem().getAlias().getName()); + Join join = select.getJoins().get(0); + assertTrue(join.isFull() && join.isOuter()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM mytable FORCE INDEX (idx) WHERE a = 1", true, + parser -> parser.withDialect(Dialect.MYSQL)); + Table table = (Table) select.getFromItem(); + assertNull(table.getAlias()); + assertNotNull(table.getIndexHint()); + } + + @Test + void testReturningAndAbsentStayClauses() throws JSQLParserException { + Delete delete = (Delete) assertSqlCanBeParsedAndDeparsed( + "DELETE FROM mytable RETURNING id", true); + assertNull(delete.getTable().getAlias()); + assertNotNull(delete.getReturningClause()); + + Insert insert = (Insert) assertSqlCanBeParsedAndDeparsed( + "INSERT INTO mytable SELECT a FROM other RETURNING id", true); + assertNull(((PlainSelect) insert.getSelect()).getFromItem().getAlias()); + assertNotNull(insert.getReturningClause()); + + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT JSON_ARRAY(a ABSENT ON NULL RETURNING JSON) FROM mytable", true); + JsonFunction function = (JsonFunction) select.getSelectItem(0).getExpression(); + assertEquals(1, function.getExpressions().size()); + assertEquals(JsonAggregateOnNullType.ABSENT, function.getOnNullType()); + assertEquals("JSON", function.getReturningType().getDataType()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT JSON_ARRAY(RETURNING JSON)", + true); + function = (JsonFunction) select.getSelectItem(0).getExpression(); + assertTrue(function.getExpressions().isEmpty()); + assertEquals("JSON", function.getReturningType().getDataType()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT JSON_OBJECT(ABSENT ON NULL)", + true); + function = (JsonFunction) select.getSelectItem(0).getExpression(); + assertTrue(function.getKeyValuePairs().isEmpty()); + assertEquals(JsonAggregateOnNullType.ABSENT, function.getOnNullType()); + } + + @Test + void testModifierAndOperatorKeywordsStayKeywords() throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT SQL_NO_CACHE a FROM mytable", true); + assertTrue(select.getMySqlSelectModifiers().contains(MySqlSelectModifier.SQL_NO_CACHE)); + assertEquals("a", ((Column) select.getSelectItem(0).getExpression()).getColumnName()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT PRIOR ename FROM emp CONNECT BY PRIOR empno = mgr", true); + assertInstanceOf(ConnectByPriorOperator.class, select.getSelectItem(0).getExpression()); + EqualsTo connect = (EqualsTo) select.getOracleHierarchical().getConnectExpression(); + assertEquals(SupportsOldOracleJoinSyntax.ORACLE_PRIOR_START, + connect.getOraclePriorPosition()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT SUM(a) OVER (ORDER BY b ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)" + + " FROM mytable", + true); + WindowElement frame = + ((AnalyticExpression) select.getSelectItem(0).getExpression()).getWindowElement(); + assertEquals(WindowOffset.Type.PRECEDING, frame.getRange().getStart().getType()); + assertNull(frame.getRange().getStart().getExpression()); + assertEquals(WindowOffset.Type.CURRENT, frame.getRange().getEnd().getType()); + + select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT XMLSERIALIZE(CONTENT x AS VARCHAR(100)) FROM mytable WHERE a IS NOT UNKNOWN", + true); + assertInstanceOf(XMLSerializeExpr.class, select.getSelectItem(0).getExpression()); + assertInstanceOf(IsUnknownExpression.class, select.getWhere()); + } + + @Test + void testPostgreSqlOnlyStaysKeyword() throws JSQLParserException { + Alter alter = (Alter) assertSqlCanBeParsedAndDeparsed( + "ALTER TABLE ONLY public.mytable ADD COLUMN a INT", true); + assertTrue(alter.isUseOnly()); + assertEquals("public.mytable", alter.getTable().getFullyQualifiedName()); + + PlainSelect select = + (PlainSelect) assertSqlCanBeParsedAndDeparsed("SELECT * FROM ONLY mytable", true); + assertTrue(select.isUsingOnly()); + + CreateIndex createIndex = (CreateIndex) assertSqlCanBeParsedAndDeparsed( + "CREATE INDEX idx ON ONLY mytable (a)", true); + assertTrue(createIndex.isOnly()); + + // Still unsupported forms must not be read as a table named only + for (String sql : new String[] {"DELETE FROM ONLY mytable", + "SELECT * FROM a JOIN ONLY b ON a.id = b.id", "SELECT * FROM a, ONLY b", + "UPDATE a SET x = 1 FROM ONLY b"}) { + assertFalse(readsTableNamedOnly(sql), sql); + } + } + + private static boolean readsTableNamedOnly(String sql) { + try { + return new TablesNamesFinder<>().getTables(CCJSqlParserUtil.parse(sql)).stream() + .anyMatch("only"::equalsIgnoreCase); + } catch (JSQLParserException exception) { + return false; + } + } + + @Test + void testExcludeColumnAndPathType() throws JSQLParserException { + CreateTable createTable = (CreateTable) assertSqlCanBeParsedAndDeparsed( + "CREATE TABLE mytable (exclude INT, p path, EXCLUDE USING gist (p WITH &&))", true, + parser -> parser.withDialect(Dialect.POSTGRESQL)); + assertEquals("exclude", createTable.getColumnDefinitions().get(0).getColumnName()); + assertEquals("path", + createTable.getColumnDefinitions().get(1).getColDataType().getDataType()); + assertEquals(1, createTable.getTableElements(ExcludeConstraint.class).size()); + + // PATH still introduces the path of a JSON_TABLE column without a type + assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM JSON_TABLE(doc, '$' COLUMNS (a PATH '$.a', b INT PATH '$.b')) jt", + true); + } }