This is an automated email from the ASF dual-hosted git repository.
mbudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 1ae39588d5 [CALCITE-6988] DuckDB dialect implementation
1ae39588d5 is described below
commit 1ae39588d53268811470b8451713d2647c17fca3
Author: Yu Xu <[email protected]>
AuthorDate: Mon Apr 28 14:56:32 2025 +0800
[CALCITE-6988] DuckDB dialect implementation
---
.../java/org/apache/calcite/sql/SqlDialect.java | 3 +
.../apache/calcite/sql/SqlDialectFactoryImpl.java | 3 +
.../calcite/sql/dialect/DuckDBSqlDialect.java | 162 +++++++++++++++++++++
.../calcite/rel/rel2sql/RelToSqlConverterTest.java | 96 ++++++++++--
4 files changed, 251 insertions(+), 13 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
index e6525a1d21..2902bf1421 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
@@ -277,6 +277,8 @@ public static DatabaseProduct getProduct(
return DatabaseProduct.CLICKHOUSE;
case "DBMS:CLOUDSCAPE":
return DatabaseProduct.DERBY;
+ case "DUCKDB":
+ return DatabaseProduct.DUCKDB;
case "EXASOL":
return DatabaseProduct.EXASOL;
case "FIREBOLT":
@@ -1427,6 +1429,7 @@ public enum DatabaseProduct {
ORACLE("Oracle", "\"", NullCollation.HIGH),
DERBY("Apache Derby", null, NullCollation.HIGH),
DB2("IBM DB2", null, NullCollation.HIGH),
+ DUCKDB("DUCKDB", null, NullCollation.LAST),
DORIS("Doris", "`", NullCollation.LOW),
EXASOL("Exasol", "\"", NullCollation.LOW),
FIREBIRD("Firebird", null, NullCollation.HIGH),
diff --git
a/core/src/main/java/org/apache/calcite/sql/SqlDialectFactoryImpl.java
b/core/src/main/java/org/apache/calcite/sql/SqlDialectFactoryImpl.java
index 9e845d0821..1553a23bbb 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlDialectFactoryImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlDialectFactoryImpl.java
@@ -24,6 +24,7 @@
import org.apache.calcite.sql.dialect.Db2SqlDialect;
import org.apache.calcite.sql.dialect.DerbySqlDialect;
import org.apache.calcite.sql.dialect.DorisSqlDialect;
+import org.apache.calcite.sql.dialect.DuckDBSqlDialect;
import org.apache.calcite.sql.dialect.ExasolSqlDialect;
import org.apache.calcite.sql.dialect.FirebirdSqlDialect;
import org.apache.calcite.sql.dialect.FireboltSqlDialect;
@@ -185,6 +186,8 @@ public class SqlDialectFactoryImpl implements
SqlDialectFactory {
return Db2SqlDialect.DEFAULT;
case DERBY:
return DerbySqlDialect.DEFAULT;
+ case DUCKDB:
+ return DuckDBSqlDialect.DEFAULT;
case DORIS:
return DorisSqlDialect.DEFAULT;
case EXASOL:
diff --git
a/core/src/main/java/org/apache/calcite/sql/dialect/DuckDBSqlDialect.java
b/core/src/main/java/org/apache/calcite/sql/dialect/DuckDBSqlDialect.java
new file mode 100644
index 0000000000..e7d3333ba3
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/dialect/DuckDBSqlDialect.java
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.sql.dialect;
+
+import org.apache.calcite.avatica.util.TimeUnitRange;
+import org.apache.calcite.config.NullCollation;
+import org.apache.calcite.rel.type.RelDataTypeSystem;
+import org.apache.calcite.rel.type.RelDataTypeSystemImpl;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlDialect;
+import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.SqlWriter;
+import org.apache.calcite.sql.fun.SqlLibraryOperators;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+/**
+ * A <code>SqlDialect</code> implementation for the DuckDB database.
+ */
+public class DuckDBSqlDialect extends SqlDialect {
+ public static final RelDataTypeSystem TYPE_SYSTEM =
+ new RelDataTypeSystemImpl() {
+
+ // We can refer to document of DuckDB 1.2.x:
+ //
https://duckdb.org/docs/stable/sql/data_types/numeric#fixed-point-decimals
+ @Override public int getMaxPrecision(SqlTypeName typeName) {
+ switch (typeName) {
+ case DECIMAL:
+ return 38;
+ default:
+ return super.getMaxPrecision(typeName);
+ }
+ }
+
+ @Override public int getMaxScale(SqlTypeName typeName) {
+ switch (typeName) {
+ case DECIMAL:
+ return 38;
+ default:
+ return super.getMaxScale(typeName);
+ }
+ }
+
+ @Override public int getMaxNumericScale() {
+ return getMaxScale(SqlTypeName.DECIMAL);
+ }
+ };
+
+ public static final SqlDialect.Context DEFAULT_CONTEXT =
SqlDialect.EMPTY_CONTEXT
+ .withDatabaseProduct(SqlDialect.DatabaseProduct.DUCKDB)
+ .withIdentifierQuoteString("\"")
+ // Refer to document:
https://duckdb.org/docs/stable/sql/query_syntax/orderby.html
+ .withNullCollation(NullCollation.LAST)
+ .withDataTypeSystem(TYPE_SYSTEM);
+
+ public static final SqlDialect DEFAULT = new
DuckDBSqlDialect(DEFAULT_CONTEXT);
+
+ /** Creates a DuckDBSqlDialect. */
+ public DuckDBSqlDialect(SqlDialect.Context context) {
+ super(context);
+ }
+
+ @Override public boolean supportsApproxCountDistinct() {
+ return true;
+ }
+
+ @Override public boolean supportsAliasedValues() {
+ return false;
+ }
+
+ @Override public void unparseCall(SqlWriter writer, SqlCall call,
+ int leftPrec, int rightPrec) {
+ switch (call.getKind()) {
+ case MAP_VALUE_CONSTRUCTOR:
+ writer.keyword(call.getOperator().getName());
+ final SqlWriter.Frame mapFrame = writer.startList("{", "}");
+ for (int i = 0; i < call.operandCount(); i++) {
+ String sep = i % 2 == 0 ? "," : ":";
+ writer.sep(sep);
+ call.operand(i).unparse(writer, leftPrec, rightPrec);
+ }
+ writer.endList(mapFrame);
+ break;
+ case FLOOR:
+ if (call.operandCount() != 2) {
+ super.unparseCall(writer, call, leftPrec, rightPrec);
+ return;
+ }
+ unparseFloor(writer, call);
+ break;
+ case CHAR_LENGTH:
+ SqlCall lengthCall = SqlLibraryOperators.LENGTH
+ .createCall(SqlParserPos.ZERO, call.getOperandList());
+ super.unparseCall(writer, lengthCall, leftPrec, rightPrec);
+ break;
+ default:
+ super.unparseCall(writer, call, leftPrec, rightPrec);
+ }
+ }
+
+ private static void unparseFloor(SqlWriter writer, SqlCall call) {
+ SqlLiteral node = call.operand(1);
+ TimeUnitRange unit = node.getValueAs(TimeUnitRange.class);
+
+ String format;
+ switch (unit) {
+ case YEAR:
+ format = "year";
+ break;
+ case MONTH:
+ format = "month";
+ break;
+ case WEEK:
+ format = "week";
+ break;
+ case DAY:
+ format = "day";
+ break;
+ case HOUR:
+ format = "hour";
+ break;
+ case MINUTE:
+ format = "minute";
+ break;
+ case SECOND:
+ format = "second";
+ break;
+ case MILLISECOND:
+ format = "milliseconds";
+ break;
+ case MICROSECOND:
+ format = "microseconds";
+ break;
+ default:
+ throw new AssertionError("DUCKDB does not support FLOOR for time unit: "
+ + unit);
+ }
+
+ // Refer to document:
https://duckdb.org/docs/stable/sql/functions/date#date_truncpart-date
+ writer.print("DATETRUNC");
+ SqlWriter.Frame frame = writer.startList("(", ")");
+ writer.print("'" + format + "'");
+ writer.sep(",", true);
+ call.operand(0).unparse(writer, 0, 0);
+ writer.endList(frame);
+ }
+
+}
diff --git
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index 77a41f894d..66766eb0b9 100644
---
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -58,6 +58,7 @@
import org.apache.calcite.sql.dialect.BigQuerySqlDialect;
import org.apache.calcite.sql.dialect.CalciteSqlDialect;
import org.apache.calcite.sql.dialect.ClickHouseSqlDialect;
+import org.apache.calcite.sql.dialect.DuckDBSqlDialect;
import org.apache.calcite.sql.dialect.HiveSqlDialect;
import org.apache.calcite.sql.dialect.JethroDataSqlDialect;
import org.apache.calcite.sql.dialect.MssqlSqlDialect;
@@ -5351,52 +5352,92 @@ private void checkLiteral2(String expression, String
expected) {
/** Test case for
* <a
href="https://issues.apache.org/jira/browse/CALCITE-6949">[CALCITE-6949]
- * ClickHouse not support floor date to
SECOND/MILLISECOND/MICROSECOND/NANOSECOND</a>. */
- @Test void testFloorClickHouse() {
+ * ClickHouse not support floor date to
SECOND/MILLISECOND/MICROSECOND/NANOSECOND</a>,
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6988">[CALCITE-6988]
+ * DuckDB dialect implementation</a>.
+ * */
+ @Test void testFloorFunction() {
String query = "SELECT floor(\"hire_date\" TO YEAR) FROM \"employee\"";
String expectedClickHouse = "SELECT toStartOfYear(`hire_date`)\n"
+ "FROM `foodmart`.`employee`";
- sql(query).withClickHouse().ok(expectedClickHouse);
+ String expectedDuckDB = "SELECT DATETRUNC('year', \"hire_date\")\n"
+ + "FROM \"foodmart\".\"employee\"";
+ sql(query)
+ .withClickHouse().ok(expectedClickHouse)
+ .withDuckDB().ok(expectedDuckDB);
+
String query1 = "SELECT floor(\"hire_date\" TO MONTH) FROM \"employee\"";
String expectedClickHouse1 = "SELECT toStartOfMonth(`hire_date`)\n"
+ "FROM `foodmart`.`employee`";
- sql(query1).withClickHouse().ok(expectedClickHouse1);
+ String expectedDuckDB1 = "SELECT DATETRUNC('month', \"hire_date\")\n"
+ + "FROM \"foodmart\".\"employee\"";
+ sql(query1)
+ .withClickHouse().ok(expectedClickHouse1)
+ .withDuckDB().ok(expectedDuckDB1);
String query2 = "SELECT floor(\"hire_date\" TO WEEK) FROM \"employee\"";
String expectedClickHouse2 = "SELECT toMonday(`hire_date`)\n"
+ "FROM `foodmart`.`employee`";
- sql(query2).withClickHouse().ok(expectedClickHouse2);
+ String expectedDuckDB2 = "SELECT DATETRUNC('week', \"hire_date\")\n"
+ + "FROM \"foodmart\".\"employee\"";
+ sql(query2)
+ .withClickHouse().ok(expectedClickHouse2)
+ .withDuckDB().ok(expectedDuckDB2);
String query3 = "SELECT floor(\"hire_date\" TO DAY) FROM \"employee\"";
String expectedClickHouse3 = "SELECT toDate(`hire_date`)\n"
+ "FROM `foodmart`.`employee`";
- sql(query3).withClickHouse().ok(expectedClickHouse3);
+ String expectedDuckDB3 = "SELECT DATETRUNC('day', \"hire_date\")\n"
+ + "FROM \"foodmart\".\"employee\"";
+ sql(query3)
+ .withClickHouse().ok(expectedClickHouse3)
+ .withDuckDB().ok(expectedDuckDB3);
String query4 = "SELECT floor(\"hire_date\" TO HOUR) FROM \"employee\"";
String expectedClickHouse4 = "SELECT toStartOfHour(`hire_date`)\n"
+ "FROM `foodmart`.`employee`";
- sql(query4).withClickHouse().ok(expectedClickHouse4);
+ String expectedDuckDB4 = "SELECT DATETRUNC('hour', \"hire_date\")\n"
+ + "FROM \"foodmart\".\"employee\"";
+ sql(query4)
+ .withClickHouse().ok(expectedClickHouse4)
+ .withDuckDB().ok(expectedDuckDB4);
String query5 = "SELECT floor(\"hire_date\" TO MINUTE) FROM \"employee\"";
String expectedClickHouse5 = "SELECT toStartOfMinute(`hire_date`)\n"
+ "FROM `foodmart`.`employee`";
- sql(query5).withClickHouse().ok(expectedClickHouse5);
+ String expectedDuckDB5 = "SELECT DATETRUNC('minute', \"hire_date\")\n"
+ + "FROM \"foodmart\".\"employee\"";
+ sql(query5)
+ .withClickHouse().ok(expectedClickHouse5)
+ .withDuckDB().ok(expectedDuckDB5);
String query6 = "SELECT floor(\"hire_date\" TO SECOND) FROM \"employee\"";
String expectedClickHouse6 = "SELECT toStartOfSecond(`hire_date`)\n"
+ "FROM `foodmart`.`employee`";
- sql(query6).withClickHouse().ok(expectedClickHouse6);
+ String expectedDuckDB6 = "SELECT DATETRUNC('second', \"hire_date\")\n"
+ + "FROM \"foodmart\".\"employee\"";
+ sql(query6)
+ .withClickHouse().ok(expectedClickHouse6)
+ .withDuckDB().ok(expectedDuckDB6);
String query7 = "SELECT floor(\"hire_date\" TO MILLISECOND) FROM
\"employee\"";
String expectedClickHouse7 = "SELECT toStartOfMillisecond(`hire_date`)\n"
+ "FROM `foodmart`.`employee`";
- sql(query7).withClickHouse().ok(expectedClickHouse7);
+ String expectedDuckDB7 = "SELECT DATETRUNC('milliseconds',
\"hire_date\")\n"
+ + "FROM \"foodmart\".\"employee\"";
+ sql(query7)
+ .withClickHouse().ok(expectedClickHouse7)
+ .withDuckDB().ok(expectedDuckDB7);
String query8 = "SELECT floor(\"hire_date\" TO MICROSECOND) FROM
\"employee\"";
String expectedClickHouse8 = "SELECT toStartOfMicrosecond(`hire_date`)\n"
+ "FROM `foodmart`.`employee`";
- sql(query8).withClickHouse().ok(expectedClickHouse8);
+ String expectedDuckDB8 = "SELECT DATETRUNC('microseconds',
\"hire_date\")\n"
+ + "FROM \"foodmart\".\"employee\"";
+ sql(query8)
+ .withClickHouse().ok(expectedClickHouse8)
+ .withDuckDB().ok(expectedDuckDB8);
String query9 = "SELECT floor(\"hire_date\" TO NANOSECOND) FROM
\"employee\"";
String expectedClickHouse9 = "SELECT toStartOfNanosecond(`hire_date`)\n"
@@ -5948,6 +5989,18 @@ private void checkLiteral2(String expression, String
expected) {
.ok(expected);
}
+ @Test void testDuckDBDecimalPrecision() {
+ final String sql = "SELECT CAST(1.23 AS DECIMAL(38, 37))";
+ final String expected = "SELECT 1.2300000000000000000000000000000000000";
+ sql(sql).withDuckDBModifiedDecimalTypeSystem()
+ .ok(expected);
+
+ final String sql1 = "SELECT CAST('1.23000000000000000123' AS DECIMAL(38,
20))";
+ final String expected1 = "SELECT 1.23000000000000000123";
+ sql(sql1).withDuckDBModifiedDecimalTypeSystem()
+ .ok(expected1);
+ }
+
/** Test for <a
href="https://issues.apache.org/jira/browse/CALCITE-6974">[CALCITE-6974]
* Default typesystem has incorrect limits for DECIMAL for
Presto/MySQL/Phoenix</a>. */
@Test void testDecimalPrecision() {
@@ -7832,9 +7885,11 @@ private void checkLiteral2(String expression, String
expected) {
// since it seems to be used across more dialects.
final String expectedSnowflake = "SELECT LENGTH(\"brand_name\")\n"
+ "FROM \"foodmart\".\"product\"";
+ final String expectedDuckDB = expectedSnowflake;
Sql sql = sql(query).withLibrary(SqlLibrary.BIG_QUERY);
sql.withBigQuery().ok(expectedBigQuery);
sql.withSnowflake().ok(expectedSnowflake);
+ sql.withDuckDB().ok(expectedDuckDB);
}
/** Test case for
@@ -8411,6 +8466,8 @@ private void checkLiteral2(String expression, String
expected) {
final String expectedClickhouse = "SELECT UNIQ(`product_id`)\n"
+ "FROM `foodmart`.`product`";
final String expectedHive = "SELECT COUNT(DISTINCT `product_id`)\nFROM
`foodmart`.`product`";
+ final String expectedDuckDB = "SELECT
APPROX_COUNT_DISTINCT(\"product_id\")\n"
+ + "FROM \"foodmart\".\"product\"";
sql(query).ok(expectedExact)
.withHive().ok(expectedHive)
@@ -8423,7 +8480,8 @@ private void checkLiteral2(String expression, String
expected) {
.withStarRocks().ok(expectedStarRocksSql)
.withMssql().ok(expectedMssql)
.withPhoenix().ok(expectedPhoenix)
- .withClickHouse().ok(expectedClickhouse);
+ .withClickHouse().ok(expectedClickhouse)
+ .withDuckDB().ok(expectedDuckDB);
}
@Test void testRowValueExpression() {
@@ -9346,6 +9404,7 @@ private void checkLiteral2(String expression, String
expected) {
final String expectedHive = "SELECT MAP ('k1', 'v1', 'k2', 'v2')";
final String expectedDoris = "SELECT MAP ('k1', 'v1', 'k2', 'v2')";
final String expectedClickHouse = "SELECT map('k1', 'v1', 'k2', 'v2')";
+ final String expectedDuckDB = "SELECT MAP { 'k1' : 'v1', 'k2' : 'v2' }";
sql(query)
.withPresto().ok(expectedPresto)
.withTrino().ok(expectedTrino)
@@ -9353,7 +9412,8 @@ private void checkLiteral2(String expression, String
expected) {
.withDoris().ok(expectedDoris)
.withSpark().ok(expectedSpark)
.withHive().ok(expectedHive)
- .withClickHouse().ok(expectedClickHouse);
+ .withClickHouse().ok(expectedClickHouse)
+ .withDuckDB().ok(expectedDuckDB);
}
@Test void testMapValueConstructorWithArray() {
@@ -9885,6 +9945,10 @@ Sql withClickHouse() {
return dialect(DatabaseProduct.CLICKHOUSE.getDialect());
}
+ Sql withDuckDB() {
+ return dialect(DatabaseProduct.DUCKDB.getDialect());
+ }
+
Sql withDerby() {
return dialect(DatabaseProduct.DERBY.getDialect());
}
@@ -10061,6 +10125,12 @@ Sql withClickHouseModifiedDecimalTypeSystem() {
return dialect(clickHouseSqlDialect);
}
+ Sql withDuckDBModifiedDecimalTypeSystem() {
+ final DuckDBSqlDialect duckDBSqlDialect =
+ new DuckDBSqlDialect(DuckDBSqlDialect.DEFAULT_CONTEXT);
+ return dialect(duckDBSqlDialect);
+ }
+
Sql withPrestoModifiedDecimalTypeSystem() {
final PrestoSqlDialect prestoSqlDialect =
new PrestoSqlDialect(PrestoSqlDialect.DEFAULT_CONTEXT);