From 8c023a204ad105606fddd66bba45824d67412518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Sun, 11 Oct 2026 20:50:39 +0800 Subject: [PATCH] fix: support SAMPLE and EXTEND as unquoted identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 付典 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 11 +- .../sf/jsqlparser/statement/KeywordsTest.java | 112 +++++++++++++++++- 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6a82cb3bb..f9907feab 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1533,6 +1533,13 @@ public class CCJSqlParser extends AbstractJSqlParser { // AS always starts an alias if (kind == K_AS) return true; + // SAMPLE followed by a size or BLOCK starts a sampling clause. + if (kind == K_SAMPLE && (getToken(2).kind == OPENING_BRACKET + || getToken(2).kind == K_BLOCK || getToken(2).kind == S_LONG + || getToken(2).kind == S_DOUBLE)) { + return false; + } + // String-literal alias: SELECT col 'myAlias' if (kind == S_CHAR_LITERAL) return true; @@ -2571,6 +2578,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2759,6 +2767,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2894,7 +2903,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */ | | /* Salesforce SOQL */ | -| | | | @@ -2952,7 +2960,6 @@ TOKEN: /* Reserved SQL Keywords and structural tokens */ | | | -| | | | diff --git a/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java b/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java index 9e2c14ce7..da4acca92 100644 --- a/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/KeywordsTest.java @@ -9,15 +9,26 @@ */ package net.sf.jsqlparser.statement; +import java.util.stream.Stream; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.TernaryExpression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.statement.piped.FromQuery; +import net.sf.jsqlparser.statement.piped.SelectPipeOperator; +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.select.PlainSelect; +import net.sf.jsqlparser.statement.select.SampleClause; +import net.sf.jsqlparser.statement.select.WithItem; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; - -import java.util.stream.Stream; +import org.junit.jupiter.params.provider.ValueSource; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.*; /** * Verifies that all non-reserved keywords can be used as unquoted identifiers (schema, table, @@ -62,4 +73,101 @@ public void testGroupsAsIdentifier() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SELECT a groups FROM mytable", true); assertSqlCanBeParsedAndDeparsed("CREATE TABLE mytable (groups INT)", true); } + + // sample and extend are non-reserved in the common dialects and reach the + // parser as ordinary table and column names in downstream rewriting tools. + @Test + public void testSampleExtendAsIdentifier() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT sample, extend FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("SELECT mytable.extend FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable WHERE extend = 'x'", true); + assertSqlCanBeParsedAndDeparsed("UPDATE mytable SET sample = 1, extend = 'x' WHERE id = 1", + true); + assertSqlCanBeParsedAndDeparsed("INSERT INTO mytable (sample, extend) VALUES (1, 'x')", + true); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM sample", true); + assertSqlCanBeParsedAndDeparsed("DELETE FROM sample WHERE extend = 'x'", true); + assertSqlCanBeParsedAndDeparsed("INSERT INTO sample (extend) VALUES ('x')", true); + assertSqlCanBeParsedAndDeparsed("SELECT a AS sample, b AS extend FROM mytable", true); + } + + @ParameterizedTest + @ValueSource(strings = {"sample", "extend", "SaMpLe", "ExTeNd"}) + void testSampleExtendImplicitAliases(String keyword) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT a " + keyword + " FROM mytable " + keyword, true); + assertEquals(keyword, select.getSelectItem(0).getAlias().getName()); + assertEquals(keyword, select.getFromItem().getAlias().getName()); + assertEquals("a", assertInstanceOf(Column.class, + select.getSelectItem(0).getExpression()).getColumnName()); + } + + @ParameterizedTest + @ValueSource(strings = {"sample", "extend", "SaMpLe", "ExTeNd"}) + void testSampleExtendCte(String keyword) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "WITH " + keyword + "(x) AS (SELECT 1) SELECT x FROM " + keyword, true); + WithItem withItem = select.getWithItemsList().get(0); + assertEquals(keyword, withItem.getAliasName()); + assertNotNull(withItem.getParenthesedStatement()); + assertNull(withItem.getExpression()); + assertEquals(keyword, assertInstanceOf(Table.class, select.getFromItem()).getName()); + assertSqlCanBeParsedAndDeparsed( + "WITH RECURSIVE " + keyword + "(x) AS (SELECT 1) SELECT * FROM " + keyword, true); + } + + @ParameterizedTest + @ValueSource(strings = {"sample", "extend", "SaMpLe", "ExTeNd"}) + void testSampleExtendExpressionContexts(String keyword) throws JSQLParserException { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT ALL " + keyword + " FROM mytable"); + assertEquals(keyword, assertInstanceOf(Column.class, + select.getSelectItem(0).getExpression()).getColumnName()); + assertSqlCanBeParsedAndDeparsed("SELECT CASE " + keyword + + " WHEN 1 THEN " + keyword + " ELSE 0 END FROM mytable", true); + assertSqlCanBeParsedAndDeparsed("UPDATE " + keyword + " SET a = 1", true); + } + + @ParameterizedTest + @ValueSource(strings = {"sample", "extend", "SaMpLe", "ExTeNd"}) + void testSampleExtendTernaryOperand(String keyword) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT flag ? " + keyword + " : 0 FROM mytable", true); + TernaryExpression ternary = assertInstanceOf(TernaryExpression.class, + select.getSelectItem(0).getExpression()); + assertEquals(keyword, assertInstanceOf(Column.class, ternary.getThenExpression()) + .getColumnName()); + } + + @ParameterizedTest + @ValueSource(strings = {"SAMPLE (10)", "SAMPLE BLOCK (10) SEED (1)", + "SAMPLE 0.1 OFFSET 1000", "USING SAMPLE 10 PERCENT (BERNOULLI)", + "TABLESAMPLE SYSTEM (10)"}) + void testSampleClauseStaysClause(String clause) throws JSQLParserException { + PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM mytable " + clause, true); + Table table = assertInstanceOf(Table.class, select.getFromItem()); + assertNull(table.getAlias()); + SampleClause sample = table.getSampleClause(); + assertNotNull(sample); + assertEquals(" " + clause, sample.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"sample", "extend", "SaMpLe", "ExTeNd"}) + void testSampleExtendPipeExpression(String keyword) throws JSQLParserException { + FromQuery query = (FromQuery) assertSqlCanBeParsedAndDeparsed( + "FROM " + keyword + " |> EXTEND " + keyword + " AS x", true); + SelectPipeOperator operator = assertInstanceOf(SelectPipeOperator.class, + query.getPipeOperators().get(0)); + assertEquals("EXTEND", operator.getOperatorName()); + assertEquals(keyword, assertInstanceOf(Column.class, + operator.getSelectItems().get(0).getExpression()).getColumnName()); + assertSqlCanBeParsedAndDeparsed( + "FROM mytable |> EXTEND " + keyword + "(x) AS x", true); + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse( + "SELECT ALL " + keyword + "(x) FROM mytable"); + assertEquals(keyword, assertInstanceOf(Function.class, + select.getSelectItem(0).getExpression()).getName()); + } }