From fa6ee43af2c2fd40203d47de0d8977fc768dcb80 Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 11 Sep 2026 16:09:07 +0900 Subject: [PATCH] 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. --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 28 +++++-- .../parser/ContextualTokenTest.java | 78 +++++++++++++++++++ 2 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/parser/ContextualTokenTest.java diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index e89a86094..9c20fa869 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2339,6 +2339,8 @@ TOKEN : /* Date/Time with time zones */ TOKEN : /* Data Types */ { + // Dedicated keyword tokens (e.g. K_BINARY/K_CHAR) are consumed by DataType(). + // Repeating them here cannot match DATA_TYPE and causes unreachable-token warnings. | | | | | | | | | | | | | | @@ -2347,15 +2349,15 @@ TOKEN : /* Data Types */ ) > | <#TYPE_BIT: "BISTRING"> - | <#TYPE_BLOB: "BLOB" | "BYTEA" | | "VARBINARY" | > - | <#TYPE_BOOLEAN: | "BOOL" > + | <#TYPE_BLOB: "BLOB" | "BYTEA" | "VARBINARY" > + | <#TYPE_BOOLEAN: "BOOL" > | <#TYPE_CLOB: "CLOB"> | <#TYPE_ENUM: "ENUM" > | <#TYPE_MAP: "MAP" > | <#TYPE_DECIMAL: "DECIMAL" | "NUMBER" | "NUMERIC" > | <#TYPE_TINYINT: "TINYINT" | "INT1" > | <#TYPE_SMALLINT: "SMALLINT" | "INT2" | "SHORT" > - | <#TYPE_INTEGER: ( "INTEGER" | "INT" | "INT4" | | ) > + | <#TYPE_INTEGER: ( "INTEGER" | "INT" | "INT4" ) > | <#TYPE_BIGINT: "BIGINT" | "INT8" | "LONG" > | <#TYPE_HUGEINT: "HUGEINT" > | <#TYPE_UTINYINT: "UTINYINT" > @@ -2365,7 +2367,7 @@ TOKEN : /* Data Types */ | <#TYPE_UHUGEINT: "UHUGEINT" > | <#TYPE_REAL: "REAL" | "FLOAT4" | "FLOAT"> | <#TYPE_DOUBLE: "DOUBLE" | "PRECISION" | "FLOAT8" | "FLOAT64"> - | <#TYPE_VARCHAR: "NVARCHAR" | "VARCHAR" | "NCHAR" | | "BPCHAR" | "TEXT" | "STRING" | | "VARYING"> + | <#TYPE_VARCHAR: "NVARCHAR" | "VARCHAR" | "NCHAR" | "BPCHAR" | "TEXT" | "STRING" | "VARYING"> | <#TYPE_TIME: "TIMETZ" > | <#TYPE_TIMESTAMP: "TIMESTAMP_NS" | "TIMESTAMP_MS" | "TIMESTAMP_S" > @@ -5756,7 +5758,8 @@ String SetOperationModifier(): [ ( tk= | tk="DISTINCT") { modifier+=tk.image; } ] { modifier+= " BY NAME"; } [ - "MATCHING" { modifier+= " MATCHING"; } + LOOKAHEAD({ isKeywordAhead("MATCHING") }) + ContextualKeyword("MATCHING") { modifier+= " MATCHING"; } "(" identifier = RelObjectName() { modifier+="(" + identifier; } ("," identifier = RelObjectName() { modifier+=", " + identifier; })* @@ -15526,14 +15529,14 @@ AlterSystemStatement AlterSystemStatement(): | ( ( - "DISTRIBUTED" "RECOVERY" { operation = AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY; } + ContextualKeyword("DISTRIBUTED") ContextualKeyword("RECOVERY") { operation = AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY; } | { operation = AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY; } ) ) | ( ( - "DISTRIBUTED" "RECOVERY" { operation = AlterSystemOperation.DISABLE_DISTRIBUTED_RECOVERY; } + ContextualKeyword("DISTRIBUTED") ContextualKeyword("RECOVERY") { operation = AlterSystemOperation.DISABLE_DISTRIBUTED_RECOVERY; } | { operation = AlterSystemOperation.DISABLE_RESTRICTED_SESSION; } ) ) @@ -15689,6 +15692,17 @@ Comment Comment(): } } +/** Consumes a keyword that remains an ordinary identifier outside this grammar context. */ +void ContextualKeyword(String expected): +{ Token keyword; } +{ + keyword= { + if (!expected.equalsIgnoreCase(keyword.image)) { + throw new ParseException("Expected " + expected + " but found " + keyword.image); + } + } +} + void AccessKeyword(String expected): { Token token; } { (token= | token=) { requireAccessSyntax(expected.equalsIgnoreCase(token.image), "Expected " + expected); } } diff --git a/src/test/java/net/sf/jsqlparser/parser/ContextualTokenTest.java b/src/test/java/net/sf/jsqlparser/parser/ContextualTokenTest.java new file mode 100644 index 000000000..61dc9aa3e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/ContextualTokenTest.java @@ -0,0 +1,78 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.parser; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.alter.AlterSystemStatement; +import net.sf.jsqlparser.statement.alter.AlterSystemOperation; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; + +class ContextualTokenTest { + @ParameterizedTest + @ValueSource(strings = { + "SELECT a FROM t UNION ALL BY NAME MATCHING (a) SELECT a FROM u", + "SELECT a FROM t UNION DISTINCT BY NAME MATCHING (a, b) SELECT a FROM u", + "ALTER SYSTEM ENABLE DISTRIBUTED RECOVERY", + "ALTER SYSTEM DISABLE DISTRIBUTED RECOVERY" + }) + void parsesPreviouslyShadowedContextualKeywords(String sql) throws Exception { + for (boolean complex : new boolean[] {false, true}) { + String parsed = assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withAllowComplexParsing(complex)).toString(); + assertEquals(parsed, CCJSqlParserUtil.parse(parsed).toString()); + } + } + + @Test + void preservesAlterSystemOperation() throws Exception { + assertEquals(AlterSystemOperation.ENABLE_DISTRIBUTED_RECOVERY, + ((AlterSystemStatement) CCJSqlParserUtil.parse( + "alter system enable distributed recovery")).getOperation()); + assertEquals(AlterSystemOperation.DISABLE_DISTRIBUTED_RECOVERY, + ((AlterSystemStatement) CCJSqlParserUtil.parse( + "alter system disable distributed recovery")).getOperation()); + } + + @ParameterizedTest + @ValueSource(strings = {"MATCHING", "DISTRIBUTED", "RECOVERY"}) + void keepsContextualWordsUsableAsIdentifiers(String word) throws Exception { + assertEquals(CCJSqlParserConstants.S_IDENTIFIER, + CCJSqlParserUtil.newParser(word).getNextToken().kind); + assertSqlCanBeParsedAndDeparsed("SELECT " + word + ", " + word + + "(a) FROM " + word + " AS x", true); + } + + @ParameterizedTest + @ValueSource( + strings = {"BYTES", "BINARY", "BOOLEAN", "UNSIGNED", "SIGNED", "CHARACTER", "CHAR"}) + void retainsDedicatedTypeTokensAndTypeParsing(String type) throws Exception { + assertNotEquals(CCJSqlParserConstants.DATA_TYPE, + CCJSqlParserUtil.newParser(type).getNextToken().kind); + assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (c " + type + ")", true); + assertSqlCanBeParsedAndDeparsed("SELECT CAST(c AS " + type + ") FROM t", true); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER SYSTEM ENABLE other RECOVERY", + "ALTER SYSTEM DISABLE DISTRIBUTED other", + "ALTER SYSTEM ENABLE DISTRIBUTED", + "SELECT a FROM t UNION ALL BY NAME other (a) SELECT a FROM u", + "SELECT a FROM t UNION ALL BY NAME MATCHING () SELECT a FROM u" + }) + void rejectsWrongContextualKeywords(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } +}