This is an automated email from the ASF dual-hosted git repository.
gianm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 25de11864a2 feat: PostgreSQL-style :: cast operator. (#20702)
25de11864a2 is described below
commit 25de11864a25d0cba7b12224a939a997a0f3289d
Author: Gian Merlino <[email protected]>
AuthorDate: Tue Oct 27 14:07:36 2026 -0700
feat: PostgreSQL-style :: cast operator. (#20702)
This patch adds cast syntax like x::BIGINT as shorthand for
CAST(x AS BIGINT).
---
docs/querying/sql-data-types.md | 4 +-
docs/querying/sql-functions.md | 2 +-
docs/querying/sql-scalar.md | 2 +-
sql/src/main/codegen/config.fmpp | 12 +
sql/src/main/codegen/includes/common.ftl | 30 ++
.../sql/calcite/planner/DruidSqlValidator.java | 71 +++-
.../druid/sql/calcite/CalciteInsertDmlTest.java | 23 ++
.../apache/druid/sql/calcite/CalciteQueryTest.java | 455 +++++++++++++++++++++
8 files changed, 592 insertions(+), 7 deletions(-)
diff --git a/docs/querying/sql-data-types.md b/docs/querying/sql-data-types.md
index 8d95ae1d0ca..1ca4103492a 100644
--- a/docs/querying/sql-data-types.md
+++ b/docs/querying/sql-data-types.md
@@ -60,7 +60,7 @@ The following table describes how Druid maps SQL types onto
native types when ru
|TINYINT|LONG|`0`||
|SMALLINT|LONG|`0`||
|INTEGER|LONG|`0`||
-|BIGINT|LONG|`0`|Druid LONG columns (except `__time`) are reported as BIGINT.
`LONG` is supported only as a `CAST` alias for `BIGINT`, for example `CAST(x AS
LONG)`, matching the name Druid's native engine uses for this type.|
+|BIGINT|LONG|`0`|Druid LONG columns (except `__time`) are reported as BIGINT.
`LONG` is supported only as a cast alias for `BIGINT`, for example `CAST(x AS
LONG)` or `x::LONG`, matching the name Druid's native engine uses for this
type.|
|TIMESTAMP|LONG|`0`, meaning 1970-01-01 00:00:00 UTC|Druid's `__time` column
is reported as TIMESTAMP. Casts between string and timestamp types assume
standard SQL formatting, such as `2000-01-02 03:04:05`, not ISO 8601
formatting. For handling other formats, use one of the [time
functions](sql-scalar.md#date-and-time-functions).|
|DATE|LONG|`0`, meaning 1970-01-01|Casting TIMESTAMP to DATE rounds down the
timestamp to the nearest day. Casts between string and date types assume
standard SQL formatting—for example, `2000-01-02`. For handling other
formats, use one of the [time
functions](sql-scalar.md#date-and-time-functions).|
|ARRAY|ARRAY|`NULL`|Druid native array types work as SQL arrays, and
multi-value strings can be converted to arrays. See [Arrays](#arrays) for more
information.|
@@ -77,6 +77,8 @@ For casts between two SQL types, the behavior depends on the
runtime type:
If a value cannot be cast to the target type, as in `CAST('foo' AS BIGINT)`,
Druid a substitutes [NULL](#null-values).
+You can also write `CAST(value AS TYPE)` as `value::TYPE`. The `::` operator
binds more tightly than unary and binary operators, so `-x::BIGINT` means
`-(x::BIGINT)`. When a select item is a column cast with `::`, such as
`x::BIGINT`, the output column is named after the input column (`x`), unless
the select list contains `*` or if the name would duplicate another output
column's name.
+
## Arrays
Druid supports [`ARRAY` types](arrays.md), which behave as standard SQL
arrays, where results are grouped by matching entire arrays. The [`UNNEST`
operator](./sql.md#unnest) can be used to perform operations on individual
array elements, translating each element into a separate row.
diff --git a/docs/querying/sql-functions.md b/docs/querying/sql-functions.md
index 5f60619aded..e94bb1f1116 100644
--- a/docs/querying/sql-functions.md
+++ b/docs/querying/sql-functions.md
@@ -1495,7 +1495,7 @@ Returns the following:
Converts a value into the specified data type.
-* **Syntax:** `CAST(value AS TYPE)`
+* **Syntax:** `CAST(value AS TYPE)` or `value::TYPE`
* **Function type:** Scalar, other
<details>
diff --git a/docs/querying/sql-scalar.md b/docs/querying/sql-scalar.md
index d88a7a5312b..c3aa3b0ed7a 100644
--- a/docs/querying/sql-scalar.md
+++ b/docs/querying/sql-scalar.md
@@ -279,7 +279,7 @@ The [DataSketches
extension](../development/extensions-core/datasketches-extensi
|`BLOOM_FILTER_TEST(expr, serialized-filter)`|Returns true if the value of
`expr` is contained in the base64-serialized Bloom filter. See the [Bloom
filter extension](../development/extensions-core/bloom-filter.md) documentation
for additional details. See the [`BLOOM_FILTER` function](sql-aggregations.md)
for computing Bloom filters.|
|`CASE expr WHEN value1 THEN result1 \[ WHEN value2 THEN result2 ... \] \[
ELSE resultN \] END`|Simple CASE.|
|`CASE WHEN boolean_expr1 THEN result1 \[ WHEN boolean_expr2 THEN result2 ...
\] \[ ELSE resultN \] END`|Searched CASE.|
-|`CAST(value AS TYPE)`|Cast value to another type. See [Data
types](sql-data-types.md) for details about how Druid SQL handles CAST.|
+|`CAST(value AS TYPE)`|Cast value to another type. Can also be written as
`value::TYPE`. See [Data types](sql-data-types.md) for details about how Druid
SQL handles CAST.|
|`COALESCE(value1, value2, ...)`|Returns the first non-null value.|
|`DECODE_BASE64_COMPLEX(dataType, expr)`| Decodes a Base64-encoded string into
a complex data type, where `dataType` is the complex data type and `expr` is
the Base64-encoded string to decode. The `hyperUnique` and
`serializablePairLongString` data types are supported by default. You can
enable support for the following complex data types by loading their
extensions:<br/><ul><li>`druid-bloom-filter`:
`bloom`</li><li>`druid-datasketches`: `arrayOfDoublesSketch`, `HLLSketch`,
`KllDoublesSk [...]
|`NULLIF(value1, value2)`|Returns NULL if `value1` and `value2` match, else
returns `value1`.|
diff --git a/sql/src/main/codegen/config.fmpp b/sql/src/main/codegen/config.fmpp
index dd6533ad201..07f74712d52 100644
--- a/sql/src/main/codegen/config.fmpp
+++ b/sql/src/main/codegen/config.fmpp
@@ -122,6 +122,18 @@ data: {
"DruidType()"
]
+ # Binary operators tokens.
+ # Example: "< INFIX_CAST: \"::\" >".
+ binaryOperatorsTokens: [
+ "< INFIX_CAST: \"::\" >"
+ ]
+
+ # Binary operators initialization.
+ # Example: "InfixCast".
+ extraBinaryExpressions: [
+ "InfixCast"
+ ]
+
# List of files in @includes directory that have parser method
# implementations for parsing custom SQL statements, literals or types
# given as part of "statementParserMethods", "literalParserMethods" or
diff --git a/sql/src/main/codegen/includes/common.ftl
b/sql/src/main/codegen/includes/common.ftl
index 0180f671c1d..71d7c038b9c 100644
--- a/sql/src/main/codegen/includes/common.ftl
+++ b/sql/src/main/codegen/includes/common.ftl
@@ -106,6 +106,36 @@ SqlTypeNameSpec DruidType() :
}
}
+// Parses the PostgreSQL-style infix cast operator (e.g. x::BIGINT).
+void InfixCast(List<Object> list, ExprContext exprContext, Span s) :
+{
+ final SqlNode dt;
+}
+{
+ <INFIX_CAST>
+ {
+ checkNonQueryExpression(exprContext);
+ list.add(new SqlParserUtil.ToTreeListItem(SqlLibraryOperators.INFIX_CAST,
getPos()));
+ }
+ (
+ // Accept LONG as an alias for BIGINT, matching CAST(x AS LONG).
+ LOOKAHEAD(2)
+ <LONG>
+ {
+ dt = new SqlDataTypeSpec(new SqlBasicTypeNameSpec(SqlTypeName.BIGINT,
getPos()), getPos());
+ }
+ |
+ dt = DataType()
+ |
+ // Accept interval types, matching CAST(x AS INTERVAL DAY).
+ LOOKAHEAD(2)
+ <INTERVAL> dt = IntervalQualifier()
+ )
+ {
+ list.add(dt);
+ }
+}
+
// Parses the supported file formats for export.
SqlIdentifier FileFormat() :
{
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidSqlValidator.java
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidSqlValidator.java
index e5b75349058..01548f5388b 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidSqlValidator.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidSqlValidator.java
@@ -42,6 +42,7 @@ import org.apache.calcite.sql.SqlUpdate;
import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.SqlWindow;
import org.apache.calcite.sql.SqlWith;
+import org.apache.calcite.sql.fun.SqlLibraryOperators;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.type.SqlTypeName;
@@ -53,6 +54,7 @@ import org.apache.calcite.sql.validate.SqlValidatorException;
import org.apache.calcite.sql.validate.SqlValidatorNamespace;
import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.sql.validate.SqlValidatorTable;
+import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.Static;
import org.apache.calcite.util.Util;
@@ -824,7 +826,48 @@ public class DruidSqlValidator extends
BaseDruidSqlValidator
}
}
- return super.performUnconditionalRewrites(node, underFrom);
+ if (node instanceof SqlSelect) {
+ aliasSelectItems((SqlSelect) node);
+ }
+
+ final SqlNode rewritten = super.performUnconditionalRewrites(node,
underFrom);
+
+ // Rewrite "x::TYPE" to standard CAST.
+ if (rewritten instanceof SqlCall cast
+ && SqlLibraryOperators.INFIX_CAST.equals(((SqlCall)
rewritten).getOperator())) {
+ return SqlStdOperatorTable.CAST.createCall(cast.getParserPosition(),
cast.getOperandList());
+ }
+
+ return rewritten;
+ }
+
+ /**
+ * Wraps select items in "AS" when {@link #deriveSelectItemAlias} has a
better name for them. This must run
+ * before other rewrites, since it looks at the syntax the user wrote.
+ */
+ private static void aliasSelectItems(final SqlSelect select)
+ {
+ final SqlNodeList selectList = select.getSelectList();
+ final List<String> aliases = new ArrayList<>(selectList.size());
+ final List<String> names = new ArrayList<>(selectList.size());
+ for (int i = 0; i < selectList.size(); i++) {
+ final SqlNode item = selectList.get(i);
+ if (item instanceof SqlIdentifier && ((SqlIdentifier) item).isStar()) {
+ // Keep all defaults, since we can't tell which names the star expands
to.
+ return;
+ }
+ final String alias = deriveSelectItemAlias(item);
+ aliases.add(alias);
+ // Use the ordinal, so unnamed items reserve the "EXPR$i" names that
Calcite will generate for them.
+ names.add(alias != null ? alias : SqlValidatorUtil.alias(item, i));
+ }
+ for (int i = 0; i < selectList.size(); i++) {
+ final String alias = aliases.get(i);
+ // Skip duplicate names, since we can't have two output columns with the
same name and different types.
+ if (alias != null && Collections.frequency(names, alias) == 1) {
+ selectList.set(i, SqlValidatorUtil.addAlias(selectList.get(i), alias));
+ }
+ }
}
/**
@@ -884,6 +927,29 @@ public class DruidSqlValidator extends
BaseDruidSqlValidator
);
}
+ /**
+ * Returns a preferred name for a select item. We use this so "x::TYPE" can
be automatically named "x".
+ */
+ @Nullable
+ private static String deriveSelectItemAlias(final SqlNode item)
+ {
+ if (item.getKind() == SqlKind.CAST) {
+ final SqlCall cast = (SqlCall) item;
+
+ // This rule is only for infix cast (x::TYPE), not regular CAST.
+ if (SqlLibraryOperators.INFIX_CAST.equals(cast.getOperator())) {
+ final SqlNode operand = cast.operand(0);
+ if (operand instanceof SqlIdentifier identifier &&
!identifier.isStar()) {
+ return Util.last(identifier.names);
+ } else {
+ return deriveSelectItemAlias(operand);
+ }
+ }
+ }
+
+ return null;
+ }
+
private SqlNode getSqlNodeFor(SqlInsert insert, int idx)
{
SqlNode src = insert.getSource();
@@ -903,7 +969,4 @@ public class DruidSqlValidator extends BaseDruidSqlValidator
&& call.getFunctionQuantifier() != null
&& call.getFunctionQuantifier().getValue() ==
SqlSelectKeyword.DISTINCT;
}
-
-
-
}
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteInsertDmlTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteInsertDmlTest.java
index 31b5aa183c8..fdaca646947 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteInsertDmlTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteInsertDmlTest.java
@@ -137,6 +137,29 @@ public class CalciteInsertDmlTest extends
CalciteIngestionDmlTest
.verify();
}
+ @Test
+ public void testInsertInfixCastColumnName()
+ {
+ testIngestionQuery()
+ .sql("INSERT INTO dst SELECT __time, dim1::BIGINT FROM foo PARTITIONED
BY ALL TIME")
+ .expectTarget(
+ "dst",
+ RowSignature.builder().add("__time", ColumnType.LONG).add("dim1",
ColumnType.LONG).build()
+ )
+ .expectResources(dataSourceRead("foo"), dataSourceRead("dst"),
dataSourceWrite("dst"))
+ .expectQuery(
+ newScanQueryBuilder()
+ .dataSource("foo")
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(expressionVirtualColumn("v0", "CAST(\"dim1\",
'LONG')", ColumnType.LONG))
+ .columns("__time", "v0")
+ .columnTypes(ColumnType.LONG, ColumnType.LONG)
+ .context(PARTITIONED_BY_ALL_TIME_QUERY_CONTEXT)
+ .build()
+ )
+ .verify();
+ }
+
@Test
public void testInsertFromViewC()
{
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
index 29f6d136515..499df1dbddd 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
@@ -4695,6 +4695,461 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
);
}
+ @Test
+ public void testFilterOnStringAsNumberInfixCast()
+ {
+ testQuery(
+ "SELECT distinct dim1 FROM druid.foo WHERE dim1::float > 9 and
dim1::float <= 10.5",
+ ImmutableList.of(
+ GroupByQuery.builder()
+ .setDataSource(CalciteTests.DATASOURCE1)
+ .setInterval(querySegmentSpec(Filtration.eternity()))
+ .setGranularity(Granularities.ALL)
+ .setDimensions(dimensions(new
DefaultDimensionSpec("dim1", "d0")))
+ .setDimFilter(
+ range("dim1", ColumnType.FLOAT, 9.0, 10.5, true,
false)
+ )
+ .setContext(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"10.1"}
+ )
+ );
+ }
+
+ @Test
+ public void testInfixCastPrecedence()
+ {
+ // "::" binds tighter than unary minus and binary operators.
+ testQuery(
+ "SELECT -m1::BIGINT, 1 + dim1::BIGINT, 'x' || cnt::VARCHAR FROM foo",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(
+ expressionVirtualColumn("v0", "(- CAST(\"m1\", 'LONG'))",
ColumnType.LONG),
+ expressionVirtualColumn("v1", "(1 + CAST(\"dim1\",
'LONG'))", ColumnType.LONG),
+ expressionVirtualColumn("v2", "concat('x',CAST(\"cnt\",
'STRING'))", ColumnType.STRING)
+ )
+ .columns("v0", "v1", "v2")
+ .columnTypes(ColumnType.LONG, ColumnType.LONG,
ColumnType.STRING)
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{-1L, null, "x1"},
+ new Object[]{-2L, 11L, "x1"},
+ new Object[]{-3L, 3L, "x1"},
+ new Object[]{-4L, 2L, "x1"},
+ new Object[]{-5L, null, "x1"},
+ new Object[]{-6L, null, "x1"}
+ )
+ );
+ }
+
+ @Test
+ public void testInfixCastToMultiWordType()
+ {
+ // Type names containing spaces are parsed in full after "::".
+ testQuery(
+ "SELECT ARRAY[cnt]::VARCHAR ARRAY, STRING_TO_ARRAY(dim1, ',')::DOUBLE
ARRAY, m1::DOUBLE PRECISION FROM foo",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(
+ expressionVirtualColumn("v0", "CAST(array(\"cnt\"),
'ARRAY<STRING>')", ColumnType.STRING_ARRAY),
+ expressionVirtualColumn(
+ "v1",
+ "CAST(string_to_array(\"dim1\",','), 'ARRAY<DOUBLE>')",
+ ColumnType.DOUBLE_ARRAY
+ ),
+ expressionVirtualColumn("v2", "\"m1\"", ColumnType.DOUBLE)
+ )
+ .columns("v0", "v1", "v2")
+ .columnTypes(ColumnType.STRING_ARRAY, ColumnType.DOUBLE_ARRAY,
ColumnType.DOUBLE)
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"[\"1\"]", "[null]", 1.0},
+ new Object[]{"[\"1\"]", "[10.1]", 2.0},
+ new Object[]{"[\"1\"]", "[2.0]", 3.0},
+ new Object[]{"[\"1\"]", "[1.0]", 4.0},
+ new Object[]{"[\"1\"]", "[null]", 5.0},
+ new Object[]{"[\"1\"]", "[null]", 6.0}
+ )
+ );
+ }
+
+ @Test
+ public void testInfixCastToComplexType()
+ {
+ // Fails the same way as CAST(... AS TYPE('COMPLEX<...>')), since complex
types are not resolvable in queries.
+ testQueryThrows(
+ "SELECT unique_dim1::TYPE('COMPLEX<hyperUnique>') FROM foo",
+ invalidSqlContains("Unknown identifier 'COMPLEX<hyperUnique>'")
+ );
+ }
+
+ @Test
+ public void testCastToComplexType()
+ {
+ testQueryThrows(
+ "SELECT CAST(unique_dim1 AS TYPE('COMPLEX<hyperUnique>')) FROM foo",
+ invalidSqlContains("Unknown identifier 'COMPLEX<hyperUnique>'")
+ );
+ }
+
+ @Test
+ public void testInfixCastSelectItemName()
+ {
+ testQuery(
+ "SELECT dim1::BIGINT FROM foo",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(expressionVirtualColumn("v0", "CAST(\"dim1\",
'LONG')", ColumnType.LONG))
+ .columns("v0")
+ .columnTypes(ColumnType.LONG)
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{null},
+ new Object[]{10L},
+ new Object[]{2L},
+ new Object[]{1L},
+ new Object[]{null},
+ new Object[]{null}
+ ),
+ RowSignature.builder().add("dim1", ColumnType.LONG).build()
+ );
+ }
+
+ @Test
+ public void testInfixCastSelectItemNameQualified()
+ {
+ testQuery(
+ "SELECT foo.dim1::BIGINT FROM foo",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(expressionVirtualColumn("v0", "CAST(\"dim1\",
'LONG')", ColumnType.LONG))
+ .columns("v0")
+ .columnTypes(ColumnType.LONG)
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{null},
+ new Object[]{10L},
+ new Object[]{2L},
+ new Object[]{1L},
+ new Object[]{null},
+ new Object[]{null}
+ ),
+ RowSignature.builder().add("dim1", ColumnType.LONG).build()
+ );
+ }
+
+ @Test
+ public void testInfixCastSelectItemNameChained()
+ {
+ testQuery(
+ "SELECT dim1::VARCHAR::BIGINT FROM foo",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(expressionVirtualColumn("v0", "CAST(\"dim1\",
'LONG')", ColumnType.LONG))
+ .columns("v0")
+ .columnTypes(ColumnType.LONG)
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{null},
+ new Object[]{10L},
+ new Object[]{2L},
+ new Object[]{1L},
+ new Object[]{null},
+ new Object[]{null}
+ ),
+ RowSignature.builder().add("dim1", ColumnType.LONG).build()
+ );
+ }
+
+ @Test
+ public void testInfixCastSelectItemNameDefault()
+ {
+ // CAST(x AS T) keeps its default name, and so does "::" applied to an
expression that has no name.
+ testQuery(
+ "SELECT CAST(dim1 AS BIGINT), (dim1 || 'x')::VARCHAR FROM foo",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(
+ expressionVirtualColumn("v0", "CAST(\"dim1\", 'LONG')",
ColumnType.LONG),
+ expressionVirtualColumn("v1", "concat(\"dim1\",'x')",
ColumnType.STRING)
+ )
+ .columns("v0", "v1")
+ .columnTypes(ColumnType.LONG, ColumnType.STRING)
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{null, "x"},
+ new Object[]{10L, "10.1x"},
+ new Object[]{2L, "2x"},
+ new Object[]{1L, "1x"},
+ new Object[]{null, "defx"},
+ new Object[]{null, "abcx"}
+ ),
+ RowSignature.builder()
+ .add("EXPR$0", ColumnType.LONG)
+ .add("EXPR$1", ColumnType.STRING)
+ .build()
+ );
+ }
+
+ @Test
+ public void testInfixCastSelectItemNameExplicitAlias()
+ {
+ testQuery(
+ "SELECT dim1::BIGINT AS dim1_long FROM foo",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(expressionVirtualColumn("v0", "CAST(\"dim1\",
'LONG')", ColumnType.LONG))
+ .columns("v0")
+ .columnTypes(ColumnType.LONG)
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{null},
+ new Object[]{10L},
+ new Object[]{2L},
+ new Object[]{1L},
+ new Object[]{null},
+ new Object[]{null}
+ ),
+ RowSignature.builder().add("dim1_long", ColumnType.LONG).build()
+ );
+ }
+
+ @Test
+ public void testInfixCastSelectItemNameDuplicate()
+ {
+ // Names that would duplicate another select item's name keep the default.
+ testQuery(
+ "SELECT dim1, dim1::BIGINT, m1::BIGINT, m1::DOUBLE, cnt::VARCHAR FROM
foo",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(
+ expressionVirtualColumn("v0", "CAST(\"dim1\", 'LONG')",
ColumnType.LONG),
+ expressionVirtualColumn("v1", "CAST(\"m1\", 'LONG')",
ColumnType.LONG),
+ expressionVirtualColumn("v2", "\"m1\"", ColumnType.DOUBLE),
+ expressionVirtualColumn("v3", "CAST(\"cnt\", 'STRING')",
ColumnType.STRING)
+ )
+ .columns("dim1", "v0", "v1", "v2", "v3")
+ .columnTypes(ColumnType.STRING, ColumnType.LONG,
ColumnType.LONG, ColumnType.DOUBLE, ColumnType.STRING)
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"", null, 1L, 1.0, "1"},
+ new Object[]{"10.1", 10L, 2L, 2.0, "1"},
+ new Object[]{"2", 2L, 3L, 3.0, "1"},
+ new Object[]{"1", 1L, 4L, 4.0, "1"},
+ new Object[]{"def", null, 5L, 5.0, "1"},
+ new Object[]{"abc", null, 6L, 6.0, "1"}
+ ),
+ RowSignature.builder()
+ .add("dim1", ColumnType.STRING)
+ .add("EXPR$1", ColumnType.LONG)
+ .add("EXPR$2", ColumnType.LONG)
+ .add("EXPR$3", ColumnType.DOUBLE)
+ .add("cnt", ColumnType.STRING)
+ .build()
+ );
+ }
+
+ @Test
+ public void testInfixCastSelectItemNameDuplicateGenerated()
+ {
+ // Names that would duplicate a name generated for an unnamed select item
keep the default.
+ testQuery(
+ "SELECT 'x', EXPR$0::BIGINT FROM (SELECT dim1 AS EXPR$0 FROM foo)",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(
+ expressionVirtualColumn("v0", "'x'", ColumnType.STRING),
+ expressionVirtualColumn("v1", "CAST(\"dim1\", 'LONG')",
ColumnType.LONG)
+ )
+ .columns("v0", "v1")
+ .columnTypes(ColumnType.STRING, ColumnType.LONG)
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"x", null},
+ new Object[]{"x", 10L},
+ new Object[]{"x", 2L},
+ new Object[]{"x", 1L},
+ new Object[]{"x", null},
+ new Object[]{"x", null}
+ ),
+ RowSignature.builder()
+ .add("EXPR$0", ColumnType.STRING)
+ .add("EXPR$1", ColumnType.LONG)
+ .build()
+ );
+ }
+
+ @Test
+ public void testInfixCastSelectItemNameStar()
+ {
+ // A star may expand to any name, so nothing in the select list is named.
+ testQuery(
+ "SELECT *, dim1::BIGINT FROM (SELECT dim1 FROM foo)",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(expressionVirtualColumn("v0", "CAST(\"dim1\",
'LONG')", ColumnType.LONG))
+ .columns("dim1", "v0")
+ .columnTypes(ColumnType.STRING, ColumnType.LONG)
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"", null},
+ new Object[]{"10.1", 10L},
+ new Object[]{"2", 2L},
+ new Object[]{"1", 1L},
+ new Object[]{"def", null},
+ new Object[]{"abc", null}
+ ),
+ RowSignature.builder()
+ .add("dim1", ColumnType.STRING)
+ .add("EXPR$1", ColumnType.LONG)
+ .build()
+ );
+ }
+
+ @Test
+ public void testInfixCastSelectItemNameOrderBy()
+ {
+ // ORDER BY dim1 refers to the select item named dim1, not the column, so
it sorts numerically.
+ testQuery(
+ "SELECT dim1::BIGINT FROM foo GROUP BY dim1 ORDER BY dim1",
+ ImmutableList.of(
+ GroupByQuery.builder()
+ .setDataSource(CalciteTests.DATASOURCE1)
+ .setInterval(querySegmentSpec(Filtration.eternity()))
+ .setGranularity(Granularities.ALL)
+ .setDimensions(dimensions(new
DefaultDimensionSpec("dim1", "d0")))
+ .setPostAggregatorSpecs(expressionPostAgg("p0",
"CAST(\"d0\", 'LONG')", ColumnType.LONG))
+ .setLimitSpec(
+ new DefaultLimitSpec(
+ ImmutableList.of(
+ new OrderByColumnSpec("p0",
Direction.ASCENDING, StringComparators.NUMERIC)
+ ),
+ Integer.MAX_VALUE
+ )
+ )
+ .setContext(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{null},
+ new Object[]{null},
+ new Object[]{null},
+ new Object[]{1L},
+ new Object[]{2L},
+ new Object[]{10L}
+ ),
+ RowSignature.builder().add("dim1", ColumnType.LONG).build()
+ );
+ }
+
+ @Test
+ public void testInfixCastToLong()
+ {
+ testQuery(
+ "SELECT dim1::LONG FROM foo",
+ ImmutableList.of(
+ newScanQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(querySegmentSpec(Filtration.eternity()))
+ .virtualColumns(expressionVirtualColumn("v0", "CAST(\"dim1\",
'LONG')", ColumnType.LONG))
+ .columns("v0")
+ .columnTypes(ColumnType.LONG)
+
.resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{null},
+ new Object[]{10L},
+ new Object[]{2L},
+ new Object[]{1L},
+ new Object[]{null},
+ new Object[]{null}
+ ),
+ RowSignature.builder().add("dim1", ColumnType.LONG).build()
+ );
+ }
+
+ @Test
+ public void testInfixCastMixedWithCastInGroupBy()
+ {
+ // "x::TYPE" and CAST(x AS TYPE) are the same expression for the purposes
of GROUP BY.
+ testQuery(
+ "SELECT CAST(m1 AS BIGINT), COUNT(*) FROM foo GROUP BY m1::BIGINT",
+ ImmutableList.of(
+ GroupByQuery.builder()
+ .setDataSource(CalciteTests.DATASOURCE1)
+ .setInterval(querySegmentSpec(Filtration.eternity()))
+ .setGranularity(Granularities.ALL)
+ .setDimensions(dimensions(new
DefaultDimensionSpec("m1", "d0", ColumnType.LONG)))
+ .setAggregatorSpecs(aggregators(new
CountAggregatorFactory("a0")))
+ .setContext(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{1L, 1L},
+ new Object[]{2L, 1L},
+ new Object[]{3L, 1L},
+ new Object[]{4L, 1L},
+ new Object[]{5L, 1L},
+ new Object[]{6L, 1L}
+ )
+ );
+ }
+
@Test
public void testSimpleLongAggregations()
{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]