Skip to content

Commit fa6ee43

Browse files
committed
fix(parser): resolve shadowed contextual keyword tokens
Share contextual keyword consumption for MATCHING and DISTRIBUTED RECOVERY, and remove unreachable dedicated-keyword alternatives from DATA_TYPE. Keep these words usable as identifiers and preserve existing type parsing. Fixes #2403.
1 parent eddb1fb commit fa6ee43

2 files changed

Lines changed: 99 additions & 7 deletions

File tree

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2339,6 +2339,8 @@ TOKEN : /* Date/Time with time zones */
23392339

23402340
TOKEN : /* Data Types */
23412341
{
2342+
// Dedicated keyword tokens (e.g. K_BINARY/K_CHAR) are consumed by DataType().
2343+
// Repeating them here cannot match DATA_TYPE and causes unreachable-token warnings.
23422344
<DATA_TYPE: ( <TYPE_BIT> | <TYPE_BLOB> | <TYPE_BOOLEAN> | <TYPE_ENUM>
23432345
| <TYPE_REAL> | <TYPE_DOUBLE> | <TYPE_UUID> | <TYPE_MAP>| <TYPE_TINYINT> | <TYPE_SMALLINT>
23442346
| <TYPE_INTEGER> | <TYPE_BIGINT> | <TYPE_HUGEINT> | <TYPE_UTINYINT> | <TYPE_USMALLINT>
@@ -2347,15 +2349,15 @@ TOKEN : /* Data Types */
23472349
) >
23482350

23492351
| <#TYPE_BIT: "BISTRING">
2350-
| <#TYPE_BLOB: "BLOB" | "BYTEA" | <K_BINARY> | "VARBINARY" | <K_BYTES> >
2351-
| <#TYPE_BOOLEAN: <K_BOOLEAN> | "BOOL" >
2352+
| <#TYPE_BLOB: "BLOB" | "BYTEA" | "VARBINARY" >
2353+
| <#TYPE_BOOLEAN: "BOOL" >
23522354
| <#TYPE_CLOB: "CLOB">
23532355
| <#TYPE_ENUM: "ENUM" >
23542356
| <#TYPE_MAP: "MAP" >
23552357
| <#TYPE_DECIMAL: "DECIMAL" | "NUMBER" | "NUMERIC" >
23562358
| <#TYPE_TINYINT: "TINYINT" | "INT1" >
23572359
| <#TYPE_SMALLINT: "SMALLINT" | "INT2" | "SHORT" >
2358-
| <#TYPE_INTEGER: ( "INTEGER" | "INT" | "INT4" | <K_SIGNED> | <K_UNSIGNED> ) >
2360+
| <#TYPE_INTEGER: ( "INTEGER" | "INT" | "INT4" ) >
23592361
| <#TYPE_BIGINT: "BIGINT" | "INT8" | "LONG" >
23602362
| <#TYPE_HUGEINT: "HUGEINT" >
23612363
| <#TYPE_UTINYINT: "UTINYINT" >
@@ -2365,7 +2367,7 @@ TOKEN : /* Data Types */
23652367
| <#TYPE_UHUGEINT: "UHUGEINT" >
23662368
| <#TYPE_REAL: "REAL" | "FLOAT4" | "FLOAT">
23672369
| <#TYPE_DOUBLE: "DOUBLE" | "PRECISION" | "FLOAT8" | "FLOAT64">
2368-
| <#TYPE_VARCHAR: "NVARCHAR" | "VARCHAR" | "NCHAR" | <K_CHAR> | "BPCHAR" | "TEXT" | "STRING" | <K_CHARACTER> | "VARYING">
2370+
| <#TYPE_VARCHAR: "NVARCHAR" | "VARCHAR" | "NCHAR" | "BPCHAR" | "TEXT" | "STRING" | "VARYING">
23692371
| <#TYPE_TIME: "TIMETZ" >
23702372
| <#TYPE_TIMESTAMP: "TIMESTAMP_NS" | "TIMESTAMP_MS" | "TIMESTAMP_S" >
23712373

@@ -5756,7 +5758,8 @@ String SetOperationModifier():
57565758
[ ( tk=<K_ALL> | tk="DISTINCT") { modifier+=tk.image; } ]
57575759
<K_BY> <K_NAME> { modifier+= " BY NAME"; }
57585760
[
5759-
"MATCHING" { modifier+= " MATCHING"; }
5761+
LOOKAHEAD({ isKeywordAhead("MATCHING") })
5762+
ContextualKeyword("MATCHING") { modifier+= " MATCHING"; }
57605763
"("
57615764
identifier = RelObjectName() { modifier+="(" + identifier; }
57625765
("," identifier = RelObjectName() { modifier+=", " + identifier; })*
@@ -15526,14 +15529,14 @@ AlterSystemStatement AlterSystemStatement():
1552615529
|
1552715530
(
1552815531
<K_ENABLE> (
15529-
"DISTRIBUTED" "RECOVERY" { operation = AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY; }
15532+
ContextualKeyword("DISTRIBUTED") ContextualKeyword("RECOVERY") { operation = AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY; }
1553015533
| <K_RESTRICTED> <K_SESSION> { operation = AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY; }
1553115534
)
1553215535
)
1553315536
|
1553415537
(
1553515538
<K_DISABLE> (
15536-
"DISTRIBUTED" "RECOVERY" { operation = AlterSystemOperation.DISABLE_DISTRIBUTED_RECOVERY; }
15539+
ContextualKeyword("DISTRIBUTED") ContextualKeyword("RECOVERY") { operation = AlterSystemOperation.DISABLE_DISTRIBUTED_RECOVERY; }
1553715540
| <K_RESTRICTED> <K_SESSION> { operation = AlterSystemOperation.DISABLE_RESTRICTED_SESSION; }
1553815541
)
1553915542
)
@@ -15689,6 +15692,17 @@ Comment Comment():
1568915692
}
1569015693
}
1569115694

