This is an automated email from the ASF dual-hosted git repository. jhyde pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/calcite.git
commit 161e220384dfbd85f896a95cac40cca23141583e Author: macroguo <[email protected]> AuthorDate: Mon Aug 7 21:46:24 2023 +0800 [CALCITE-5184] Support "LIMIT start, ALL" in MySQL conformance, equivalent to "OFFSET start" Add tests for LIMIT to sort.iq; add 'scott-mysql' and 'scott-lenient' Quidem data sources, similar to the 'scott' data source but with particular conformances. (Julian Hyde) Close apache/calcite#3352 Co-authored-by: Julian Hyde <[email protected]> Co-authored-by: macroguo <[email protected]> --- core/src/main/codegen/templates/Parser.jj | 16 +- .../apache/calcite/runtime/CalciteResource.java | 4 +- .../calcite/runtime/CalciteResource.properties | 2 +- .../org/apache/calcite/test/CoreQuidemTest.java | 21 +++ core/src/test/resources/sql/sort.iq | 165 +++++++++++++++++++++ site/_docs/reference.md | 2 +- .../apache/calcite/sql/parser/SqlParserTest.java | 45 +++++- 7 files changed, 240 insertions(+), 15 deletions(-) diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index 0a11b28426..6a4991fc1b 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -758,18 +758,28 @@ void FetchClause(SqlNode[] offsetFetch) : */ void LimitClause(Span s, SqlNode[] offsetFetch) : { + final String error; } { // Postgres-style syntax. "LIMIT ... OFFSET ..." <LIMIT> { s.add(this); } ( - // MySQL-style syntax. "LIMIT start, count" + // MySQL-style syntax. "LIMIT start, count" or "LIMIT start, ALL" LOOKAHEAD(2) offsetFetch[0] = UnsignedNumericLiteralOrParam() - <COMMA> offsetFetch[1] = UnsignedNumericLiteralOrParam() { + <COMMA> + ( + offsetFetch[1] = UnsignedNumericLiteralOrParam() { + error = "count"; + } + | + <ALL> { + error = "ALL"; + } + ) { if (!this.conformance.isLimitStartCountAllowed()) { throw SqlUtil.newContextException(s.end(this), - RESOURCE.limitStartCountNotAllowed()); + RESOURCE.limitStartCountOrAllNotAllowed(error)); } } | diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java index 1b267bc598..c09c309ccc 100644 --- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java +++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java @@ -39,8 +39,8 @@ public interface CalciteResource { @BaseMessage("Percent remainder ''%'' is not allowed under the current SQL conformance level") ExInst<CalciteException> percentRemainderNotAllowed(); - @BaseMessage("''LIMIT start, count'' is not allowed under the current SQL conformance level") - ExInst<CalciteException> limitStartCountNotAllowed(); + @BaseMessage("''LIMIT start, {0}'' is not allowed under the current SQL conformance level") + ExInst<CalciteException> limitStartCountOrAllNotAllowed(String a0); @BaseMessage("''OFFSET start LIMIT count'' is not allowed under the current SQL conformance level") ExInst<CalciteException> offsetLimitNotAllowed(); diff --git a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties index ca9003edfd..8dfd6fb9f3 100644 --- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties +++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties @@ -21,7 +21,7 @@ ParserContext=line {0,number,#}, column {1,number,#} BangEqualNotAllowed=Bang equal ''!='' is not allowed under the current SQL conformance level PercentRemainderNotAllowed=Percent remainder ''%'' is not allowed under the current SQL conformance level -LimitStartCountNotAllowed=''LIMIT start, count'' is not allowed under the current SQL conformance level +LimitStartCountOrAllNotAllowed=''LIMIT start, {0}'' is not allowed under the current SQL conformance level OffsetLimitNotAllowed=''OFFSET start LIMIT count'' is not allowed under the current SQL conformance level ApplyNotAllowed=APPLY operator is not allowed under the current SQL conformance level ValueNotAllowed=VALUE is not allowed under the current SQL conformance level diff --git a/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java b/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java index 6814907db4..9f498b7df2 100644 --- a/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java +++ b/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java @@ -17,6 +17,7 @@ package org.apache.calcite.test; import org.apache.calcite.config.CalciteConnectionProperty; +import org.apache.calcite.sql.validate.SqlConformanceEnum; import net.hydromatic.quidem.Quidem; @@ -64,6 +65,26 @@ class CoreQuidemTest extends QuidemTest { ExtensionDdlExecutor.class.getName() + "#PARSER_FACTORY") .with(CalciteAssert.Config.SCOTT) .connect(); + case "scott-lenient": + // Same as "scott", but uses LENIENT conformance. + // TODO: add a way to change conformance without defining a new + // connection + return CalciteAssert.that() + .with(CalciteConnectionProperty.PARSER_FACTORY, + ExtensionDdlExecutor.class.getName() + "#PARSER_FACTORY") + .with(CalciteConnectionProperty.CONFORMANCE, + SqlConformanceEnum.LENIENT) + .with(CalciteAssert.Config.SCOTT) + .connect(); + case "scott-mysql": + // Same as "scott", but uses MySQL conformance. + return CalciteAssert.that() + .with(CalciteConnectionProperty.PARSER_FACTORY, + ExtensionDdlExecutor.class.getName() + "#PARSER_FACTORY") + .with(CalciteConnectionProperty.CONFORMANCE, + SqlConformanceEnum.MYSQL_5) + .with(CalciteAssert.Config.SCOTT) + .connect(); default: return super.connect(name, reference); } diff --git a/core/src/test/resources/sql/sort.iq b/core/src/test/resources/sql/sort.iq index 38ce22de5b..4e11362b24 100644 --- a/core/src/test/resources/sql/sort.iq +++ b/core/src/test/resources/sql/sort.iq @@ -138,6 +138,171 @@ EnumerableSort(sort0=[$0], sort1=[$1], dir0=[DESC], dir1=[ASC]) EnumerableTableScan(table=[[scott, DEPT]]) !plan +# Return the 2nd and 3rd department +select * +from dept +order by deptno limit 2 offset 1; ++--------+----------+---------+ +| DEPTNO | DNAME | LOC | ++--------+----------+---------+ +| 20 | RESEARCH | DALLAS | +| 30 | SALES | CHICAGO | ++--------+----------+---------+ +(2 rows) + +!ok + +# Return the 2nd and 3rd department (using ansi FETCH syntax) +select * +from dept +order by deptno offset 1 fetch next 2 rows only; ++--------+----------+---------+ +| DEPTNO | DNAME | LOC | ++--------+----------+---------+ +| 20 | RESEARCH | DALLAS | +| 30 | SALES | CHICAGO | ++--------+----------+---------+ +(2 rows) + +!ok + +# Return the 1st and 2nd departments +select * +from dept +order by deptno limit 2; ++--------+------------+----------+ +| DEPTNO | DNAME | LOC | ++--------+------------+----------+ +| 10 | ACCOUNTING | NEW YORK | +| 20 | RESEARCH | DALLAS | ++--------+------------+----------+ +(2 rows) + +!ok + +# Return the 1st and 2nd departments (using ansi FETCH syntax) +select * +from dept +order by deptno fetch next 2 rows only; ++--------+------------+----------+ +| DEPTNO | DNAME | LOC | ++--------+------------+----------+ +| 10 | ACCOUNTING | NEW YORK | +| 20 | RESEARCH | DALLAS | ++--------+------------+----------+ +(2 rows) + +!ok + +# Return 2nd and subsequent departments +select * +from dept +order by deptno offset 1; ++--------+------------+---------+ +| DEPTNO | DNAME | LOC | ++--------+------------+---------+ +| 20 | RESEARCH | DALLAS | +| 30 | SALES | CHICAGO | +| 40 | OPERATIONS | BOSTON | ++--------+------------+---------+ +(3 rows) + +!ok + +# No limit +select * +from dept +order by deptno; ++--------+------------+----------+ +| DEPTNO | DNAME | LOC | ++--------+------------+----------+ +| 10 | ACCOUNTING | NEW YORK | +| 20 | RESEARCH | DALLAS | +| 30 | SALES | CHICAGO | +| 40 | OPERATIONS | BOSTON | ++--------+------------+----------+ +(4 rows) + +!ok + +!use scott-lenient + +# Return the 2nd and 3rd department, using 'offset .. limit', +# which is only allowed in lenient conformance (and Trino) +select * +from dept +order by deptno offset 1 limit 2; ++--------+----------+---------+ +| DEPTNO | DNAME | LOC | ++--------+----------+---------+ +| 20 | RESEARCH | DALLAS | +| 30 | SALES | CHICAGO | ++--------+----------+---------+ +(2 rows) + +!ok + +!use scott-mysql + +# Returns 2nd and 3rd department, expressed using MySQL syntax +select * +from dept +order by deptno limit 1, 2; ++--------+----------+---------+ +| DEPTNO | DNAME | LOC | ++--------+----------+---------+ +| 20 | RESEARCH | DALLAS | +| 30 | SALES | CHICAGO | ++--------+----------+---------+ +(2 rows) + +!ok + +# Return the 1st and 2nd departments +select * +from dept +order by deptno limit 2; ++--------+------------+----------+ +| DEPTNO | DNAME | LOC | ++--------+------------+----------+ +| 10 | ACCOUNTING | NEW YORK | +| 20 | RESEARCH | DALLAS | ++--------+------------+----------+ +(2 rows) + +!ok + +# Return 2nd and subsequent departments, expressed using MySQL syntax +select * +from dept +order by deptno limit 1, all; ++--------+------------+---------+ +| DEPTNO | DNAME | LOC | ++--------+------------+---------+ +| 20 | RESEARCH | DALLAS | +| 30 | SALES | CHICAGO | +| 40 | OPERATIONS | BOSTON | ++--------+------------+---------+ +(3 rows) + +!ok + +# No limit, expressed using MySQL syntax +select * +from dept +order by deptno limit all; ++--------+------------+----------+ +| DEPTNO | DNAME | LOC | ++--------+------------+----------+ +| 10 | ACCOUNTING | NEW YORK | +| 20 | RESEARCH | DALLAS | +| 30 | SALES | CHICAGO | +| 40 | OPERATIONS | BOSTON | ++--------+------------+----------+ +(4 rows) + +!ok + !use post # [CALCITE-603] WITH ... ORDER BY cannot find table diff --git a/site/_docs/reference.md b/site/_docs/reference.md index cf521bc0ce..abd151131f 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -192,7 +192,7 @@ query: | query INTERSECT [ ALL | DISTINCT ] query } [ ORDER BY orderItem [, orderItem ]* ] - [ LIMIT { [ start, ] count | ALL } ] + [ LIMIT [ start, ] { count | ALL } ] [ OFFSET start { ROW | ROWS } ] [ FETCH { FIRST | NEXT } [ count ] { ROW | ROWS } ONLY ] diff --git a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java index d78fab4324..c23377c0c3 100644 --- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java +++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java @@ -36,6 +36,7 @@ import org.apache.calcite.sql.test.SqlTestFactory; import org.apache.calcite.sql.test.SqlTests; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.calcite.sql.util.SqlShuttle; +import org.apache.calcite.sql.validate.SqlConformance; import org.apache.calcite.sql.validate.SqlConformanceEnum; import org.apache.calcite.test.DiffTestCase; import org.apache.calcite.test.IntervalTest; @@ -3730,11 +3731,6 @@ public class SqlParserTest { sql("select a from foo limit 2,3 ^fetch^ next 4 rows only") .withConformance(SqlConformanceEnum.LENIENT) .fails("(?s).*Encountered \"fetch\" at line 1.*"); - - // "limit start, all" is not valid - sql("select a from foo limit 2, ^all^") - .withConformance(SqlConformanceEnum.LENIENT) - .fails("(?s).*Encountered \"all\" at line 1.*"); } /** Test case for @@ -3768,11 +3764,44 @@ public class SqlParserTest { sql("select a from foo offset 2 limit 3 ^fetch^ next 4 rows only") .withConformance(SqlConformanceEnum.LENIENT) .fails("(?s).*Encountered \"fetch\" at line 1.*"); + } - // "limit start, all" is not valid - sql("select a from foo offset 1 limit 2, ^all^") + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-5184">[CALCITE-5184] + * Support "LIMIT start, ALL" (in MySQL conformance)</a>. + * + * @see SqlConformance#isLimitStartCountAllowed() */ + @Test void testLimitStartAll() { + String expected = "SELECT `A`\n" + + "FROM `FOO`\n" + + "OFFSET 2 ROWS"; + sql("select a from foo ^limit 2, all^") + .withConformance(SqlConformanceEnum.DEFAULT) + .fails("'LIMIT start, ALL' is not allowed under the " + + "current SQL conformance level") + .withConformance(SqlConformanceEnum.MYSQL_5) + .ok(expected) .withConformance(SqlConformanceEnum.LENIENT) - .fails("(?s).*Encountered \"all\" at line 1.*"); + .ok(expected); + + // 'limit 2, all' will override 'offset 1' (if allowed by conformance) + sql("select a from foo ^offset 1 limit 2, all^") + .withConformance(SqlConformanceEnum.DEFAULT) + .fails("'LIMIT start, ALL' is not allowed under the " + + "current SQL conformance level") + .withConformance(SqlConformanceEnum.MYSQL_5) + .fails("'OFFSET start LIMIT count' is not allowed under the " + + "current SQL conformance level") + .withConformance(SqlConformanceEnum.LENIENT) + .ok(expected); + + // 'offset 1' will override 'limit 2, all' + String expected2 = "SELECT `A`\n" + + "FROM `FOO`\n" + + "OFFSET 1 ROWS"; + sql("select a from foo limit 2, all offset 1") + .withConformance(SqlConformanceEnum.LENIENT) + .ok(expected2); } @Test void testSqlInlineComment() {