15695+
/** Consumes a keyword that remains an ordinary identifier outside this grammar context. */
15696+
void ContextualKeyword(String expected):
15697+
{ Token keyword; }
15698+
{
15699+
keyword=<S_IDENTIFIER> {
15700+
if (!expected.equalsIgnoreCase(keyword.image)) {
15701+
throw new ParseException("Expected " + expected + " but found " + keyword.image);
15702+
}
15703+
}
15704+
}
15705+
1569215706
void AccessKeyword(String expected):
1569315707
{ Token token; }
1569415708
{ (token=<S_IDENTIFIER> | token=<DATA_TYPE>) { requireAccessSyntax(expected.equalsIgnoreCase(token.image), "Expected " + expected); } }
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.parser;
11+
12+
import net.sf.jsqlparser.JSQLParserException;
13+
import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
14+
import net.sf.jsqlparser.statement.alter.AlterSystemOperation;
15+
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.params.ParameterizedTest;
17+
import org.junit.jupiter.params.provider.ValueSource;
18+
19+
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
20+
import static org.junit.jupiter.api.Assertions.*;
21+
22+
class ContextualTokenTest {
23+
@ParameterizedTest
24+
@ValueSource(strings = {
25+
"SELECT a FROM t UNION ALL BY NAME MATCHING (a) SELECT a FROM u",
26+
"SELECT a FROM t UNION DISTINCT BY NAME MATCHING (a, b) SELECT a FROM u",
27+
"ALTER SYSTEM ENABLE DISTRIBUTED RECOVERY",
28+
"ALTER SYSTEM DISABLE DISTRIBUTED RECOVERY"
29+
})
30+
void parsesPreviouslyShadowedContextualKeywords(String sql) throws Exception {
31+
for (boolean complex : new boolean[] {false, true}) {
32+
String parsed = assertSqlCanBeParsedAndDeparsed(sql, true,
33+
parser -> parser.withAllowComplexParsing(complex)).toString();
34+
assertEquals(parsed, CCJSqlParserUtil.parse(parsed).toString());
35+
}
36+
}
37+
38+
@Test
39+
void preservesAlterSystemOperation() throws Exception {
40+
assertEquals(AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY,
41+
((AlterSystemStatement) CCJSqlParserUtil.parse(
42+
"alter system enable distributed recovery")).getOperation());
43+
assertEquals(AlterSystemOperation.DISABLE_DISTRIBUTED_RECOVERY,
44+
((AlterSystemStatement) CCJSqlParserUtil.parse(
45+
"alter system disable distributed recovery")).getOperation());
46+
}
47+
48+
@ParameterizedTest
49+
@ValueSource(strings = {"MATCHING", "DISTRIBUTED", "RECOVERY"})
50+
void keepsContextualWordsUsableAsIdentifiers(String word) throws Exception {
51+
assertEquals(CCJSqlParserConstants.S_IDENTIFIER,
52+
CCJSqlParserUtil.newParser(word).getNextToken().kind);
53+
assertSqlCanBeParsedAndDeparsed("SELECT " + word + ", " + word
54+
+ "(a) FROM " + word + " AS x", true);
55+
}
56+
57+
@ParameterizedTest
58+
@ValueSource(
59+
strings = {"BYTES", "BINARY", "BOOLEAN", "UNSIGNED", "SIGNED", "CHARACTER", "CHAR"})
60+
void retainsDedicatedTypeTokensAndTypeParsing(String type) throws Exception {
61+
assertNotEquals(CCJSqlParserConstants.DATA_TYPE,
62+
CCJSqlParserUtil.newParser(type).getNextToken().kind);
63+
assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (c " + type + ")", true);
64+
assertSqlCanBeParsedAndDeparsed("SELECT CAST(c AS " + type + ") FROM t", true);
65+
}
66+
67+
@ParameterizedTest
68+
@ValueSource(strings = {
69+
"ALTER SYSTEM ENABLE other RECOVERY",
70+
"ALTER SYSTEM DISABLE DISTRIBUTED other",
71+
"ALTER SYSTEM ENABLE DISTRIBUTED",
72+
"SELECT a FROM t UNION ALL BY NAME other (a) SELECT a FROM u",
73+
"SELECT a FROM t UNION ALL BY NAME MATCHING () SELECT a FROM u"
74+
})
75+
void rejectsWrongContextualKeywords(String sql) {
76+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
77+
}
78+
}

0 commit comments

Comments
 (0)