diff --git a/docs/content/querying/sql.md b/docs/content/querying/sql.md
index 66a2c3c05e59..6b641ce800b9 100644
--- a/docs/content/querying/sql.md
+++ b/docs/content/querying/sql.md
@@ -1,77 +1,331 @@
---
layout: doc_page
---
-# SQL Support for Druid
-
-## Built-in SQL
+# SQL
Built-in SQL is an
experimental feature. The API described here is
subject to change.
-Druid includes a native SQL layer with an [Apache Calcite](https://calcite.apache.org/)-based parser and planner. All
-parsing and planning takes place on the Broker, where SQL is converted to native Druid queries. Those native Druid
-queries are then passed down to data nodes. Each Druid datasource appears as a table in the "druid" schema. Datasource
-and column names are both case-sensitive and can optionally be quoted using double quotes. Literal strings should be
-quoted with single quotes, like `'foo'`. Literal strings with Unicode escapes can be written like `U&'fo\00F6'`, where
-character codes in hex are prefixed by a backslash.
+Druid SQL is a built-in SQL layer and an alternative to Druid's native JSON-based query language, and is powered by a
+parser and planner based on [Apache Calcite](https://calcite.apache.org/). Druid SQL translates SQL into native Druid
+queries on the query broker (the first node you query), which are then passed down to data nodes as native Druid
+queries. Other than the (slight) overhead of translating SQL on the broker, there isn't an additional performance
+penalty versus native queries.
-Add "EXPLAIN PLAN FOR" to the beginning of any query to see how Druid will plan that query.
+To enable Druid SQL, make sure you have set `druid.sql.enable = true` either in your common.runtime.properties or your
+broker's runtime.properties.
-### Querying with JDBC
+## Query syntax
-You can make Druid SQL queries using the [Avatica JDBC driver](https://calcite.apache.org/avatica/downloads/). Once
-you've downloaded the Avatica client jar, add it to your classpath and use the connect string:
+Each Druid datasource appears as a table in the "druid" schema. This is also the default schema, so Druid datasources
+can be referenced as either `druid.dataSourceName` or simply `dataSourceName`.
+
+Identifiers like datasource and column names can optionally be quoted using double quotes. To escape a double quote
+inside an identifier, use another double quote, like `"My ""very own"" identifier"`. All identifiers are case-sensitive
+and no implicit case conversions are performed.
+
+Literal strings should be quoted with single quotes, like `'foo'`. Literal strings with Unicode escapes can be written
+like `U&'fo\00F6'`, where character codes in hex are prefixed by a backslash. Literal numbers can be written in forms
+like `100` (denoting an integer), `100.0` (denoting a floating point value), or `1.0e5` (scientific notation). Literal
+timestamps can be written like `TIMESTAMP '2000-01-01 00:00:00'`. Literal intervals, used for time arithmetic, can be
+written like `INTERVAL '1' HOUR`, `INTERVAL '1 02:03' DAY TO MINUTE`, `INTERVAL '1-2' YEAR TO MONTH`, and so on.
+
+Druid SQL supports SELECT queries with the following structure:
```
-jdbc:avatica:remote:url=http://BROKER:8082/druid/v2/sql/avatica/
+[ EXPLAIN PLAN FOR ]
+[ WITH tableName [ ( column1, column2, ... ) ] AS ( query ) ]
+SELECT [ ALL | DISTINCT ] { * | exprs }
+FROM table
+[ WHERE expr ]
+[ GROUP BY exprs ]
+[ HAVING expr ]
+[ ORDER BY expr [ ASC | DESC ], expr [ ASC | DESC ], ... ]
+[ LIMIT limit ]
```
-Example code:
+The FROM clause refers to either a Druid datasource, like `druid.foo`, an [INFORMATION_SCHEMA table](#retrieving-metadata), a
+subquery, or a common-table-expression provided in the WITH clause. If the FROM clause references a subquery or a
+common-table-expression, and both levels of queries are aggregations and they cannot be combined into a single level of
+aggregation, the overall query will be executed as a [nested GroupBy](groupbyquery.html#nested-groupbys).
+
+The WHERE clause refers to columns in the FROM table, and will be translated to [native filters](filters.html). The
+WHERE clause can also reference a subquery, like `WHERE col1 IN (SELECT foo FROM ...)`. Queries like this are executed
+as [semi-joins](#query-execution), described below.
+
+The GROUP BY clause refers to columns in the FROM table. Using GROUP BY, DISTINCT, or any aggregation functions will
+trigger an aggregation query using one of Druid's [three native aggregation query types](#query-execution).
+
+The HAVING clause refers to columns that are present after execution of GROUP BY. It can be used to filter on either
+grouping expressions or aggregated values. It can only be used together with GROUP BY.
+
+The ORDER BY clause refers to columns that are present after execution of GROUP BY. It can be used to order the results
+based on either grouping expressions or aggregated values. The ORDER BY expression can be a column name, alias, or
+ordinal position (like `ORDER BY 2` to order by the second column). ORDER BY can only be used together with GROUP BY.
+
+The LIMIT clause can be used to limit the number of rows returned. It can be used with any query type. It is pushed down
+to data nodes for queries that run with the native TopN query type, but not the native GroupBy query type. Future
+versions of Druid will support pushing down limits using the native GroupBy query type as well. If you notice that
+adding a limit doesn't change performance very much, then it's likely that Druid didn't push down the limit for your
+query.
+
+Add "EXPLAIN PLAN FOR" to the beginning of any query to see how it would be run as a native Druid query. In this case,
+the query will not actually be executed.
+
+### Aggregation functions
+
+Aggregation functions can appear in the SELECT clause of any query. Any aggregator can be filtered using syntax like
+`AGG(expr) FILTER(WHERE whereExpr)`. Filtered aggregators will only aggregate rows that match their filter. It's
+possible for two aggregators in the same SQL query to have different filters.
+
+Only the COUNT aggregation can accept DISTINCT.
+
+|Function|Notes|
+|--------|-----|
+|`COUNT(*)`|Counts the number of rows.|
+|`COUNT(DISTINCT expr)`|Counts distinct values of expr, which can be string, numeric, or hyperUnique. By default this is approximate, using a variant of [HyperLogLog](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf). To get exact counts set "useApproximateCountDistinct" to "false". If you do this, expr must be string or numeric, since exact counts are not possible using hyperUnique columns. See also `APPROX_COUNT_DISTINCT(expr)`. In exact mode, only one distinct count per query is permitted.|
+|`SUM(expr)`|Sums numbers.|
+|`MIN(expr)`|Takes the minimum of numbers.|
+|`MAX(expr)`|Takes the maximum of numbers.|
+|`AVG(expr)`|Averages numbers.|
+|`APPROX_COUNT_DISTINCT(expr)`|Counts distinct values of expr, which can be a regular column or a hyperUnique column. This is always approximate, regardless of the value of "useApproximateCountDistinct". See also `COUNT(DISTINCT expr)`.|
+|`APPROX_QUANTILE(expr, probability, [resolution])`|Computes approximate quantiles on numeric or approxHistogram exprs. The "probability" should be between 0 and 1 (exclusive). The "resolution" is the number of centroids to use for the computation. Higher resolutions will give more precise results but also have higher overhead. If not provided, the default resolution is 50. The [approximate histogram extension](../development/extensions-core/approximate-histograms.html) must be loaded to use this function.|
+
+### Numeric functions
+
+Numeric functions will return 64 bit integers or 64 bit floats, depending on their inputs.
+
+|Function|Notes|
+|--------|-----|
+|`ABS(expr)`|Absolute value.|
+|`CEIL(expr)`|Ceiling.|
+|`EXP(expr)`|e to the power of expr.|
+|`FLOOR(expr)`|Floor.|
+|`LN(expr)`|Logarithm (base e).|
+|`LOG10(expr)`|Logarithm (base 10).|
+|`POW(expr, power)`|expr to a power.|
+|`SQRT(expr)`|Square root.|
+|`x + y`|Addition.|
+|`x - y`|Subtraction.|
+|`x * y`|Multiplication.|
+|`x / y`|Division.|
+|`x % y`|Mod.|
+
+### String functions
+
+String functions accept strings, and return a type appropriate to the function.
+
+|Function|Notes|
+|--------|-----|
+|`x \|\| y`|Concat strings x and y.|
+|`CHARACTER_LENGTH(expr)`|Length of expr in UTF-16 code units.|
+|`LOOKUP(expr, lookupName)`|Look up expr in a registered [query-time lookup table](lookups.html).|
+|`LOWER(expr)`|Returns expr in all lowercase.|
+|`REGEXP_EXTRACT(expr, pattern, [index])`|Apply regular expression pattern and extract a capture group, or null if there is no match. If index is unspecified or zero, returns the substring that matched the pattern.|
+|`REPLACE(expr, pattern, replacement)`|Replaces pattern with replacement in expr, and returns the result.|
+|`SUBSTRING(expr, index, [length])`|Returns a substring of expr starting at index, with a max length, both measured in UTF-16 code units.|
+|`TRIM(expr)`|Returns expr with leading and trailing whitespace removed.|
+|`UPPER(expr)`|Returns expr in all uppercase.|
-```java
-// Connect to /druid/v2/sql/avatica/ on your broker.
-String url = "jdbc:avatica:remote:url=http://localhost:8082/druid/v2/sql/avatica/";
+### Time functions
-// Set any connection context parameters you need here (see "Connection context" below).
-// Or leave empty for default behavior.
-Properties connectionProperties = new Properties();
+Time functions can be used with Druid's `__time` column, with any column storing millisecond timestamps through use
+of the `MILLIS_TO_TIMESTAMP` function, or with any column storing string timestamps through use of the `TIME_PARSE`
+function. By default, time operations use the UTC time zone. You can change the time zone by setting the connection
+context parameter "sqlTimeZone" to the name of another time zone, like "America/Los_Angeles", or to an offset like
+"-08:00". If you need to mix multiple time zones in the same query, or if you need to use a time zone other than
+the connection time zone, some functions also accept time zones as parameters. These parameters always take precedence
+over the connection time zone.
+
+|Function|Notes|
+|--------|-----|
+|`CURRENT_TIMESTAMP`|Current timestamp in the connection's time zone.|
+|`CURRENT_DATE`|Current date in the connection's time zone.|
+|`TIME_FLOOR(, , [, []])`|Rounds down a timestamp, returning it as a new timestamp. Period can be any ISO8601 period, like P3M (quarters) or PT12H (half-days). The time zone, if provided, should be a time zone name like "America/Los_Angeles" or offset like "-08:00". This function is similar to `FLOOR` but is more flexible.|
+|`TIME_SHIFT(, , , [])`|Shifts a timestamp by a period (step times), returning it as a new timestamp. Period can be any ISO8601 period. Step may be negative. The time zone, if provided, should be a time zone name like "America/Los_Angeles" or offset like "-08:00".|
+|`TIME_EXTRACT(, [, []])`|Extracts a time part from expr, returning it as a number. Unit can be EPOCH, SECOND, MINUTE, HOUR, DAY (day of month), DOW (day of week), DOY (day of year), WEEK (week of [week year](https://en.wikipedia.org/wiki/ISO_week_date)), MONTH (1 through 12), QUARTER (1 through 4), or YEAR. The time zone, if provided, should be a time zone name like "America/Los_Angeles" or offset like "-08:00". This function is similar to `EXTRACT` but is more flexible. Unit and time zone must be literals, and must be provided quoted, like `TIME_EXTRACT(__time, 'HOUR')` or `TIME_EXTRACT(__time, 'HOUR', 'America/Los_Angeles')`.|
+|`TIME_PARSE(, [, []])`|Parses a string into a timestamp using a given [Joda DateTimeFormat pattern](http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html), or ISO8601 (e.g. `2000-01-02T03:04:05Z`) if the pattern is not provided. The time zone, if provided, should be a time zone name like "America/Los_Angeles" or offset like "-08:00", and will be used as the time zone for strings that do not include a time zone offset. Pattern and time zone must be literals. Strings that cannot be parsed as timestamps will be returned as NULL.|
+|`TIME_FORMAT(, [, []])`|Formats a timestamp as a string with a given [Joda DateTimeFormat pattern](http://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html), or ISO8601 (e.g. `2000-01-02T03:04:05Z`) if the pattern is not provided. The time zone, if provided, should be a time zone name like "America/Los_Angeles" or offset like "-08:00". Pattern and time zone must be literals.|
+|`MILLIS_TO_TIMESTAMP(millis_expr)`|Converts a number of milliseconds since the epoch into a timestamp.|
+|`TIMESTAMP_TO_MILLIS(timestamp_expr)`|Converts a timestamp into a number of milliseconds since the epoch.|
+|`EXTRACT( FROM timestamp_expr)`|Extracts a time part from expr, returning it as a number. Unit can be EPOCH, SECOND, MINUTE, HOUR, DAY (day of month), DOW (day of week), DOY (day of year), WEEK (week of year), MONTH, QUARTER, or YEAR. Units must be provided unquoted, like `EXTRACT(HOUR FROM __time)`.|
+|`FLOOR(timestamp_expr TO )`|Rounds down a timestamp, returning it as a new timestamp. Unit can be SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, or YEAR.|
+|`CEIL(timestamp_expr TO )`|Rounds up a timestamp, returning it as a new timestamp. Unit can be SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, or YEAR.|
+|`timestamp_expr { + \| - } `|Add or subtract an amount of time from a timestamp. interval_expr can include interval literals like `INTERVAL '2' HOUR`, and may include interval arithmetic as well. This operator treats days as uniformly 86400 seconds long, and does not take into account daylight savings time. To account for daylight savings time, use TIME_SHIFT instead.|
+
+### Comparison operators
+
+|Function|Notes|
+|--------|-----|
+|`x = y`|Equals.|
+|`x <> y`|Not-equals.|
+|`x > y`|Greater than.|
+|`x >= y`|Greater than or equal to.|
+|`x < y`|Less than.|
+|`x <= y`|Less than or equal to.|
+|`x LIKE pattern [ESCAPE esc]`|True if x matches a SQL LIKE pattern (with an optional escape).|
+|`x NOT LIKE pattern [ESCAPE esc]`|True if x does not match a SQL LIKE pattern (with an optional escape).|
+|`x IS NULL`|True if x is NULL or empty string.|
+|`x IS NOT NULL`|True if x is neither NULL nor empty string.|
+|`x IS TRUE`|True if x is true.|
+|`x IS NOT TRUE`|True if x is not true.|
+|`x IS FALSE`|True if x is false.|
+|`x IS NOT FALSE`|True if x is not false.|
+|`x IN (values)`|True if x is one of the listed values.|
+|`x NOT IN (values)`|True if x is not one of the listed values.|
+|`x IN (subquery)`|True if x is returned by the subquery. See [Syntax and execution](#syntax-and-execution) above for details about how Druid SQL handles `IN (subquery)`.|
+|`x NOT IN (subquery)`|True if x is not returned by the subquery. See [Syntax and execution](#syntax-and-execution) for details about how Druid SQL handles `IN (subquery)`.|
+|`x AND y`|Boolean AND.|
+|`x OR y`|Boolean OR.|
+|`NOT x`|Boolean NOT.|
+
+### Other functions
+
+|Function|Notes|
+|--------|-----|
+|`CAST(value AS TYPE)`|Cast value to another type. See [Data types and casts](#data-types-and-casts) for details about how Druid SQL handles CAST.|
+|`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.|
+|`NULLIF(value1, value2)`|Returns NULL if value1 and value2 match, else returns value1.|
+|`COALESCE(value1, value2, ...)`|Returns the first value that is neither NULL nor empty string.|
-try (Connection connection = DriverManager.getConnection(url, connectionProperties)) {
- try (ResultSet resultSet = connection.createStatement().executeQuery("SELECT COUNT(*) AS cnt FROM data_source")) {
- while (resultSet.next()) {
- // Do something
- }
- }
-}
-```
+### Unsupported features
-Table metadata is available over JDBC using `connection.getMetaData()` or by querying the "INFORMATION_SCHEMA" tables
-(see below).
+Druid does not support all SQL features, including:
-Parameterized queries don't work properly, so avoid those.
+- OVER clauses, and analytic functions such as `LAG` and `LEAD`.
+- JOIN clauses, other than semi-joins as described above.
+- OFFSET clauses.
+- DDL and DML.
-### Querying with JSON over HTTP
+Additionally, some Druid features are not supported by the SQL language. Some unsupported Druid features include:
-You can make Druid SQL queries using JSON over HTTP by POSTing to the endpoint `/druid/v2/sql/`. The request format
-is:
+- [Multi-value dimensions](multi-value-dimensions.html).
+- [DataSketches aggregators](../development/extensions-core/datasketches-aggregators.html).
+
+## Data types and casts
+
+Druid natively supports four main column types: "long" (64 bit signed int), "float" (32 bit float), "string" (UTF-8
+encoded strings), and "complex" (catch-all for more exotic data types like hyperUnique and approxHistogram columns).
+Timestamps (including the `__time` column) are stored as longs, with the value being the number of milliseconds since 1
+January 1970 UTC.
+
+At runtime, Druid will widen floats to "double" (64 bit float) for certain features, like `SUM` aggregators. But this
+widening is not universal; some floating point operations retain 32 bit precision.
+
+Druid generally treats NULLs and empty strings interchangeably, rather than according to the SQL standard. As such,
+Druid SQL only has partial support for NULLs. For example, the expressions `col IS NULL` and `col = ''` are equivalent,
+and both will evaluate to true if `col` contains an empty string. Similarly, the expression `COALESCE(col1, col2)` will
+return `col2` if `col1` is an empty string. While the `COUNT(*)` aggregator counts all rows, the `COUNT(expr)`
+aggregator will count the number of rows where expr is neither null nor the empty string. String columns in Druid are
+NULLable. Numeric columns are NOT NULL; if you query a numeric column that is not present in all segments of your Druid
+datasource, then it will be treated as zero for rows from those segments.
+
+For mathematical operations, Druid SQL will use integer math if all operands involved in an expression are integers.
+Otherwise, Druid will switch to floating point math. You can force this to happen by casting one of your operands
+to FLOAT.
+
+The following table describes how SQL types map onto Druid types during query runtime. Casts between two SQL types
+that have the same Druid runtime type will have no effect, other than exceptions noted in the table. Casts between two
+SQL types that have different Druid runtime types will generate a runtime cast in Druid. If a value cannot be properly
+cast to another value, as in `CAST('foo' AS BIGINT)`, the runtime will substitute a default value. NULL values cast
+to non-nullable types will also be substitued with a default value (for example, nulls cast to numbers will be
+converted to zeroes).
+
+|SQL type|Druid runtime type|Default value|Notes|
+|--------|------------------|-------------|-----|
+|CHAR|STRING|`''`||
+|VARCHAR|STRING|`''`|Druid STRING columns are reported as VARCHAR|
+|DECIMAL|FLOAT or DOUBLE|`0.0`|DECIMAL uses floating point, not fixed point math|
+|FLOAT|FLOAT or DOUBLE|`0.0`|Druid FLOAT columns are reported as FLOAT|
+|REAL|FLOAT or DOUBLE|`0.0`||
+|DOUBLE|FLOAT or DOUBLE|`0.0`||
+|BOOLEAN|LONG|`false`||
+|TINYINT|LONG|`0`||
+|SMALLINT|LONG|`0`||
+|INTEGER|LONG|`0`||
+|BIGINT|LONG|`0`|Druid LONG columns (except `__time`) are reported as BIGINT|
+|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, e.g. `2000-01-02 03:04:05`, _not_ ISO8601 formatting. For handling other formats, use one of the [time functions](#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, e.g. `2000-01-02`. For handling other formats, use one of the [time functions](#time-functions)|
+|OTHER|COMPLEX|none|May represent various Druid column types such as hyperUnique, approxHistogram, etc|
+
+## Query execution
+
+Queries without aggregations will use Druid's [Select](select-query.html) native query type.
+
+Aggregation queries (using GROUP BY, DISTINCT, or any aggregation functions) will use one of Druid's three native
+aggregation query types. Two (Timeseries and TopN) are specialized for specific types of aggregations, whereas the other
+(GroupBy) is general-purpose.
+
+- [Timeseries](timeseriesquery.html) is used for queries that GROUP BY `FLOOR(__time TO )` or `TIME_FLOOR(__time,
+period)`, have no other grouping expressions, no HAVING or LIMIT clauses, no nesting, and either no ORDER BY, or an
+ORDER BY that orders by same expression as present in GROUP BY. It also uses Timeseries for "grand total" queries that
+have aggregation functions but no GROUP BY. This query type takes advantage of the fact that Druid segments are sorted
+by time.
+
+- [TopN](topnquery.html) is used by default for queries that group by a single expression, do have ORDER BY and LIMIT
+clauses, do not have HAVING clauses, and are not nested. However, the TopN query type will deliver approximate ranking
+and results in some cases; if you want to avoid this, set "useApproximateTopN" to "false". TopN results are always
+computed in memory. See the TopN documentation for more details.
+
+- [GroupBy](groupbyquery.html) is used for all other aggregations, including any nested aggregation queries. Druid's
+GroupBy is a traditional aggregation engine: it delivers exact results and rankings and supports a wide variety of
+features. GroupBy aggregates in memory if it can, but it may spill to disk if it doesn't have enough memory to complete
+your query. Results are streamed back from data nodes through the broker if you ORDER BY the same expressions in your
+GROUP BY clause, or if you don't have an ORDER BY at all. If your query has an ORDER BY referencing expressions that
+don't appear in the GROUP BY clause (like aggregation functions) then the broker will materialize a list of results in
+memory, up to a max of your LIMIT, if any. See the GroupBy documentation for details about tuning performance and memory
+use.
+
+If your query does nested aggregations (an aggregation subquery in your FROM clause) then Druid will execute it as a
+[nested GroupBy](groupbyquery.html#nested-groupbys). In nested GroupBys, the innermost aggregation is distributed, but
+all outer aggregations beyond that take place locally on the query broker.
+
+Semi-join queries containing WHERE clauses like `col IN (SELECT expr FROM ...)` are executed with a special process. The
+broker will first translate the subquery into a GroupBy to find distinct values of `expr`. Then, the broker will rewrite
+the subquery to a literal filter, like `col IN (val1, val2, ...)` and run the outer query. The configuration parameter
+druid.sql.planner.maxSemiJoinRowsInMemory controls the maximum number of values that will be materialized for this kind
+of plan.
+
+For all native query types, filters on the `__time` column will be translated into top-level query "intervals" whenever
+possible, which allows Druid to use its global time index to quickly prune the set of data that must be scanned. In
+addition, Druid will use indexes local to each data node to further speed up WHERE evaluation. This can typically be
+done for filters that involve boolean combinations of references to and functions of single columns, like
+`WHERE col1 = 'a' AND col2 = 'b'`, but not `WHERE col1 = col2`.
+
+### Approximate algorithms
+
+Druid SQL will use approximate algorithms in some situations:
+
+- The `COUNT(DISTINCT col)` aggregation functions by default uses a variant of
+[HyperLogLog](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf), a fast approximate distinct counting
+algorithm. Druid SQL will switch to exact distinct counts if you set "useApproximateCountDistinct" to "false", either
+through query context or through broker configuration.
+- GROUP BY queries over a single column with ORDER BY and LIMIT may be executed using the TopN engine, which uses an
+approximate algorithm. Druid SQL will switch to an exact grouping algorithm if you set "useApproximateTopN" to "false",
+either through query context or through broker configuration.
+- The APPROX_COUNT_DISTINCT and APPROX_QUANTILE aggregation functions always use approximate algorithms, regardless
+of configuration.
-```json
-{
- "query" : "SELECT COUNT(*) FROM data_source WHERE foo = 'bar'"
-}
-```
+## Client APIs
+
+### JSON over HTTP
-You can use _curl_ to send these queries from the command-line:
+You can make Druid SQL queries using JSON over HTTP by posting to the endpoint `/druid/v2/sql/`. The request should
+be a JSON object with a "query" field, like `{"query" : "SELECT COUNT(*) FROM data_source WHERE foo = 'bar'"}`. You can
+use _curl_ to send these queries from the command-line:
```bash
-curl -XPOST -H'Content-Type: application/json' http://BROKER:8082/druid/v2/sql/ -d '{"query":"SELECT COUNT(*) FROM data_source"}'
-```
+$ cat query.json
+{"query":"SELECT COUNT(*) FROM data_source"}
-Metadata is only available over the HTTP API by querying the "INFORMATION_SCHEMA" tables (see below).
+$ curl -XPOST -H'Content-Type: application/json' http://BROKER:8082/druid/v2/sql/ -d @query.json
+[{"EXPR$0":24433}]
+```
-You can provide [connection context parameters](#connection-context) by adding a "context" map, like:
+You can also provide [connection context parameters](#connection-context) by adding a "context" map, like:
```json
{
@@ -82,176 +336,73 @@ You can provide [connection context parameters](#connection-context) by adding a
}
```
-### Metadata
-
-Druid brokers infer table and column metadata for each dataSource from segments loaded in the cluster, and use this to
-plan SQL queries. This metadata is cached on broker startup and also updated periodically in the background through
-[SegmentMetadata queries](../querying/segmentmetadataquery.html). Background metadata refreshing is triggered by
-segments entering and exiting the cluster, and can also be throttled through configuration.
-
-This cached metadata is queryable through "INFORMATION_SCHEMA" tables. For example, to retrieve metadata for the Druid
-datasource "foo", use the query:
-
-```sql
-SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = 'foo'
-```
-
-See the [INFORMATION_SCHEMA tables](#information_schema-tables) section below for details on the available metadata.
-
-You can access table and column metadata through JDBC using `connection.getMetaData()`.
-
-### Approximate queries
-
-The following SQL queries and features may be executed using approximate algorithms:
-
-- `COUNT(DISTINCT col)` and `APPROX_COUNT_DISTINCT(col)` aggregations by default use
-[HyperLogLog](http://algo.inria.fr/flajolet/Publications/FlFuGaMe07.pdf), a fast approximate distinct counting
-algorithm. To disable this behavior for `COUNT(DISTINCT col)`, and use exact distinct counts, set
-"useApproximateCountDistinct" to "false", either through query context or through broker configuration.
-`APPROX_COUNT_DISTINCT(col)` is always approximate, regardless of this setting.
-- TopN-style queries with a single grouping column, like
-`SELECT col1, SUM(col2) FROM data_source GROUP BY col1 ORDER BY SUM(col2) DESC LIMIT 100`, by default will be executed
-as [TopN queries](topnquery.html), which use an approximate algorithm. To disable this behavior, and use exact
-algorithms for topN-style queries, set "useApproximateTopN" to "false", either through query context or through broker
-configuration.
-
-In both cases, the exact algorithms are generally slower and more resource intensive.
-
-### Time functions
+Metadata is available over the HTTP API by querying the ["INFORMATION_SCHEMA" tables](#retrieving-metadata).
-Druid's SQL language supports a number of time operations, including:
+### JDBC
-- `FLOOR(__time TO )` for grouping or filtering on time buckets, like `SELECT FLOOR(__time TO MONTH), SUM(cnt) FROM data_source GROUP BY FLOOR(__time TO MONTH)`
-- `EXTRACT( FROM __time)` for grouping or filtering on time parts, like `SELECT EXTRACT(HOUR FROM __time), SUM(cnt) FROM data_source GROUP BY EXTRACT(HOUR FROM __time)`
-- Comparisons to `TIMESTAMP ''` for time filters, like `SELECT COUNT(*) FROM data_source WHERE __time >= TIMESTAMP '2000-01-01 00:00:00' AND __time < TIMESTAMP '2001-01-01 00:00:00'`
-- `CURRENT_TIMESTAMP` for the current time, usable in filters like `SELECT COUNT(*) FROM data_source WHERE __time >= CURRENT_TIMESTAMP - INTERVAL '1' HOUR`
-
-By default, time operations use the UTC time zone. You can change the time zone for time operations by setting the
-connection context parameter "sqlTimeZone" to the name of the time zone, like "America/Los_Angeles".
-
-### Query-time lookups
-
-Druid [query-time lookups](lookups.html) can be accessed through the `LOOKUP(expression, lookupName)` function. The
-"lookupName" must refer to a lookup you have registered with Druid's lookup framework. For example, the following
-query can be used to perform a groupBy on looked-up values:
-
-```sql
-SELECT LOOKUP(col, 'my_lookup') AS col_with_lookup FROM data_source GROUP BY LOOKUP(col, 'my_lookup')
-```
-
-### Subqueries
+You can make Druid SQL queries using the [Avatica JDBC driver](https://calcite.apache.org/avatica/downloads/). Once
+you've downloaded the Avatica client jar, add it to your classpath and use the connect string
+`jdbc:avatica:remote:url=http://BROKER:8082/druid/v2/sql/avatica/`.
-Druid's SQL layer supports many types of subqueries, including the ones listed below.
+Example code:
-#### Nested groupBy
+```java
+// Connect to /druid/v2/sql/avatica/ on your broker.
+String url = "jdbc:avatica:remote:url=http://localhost:8082/druid/v2/sql/avatica/";
-Subqueries involving `FROM (SELECT ... GROUP BY ...)` may be executed as
-[nested groupBys](groupbyquery.html#nested-groupbys). For example, the following query can be used to perform an
-exact distinct count using a nested groupBy.
+// Set any connection context parameters you need here (see "Connection context" below).
+// Or leave empty for default behavior.
+Properties connectionProperties = new Properties();
-```sql
-SELECT COUNT(*) FROM (SELECT DISTINCT col FROM data_source)
+try (Connection connection = DriverManager.getConnection(url, connectionProperties)) {
+ try (
+ final Statement statement = client.createStatement();
+ final ResultSet resultSet = statement.executeQuery(query)
+ ) {
+ while (resultSet.next()) {
+ // Do something
+ }
+ }
+}
```
-#### Semi-joins
-
-Semi-join subqueries involving `WHERE ... IN (SELECT ...)`, like the following, are executed with a special process.
+Table metadata is available over JDBC using `connection.getMetaData()` or by querying the
+["INFORMATION_SCHEMA" tables](#retrieving-metadata). Parameterized queries (using `?` or other placeholders) don't work properly,
+so avoid those.
-```sql
-SELECT x, COUNT(*)
-FROM data_source_1
-WHERE x IN (SELECT x FROM data_source_2 WHERE y = 'baz')
-GROUP BY x
-```
-
-For this query, the broker will first translate the inner select on data_source_2 into a groupBy to find distinct
-`x` values. Then it'll use those distinct values to build an "in" filter on data_source_1 for the outer query. The
-configuration parameter `druid.sql.planner.maxSemiJoinRowsInMemory` controls the maximum number of values that will be
-materialized for this kind of plan.
+Druid's JDBC server does not share connection state between brokers. This means that if you're using JDBC and have
+multiple Druid brokers, you should either connect to a specific broker, or use a load balancer with sticky sessions
+enabled.
### Connection context
-Druid's SQL layer supports a connection context that influences SQL query planning and Druid native query execution.
-The parameters in the table below affect SQL planning. All other context parameters you provide will be attached to
-Druid queries and can affect how they run. See [Query context](query-context.html) for details on the possible options.
+Druid SQL supports setting connection parameters on the client. The parameters in the table below affect SQL planning.
+All other context parameters you provide will be attached to Druid queries and can affect how they run. See
+[Query context](query-context.html) for details on the possible options.
|Parameter|Description|Default value|
|---------|-----------|-------------|
-|`sqlTimeZone`|Sets the time zone for this connection. Should be a time zone name like "America/Los_Angeles".|UTC|
+|`sqlTimeZone`|Sets the time zone for this connection, which will affect how time functions and timestamp literals behave. Should be a time zone name like "America/Los_Angeles" or offset like "-08:00".|UTC|
|`useApproximateCountDistinct`|Whether to use an approximate cardinalty algorithm for `COUNT(DISTINCT foo)`.|druid.sql.planner.useApproximateCountDistinct on the broker|
|`useApproximateTopN`|Whether to use approximate [TopN queries](topnquery.html) when a SQL query could be expressed as such. If false, exact [GroupBy queries](groupbyquery.html) will be used instead.|druid.sql.planner.useApproximateTopN on the broker|
|`useFallback`|Whether to evaluate operations on the broker when they cannot be expressed as Druid queries. This option is not recommended for production since it can generate unscalable query plans. If false, SQL queries that cannot be translated to Druid queries will fail.|druid.sql.planner.useFallback on the broker|
Connection context can be specified as JDBC connection properties or as a "context" object in the JSON API.
-### Configuration
-
-Druid's SQL layer can be configured through the following properties in common.runtime.properties or the broker's
-runtime.properties. Either location is equivalent since these properties are only respected by the broker.
-
-#### SQL Server Configuration
-
-The broker's [built-in SQL server](../querying/sql.html) can be configured through the following properties.
-
-|Property|Description|Default|
-|--------|-----------|-------|
-|`druid.sql.enable`|Whether to enable SQL at all, including background metadata fetching. If false, this overrides all other SQL-related properties and disables SQL metadata, serving, and planning completely.|false|
-|`druid.sql.avatica.enable`|Whether to enable an Avatica server at `/druid/v2/sql/avatica/`.|true|
-|`druid.sql.avatica.connectionIdleTimeout`|Avatica client connection idle timeout.|PT30M|
-|`druid.sql.avatica.maxConnections`|Maximum number of open connections for the Avatica server. These are not HTTP connections, but are logical client connections that may span multiple HTTP connections.|25|
-|`druid.sql.avatica.maxStatementsPerConnection`|Maximum number of simultaneous open statements per Avatica client connection.|4|
-|`druid.sql.avatica.maxRowsPerFrame`|Maximum number of rows to return in a single JDBC frame. Setting this property to -1 indicates that no row limit should be applied. Clients can optionally specify a row limit in their requests; if a client specifies a row limit, the lesser value of the client-provided limit and `maxRowsPerFrame` will be used.|100,000|
-|`druid.sql.http.enable`|Whether to enable a simple JSON over HTTP route at `/druid/v2/sql/`.|true|
-
-#### SQL Planner Configuration
-
-The broker's [SQL planner](../querying/sql.html) can be configured through the following properties.
-
-|Property|Description|Default|
-|--------|-----------|-------|
-|`druid.sql.planner.maxQueryCount`|Maximum number of queries to issue, including nested queries. Set to 1 to disable sub-queries, or set to 0 for unlimited.|8|
-|`druid.sql.planner.maxSemiJoinRowsInMemory`|Maximum number of rows to keep in memory for executing two-stage semi-join queries like `SELECT * FROM Employee WHERE DeptName IN (SELECT DeptName FROM Dept)`.|100000|
-|`druid.sql.planner.maxTopNLimit`|Maximum threshold for a [TopN query](../querying/topnquery.html). Higher limits will be planned as [GroupBy queries](../querying/groupbyquery.html) instead.|100000|
-|`druid.sql.planner.metadataRefreshPeriod`|Throttle for metadata refreshes.|PT1M|
-|`druid.sql.planner.selectPageSize`|Page size threshold for [Select queries](../querying/select-query.html). Select queries for larger resultsets will be issued back-to-back using pagination.|1000|
-|`druid.sql.planner.useApproximateCountDistinct`|Whether to use an approximate cardinalty algorithm for `COUNT(DISTINCT foo)`.|true|
-|`druid.sql.planner.useApproximateTopN`|Whether to use approximate [TopN queries](../querying/topnquery.html) when a SQL query could be expressed as such. If false, exact [GroupBy queries](../querying/groupbyquery.html) will be used instead.|true|
-|`druid.sql.planner.useFallback`|Whether to evaluate operations on the broker when they cannot be expressed as Druid queries. This option is not recommended for production since it can generate unscalable query plans. If false, SQL queries that cannot be translated to Druid queries will fail.|false|
-
-### Extensions
-
-Some Druid extensions also include SQL language extensions.
+### Retrieving metadata
-If the [approximate histogram extension](../development/extensions-core/approximate-histograms.html) is loaded:
-
-- `APPROX_QUANTILE(column, probability)` or `APPROX_QUANTILE(column, probability, resolution)` on numeric or
-approximate histogram columns computes approximate quantiles. The "probability" should be between 0 and 1 (exclusive).
-The "resolution" is the number of centroids to use for the computation. Higher resolutions will be give more
-precise results but also have higher overhead. If not provided, the default resolution is 50.
-
-### Unsupported features
-
-Druid does not support all SQL features. Most of these are due to missing features in Druid's native JSON-based query
-language. Some unsupported SQL features include:
-
-- Grouping on functions of multiple columns, like concatenation: `SELECT COUNT(*) FROM data_source GROUP BY dim1 || ' ' || dim2`
-- Filtering on non-boolean interactions between columns, like two columns equaling each other: `SELECT COUNT(*) FROM data_source WHERE dim1 = dim2`.
-- A number of miscellaneous functions, like `TRIM`.
-- Joins, other than semi-joins as described above.
-
-Additionally, some Druid features are not supported by the SQL language. Some unsupported Druid features include:
-
-- [Multi-value dimensions](multi-value-dimensions.html).
-- [DataSketches](../development/extensions-core/datasketches-aggregators.html).
-
-## Third-party SQL libraries
-
-A number of third parties have also released SQL libraries for Druid. Links to popular options can be found on
-our [libraries](/libraries.html) page. These libraries make native Druid JSON queries and do not use Druid's SQL layer.
+Druid brokers infer table and column metadata for each dataSource from segments loaded in the cluster, and use this to
+plan SQL queries. This metadata is cached on broker startup and also updated periodically in the background through
+[SegmentMetadata queries](segmentmetadataquery.html). Background metadata refreshing is triggered by
+segments entering and exiting the cluster, and can also be throttled through configuration.
-## INFORMATION_SCHEMA tables
+You can access table and column metadata through JDBC using `connection.getMetaData()`, or through the
+INFORMATION_SCHEMA tables described below. For example, to retrieve metadata for the Druid
+datasource "foo", use the query:
-Druid metadata is queryable through "INFORMATION_SCHEMA" tables described below.
+```sql
+SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = 'foo'
+```
### SCHEMATA table
@@ -295,3 +446,25 @@ Druid metadata is queryable through "INFORMATION_SCHEMA" tables described below.
|CHARACTER_SET_NAME||
|COLLATION_NAME||
|JDBC_TYPE|Type code from java.sql.Types (Druid extension)|
+
+## Server configuration
+
+The Druid SQL server is configured through the following properties on the broker.
+
+|Property|Description|Default|
+|--------|-----------|-------|
+|`druid.sql.enable`|Whether to enable SQL at all, including background metadata fetching. If false, this overrides all other SQL-related properties and disables SQL metadata, serving, and planning completely.|false|
+|`druid.sql.avatica.enable`|Whether to enable JDBC querying at `/druid/v2/sql/avatica/`.|true|
+|`druid.sql.avatica.maxConnections`|Maximum number of open connections for the Avatica server. These are not HTTP connections, but are logical client connections that may span multiple HTTP connections.|50|
+|`druid.sql.avatica.maxRowsPerFrame`|Maximum number of rows to return in a single JDBC frame. Setting this property to -1 indicates that no row limit should be applied. Clients can optionally specify a row limit in their requests; if a client specifies a row limit, the lesser value of the client-provided limit and `maxRowsPerFrame` will be used.|100,000|
+|`druid.sql.avatica.maxStatementsPerConnection`|Maximum number of simultaneous open statements per Avatica client connection.|1|
+|`druid.sql.avatica.connectionIdleTimeout`|Avatica client connection idle timeout.|PT5M|
+|`druid.sql.http.enable`|Whether to enable JSON over HTTP querying at `/druid/v2/sql/`.|true|
+|`druid.sql.planner.maxQueryCount`|Maximum number of queries to issue, including nested queries. Set to 1 to disable sub-queries, or set to 0 for unlimited.|8|
+|`druid.sql.planner.maxSemiJoinRowsInMemory`|Maximum number of rows to keep in memory for executing two-stage semi-join queries like `SELECT * FROM Employee WHERE DeptName IN (SELECT DeptName FROM Dept)`.|100000|
+|`druid.sql.planner.maxTopNLimit`|Maximum threshold for a [TopN query](../querying/topnquery.html). Higher limits will be planned as [GroupBy queries](../querying/groupbyquery.html) instead.|100000|
+|`druid.sql.planner.metadataRefreshPeriod`|Throttle for metadata refreshes.|PT1M|
+|`druid.sql.planner.selectPageSize`|Page size threshold for [Select queries](../querying/select-query.html). Select queries for larger resultsets will be issued back-to-back using pagination.|1000|
+|`druid.sql.planner.useApproximateCountDistinct`|Whether to use an approximate cardinalty algorithm for `COUNT(DISTINCT foo)`.|true|
+|`druid.sql.planner.useApproximateTopN`|Whether to use approximate [TopN queries](../querying/topnquery.html) when a SQL query could be expressed as such. If false, exact [GroupBy queries](../querying/groupbyquery.html) will be used instead.|true|
+|`druid.sql.planner.useFallback`|Whether to evaluate operations on the broker when they cannot be expressed as Druid queries. This option is not recommended for production since it can generate unscalable query plans. If false, SQL queries that cannot be translated to Druid queries will fail.|false|
diff --git a/extensions-core/histogram/src/main/java/io/druid/query/aggregation/histogram/sql/QuantileSqlAggregator.java b/extensions-core/histogram/src/main/java/io/druid/query/aggregation/histogram/sql/QuantileSqlAggregator.java
index 94a704e4393c..d0578bb55e37 100644
--- a/extensions-core/histogram/src/main/java/io/druid/query/aggregation/histogram/sql/QuantileSqlAggregator.java
+++ b/extensions-core/histogram/src/main/java/io/druid/query/aggregation/histogram/sql/QuantileSqlAggregator.java
@@ -19,7 +19,6 @@
package io.druid.query.aggregation.histogram.sql;
-import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import io.druid.java.util.common.StringUtils;
import io.druid.query.aggregation.AggregatorFactory;
@@ -28,13 +27,14 @@
import io.druid.query.aggregation.histogram.ApproximateHistogramFoldingAggregatorFactory;
import io.druid.query.aggregation.histogram.QuantilePostAggregator;
import io.druid.query.filter.DimFilter;
+import io.druid.segment.VirtualColumn;
import io.druid.segment.column.ValueType;
+import io.druid.segment.virtual.ExpressionVirtualColumn;
import io.druid.sql.calcite.aggregation.Aggregation;
import io.druid.sql.calcite.aggregation.Aggregations;
import io.druid.sql.calcite.aggregation.SqlAggregator;
+import io.druid.sql.calcite.expression.DruidExpression;
import io.druid.sql.calcite.expression.Expressions;
-import io.druid.sql.calcite.expression.RowExtraction;
-import io.druid.sql.calcite.planner.DruidOperatorTable;
import io.druid.sql.calcite.planner.PlannerContext;
import io.druid.sql.calcite.table.RowSignature;
import org.apache.calcite.rel.core.AggregateCall;
@@ -49,6 +49,7 @@
import org.apache.calcite.sql.type.SqlTypeFamily;
import org.apache.calcite.sql.type.SqlTypeName;
+import java.util.ArrayList;
import java.util.List;
public class QuantileSqlAggregator implements SqlAggregator
@@ -66,7 +67,6 @@ public SqlAggFunction calciteFunction()
public Aggregation toDruidAggregation(
final String name,
final RowSignature rowSignature,
- final DruidOperatorTable operatorTable,
final PlannerContext plannerContext,
final List existingAggregations,
final Project project,
@@ -74,16 +74,16 @@ public Aggregation toDruidAggregation(
final DimFilter filter
)
{
- final RowExtraction rex = Expressions.toRowExtraction(
+ final DruidExpression input = Expressions.toDruidExpression(
plannerContext,
- rowSignature.getRowOrder(),
+ rowSignature,
Expressions.fromFieldAccess(
rowSignature,
project,
aggregateCall.getArgList().get(0)
)
);
- if (rex == null) {
+ if (input == null) {
return null;
}
@@ -119,17 +119,32 @@ public Aggregation toDruidAggregation(
factory,
filter,
ApproximateHistogramAggregatorFactory.class,
- new Predicate()
- {
- @Override
- public boolean apply(final ApproximateHistogramAggregatorFactory theFactory)
- {
- return theFactory.getFieldName().equals(rex.getColumn())
- && theFactory.getResolution() == resolution
- && theFactory.getNumBuckets() == numBuckets
- && theFactory.getLowerLimit() == lowerLimit
- && theFactory.getUpperLimit() == upperLimit;
+ theFactory -> {
+ // Check input for equivalence.
+ final boolean inputMatches;
+ final VirtualColumn virtualInput = existing.getVirtualColumns()
+ .stream()
+ .filter(
+ virtualColumn ->
+ virtualColumn.getOutputName()
+ .equals(theFactory.getFieldName())
+ )
+ .findFirst()
+ .orElse(null);
+
+ if (virtualInput == null) {
+ inputMatches = input.isDirectColumnAccess()
+ && input.getDirectColumn().equals(theFactory.getFieldName());
+ } else {
+ inputMatches = ((ExpressionVirtualColumn) virtualInput).getExpression()
+ .equals(input.getExpression());
}
+
+ return inputMatches
+ && theFactory.getResolution() == resolution
+ && theFactory.getNumBuckets() == numBuckets
+ && theFactory.getLowerLimit() == lowerLimit
+ && theFactory.getUpperLimit() == upperLimit;
}
);
@@ -143,19 +158,39 @@ public boolean apply(final ApproximateHistogramAggregatorFactory theFactory)
}
}
- if (rowSignature.getColumnType(rex.getColumn()) == ValueType.COMPLEX) {
- aggregatorFactory = new ApproximateHistogramFoldingAggregatorFactory(
- histogramName,
- rex.getColumn(),
- resolution,
- numBuckets,
- lowerLimit,
- upperLimit
- );
+ // No existing match found. Create a new one.
+ final List virtualColumns = new ArrayList<>();
+
+ if (input.isDirectColumnAccess()) {
+ if (rowSignature.getColumnType(input.getDirectColumn()) == ValueType.COMPLEX) {
+ aggregatorFactory = new ApproximateHistogramFoldingAggregatorFactory(
+ histogramName,
+ input.getDirectColumn(),
+ resolution,
+ numBuckets,
+ lowerLimit,
+ upperLimit
+ );
+ } else {
+ aggregatorFactory = new ApproximateHistogramAggregatorFactory(
+ histogramName,
+ input.getDirectColumn(),
+ resolution,
+ numBuckets,
+ lowerLimit,
+ upperLimit
+ );
+ }
} else {
+ final ExpressionVirtualColumn virtualColumn = input.toVirtualColumn(
+ String.format("%s:v", name),
+ ValueType.FLOAT,
+ plannerContext.getExprMacroTable()
+ );
+ virtualColumns.add(virtualColumn);
aggregatorFactory = new ApproximateHistogramAggregatorFactory(
histogramName,
- rex.getColumn(),
+ virtualColumn.getOutputName(),
resolution,
numBuckets,
lowerLimit,
@@ -164,6 +199,7 @@ public boolean apply(final ApproximateHistogramAggregatorFactory theFactory)
}
return Aggregation.create(
+ virtualColumns,
ImmutableList.of(aggregatorFactory),
new QuantilePostAggregator(name, histogramName, probability)
).filter(filter);
diff --git a/extensions-core/histogram/src/test/java/io/druid/query/aggregation/histogram/sql/QuantileSqlAggregatorTest.java b/extensions-core/histogram/src/test/java/io/druid/query/aggregation/histogram/sql/QuantileSqlAggregatorTest.java
index ead58242e977..be2a4699e53e 100644
--- a/extensions-core/histogram/src/test/java/io/druid/query/aggregation/histogram/sql/QuantileSqlAggregatorTest.java
+++ b/extensions-core/histogram/src/test/java/io/druid/query/aggregation/histogram/sql/QuantileSqlAggregatorTest.java
@@ -35,16 +35,19 @@
import io.druid.query.aggregation.histogram.ApproximateHistogramDruidModule;
import io.druid.query.aggregation.histogram.ApproximateHistogramFoldingAggregatorFactory;
import io.druid.query.aggregation.histogram.QuantilePostAggregator;
+import io.druid.query.expression.TestExprMacroTable;
import io.druid.query.filter.NotDimFilter;
import io.druid.query.filter.SelectorDimFilter;
import io.druid.query.spec.MultipleIntervalSegmentSpec;
import io.druid.segment.IndexBuilder;
import io.druid.segment.QueryableIndex;
import io.druid.segment.TestHelper;
+import io.druid.segment.column.ValueType;
import io.druid.segment.incremental.IncrementalIndexSchema;
+import io.druid.segment.virtual.ExpressionVirtualColumn;
import io.druid.server.initialization.ServerConfig;
import io.druid.sql.calcite.aggregation.SqlAggregator;
-import io.druid.sql.calcite.expression.SqlExtractionOperator;
+import io.druid.sql.calcite.expression.SqlOperatorConversion;
import io.druid.sql.calcite.filtration.Filtration;
import io.druid.sql.calcite.planner.Calcites;
import io.druid.sql.calcite.planner.DruidOperatorTable;
@@ -131,7 +134,7 @@ public void setUp() throws Exception
);
final DruidOperatorTable operatorTable = new DruidOperatorTable(
ImmutableSet.of(new QuantileSqlAggregator()),
- ImmutableSet.of()
+ ImmutableSet.of()
);
plannerFactory = new PlannerFactory(
rootSchema,
@@ -159,6 +162,7 @@ public void testQuantileOnFloatAndLongs() throws Exception
+ "APPROX_QUANTILE(m1, 0.5, 50),\n"
+ "APPROX_QUANTILE(m1, 0.98, 200),\n"
+ "APPROX_QUANTILE(m1, 0.99),\n"
+ + "APPROX_QUANTILE(m1 * 2, 0.97),\n"
+ "APPROX_QUANTILE(m1, 0.99) FILTER(WHERE dim1 = 'abc'),\n"
+ "APPROX_QUANTILE(m1, 0.999) FILTER(WHERE dim1 <> 'abc'),\n"
+ "APPROX_QUANTILE(m1, 0.999) FILTER(WHERE dim1 = 'abc'),\n"
@@ -170,7 +174,17 @@ public void testQuantileOnFloatAndLongs() throws Exception
// Verify results
final List results = Sequences.toList(plannerResult.run(), new ArrayList());
final List expectedResults = ImmutableList.of(
- new Object[]{1.0, 3.0, 5.880000114440918, 5.940000057220459, 6.0, 4.994999885559082, 6.0, 1.0}
+ new Object[]{
+ 1.0,
+ 3.0,
+ 5.880000114440918,
+ 5.940000057220459,
+ 11.640000343322754,
+ 6.0,
+ 4.994999885559082,
+ 6.0,
+ 1.0
+ }
);
Assert.assertEquals(expectedResults.size(), results.size());
for (int i = 0; i < expectedResults.size(); i++) {
@@ -183,28 +197,38 @@ public void testQuantileOnFloatAndLongs() throws Exception
.dataSource(CalciteTests.DATASOURCE1)
.intervals(new MultipleIntervalSegmentSpec(ImmutableList.of(Filtration.eternity())))
.granularity(Granularities.ALL)
+ .virtualColumns(
+ new ExpressionVirtualColumn(
+ "a4:v",
+ "(\"m1\" * 2)",
+ ValueType.FLOAT,
+ TestExprMacroTable.INSTANCE
+ )
+ )
.aggregators(ImmutableList.of(
new ApproximateHistogramAggregatorFactory("a0:agg", "m1", null, null, null, null),
new ApproximateHistogramAggregatorFactory("a2:agg", "m1", 200, null, null, null),
+ new ApproximateHistogramAggregatorFactory("a4:agg", "a4:v", null, null, null, null),
new FilteredAggregatorFactory(
- new ApproximateHistogramAggregatorFactory("a4:agg", "m1", null, null, null, null),
+ new ApproximateHistogramAggregatorFactory("a5:agg", "m1", null, null, null, null),
new SelectorDimFilter("dim1", "abc", null)
),
new FilteredAggregatorFactory(
- new ApproximateHistogramAggregatorFactory("a5:agg", "m1", null, null, null, null),
+ new ApproximateHistogramAggregatorFactory("a6:agg", "m1", null, null, null, null),
new NotDimFilter(new SelectorDimFilter("dim1", "abc", null))
),
- new ApproximateHistogramAggregatorFactory("a7:agg", "cnt", null, null, null, null)
+ new ApproximateHistogramAggregatorFactory("a8:agg", "cnt", null, null, null, null)
))
.postAggregators(ImmutableList.of(
new QuantilePostAggregator("a0", "a0:agg", 0.01f),
new QuantilePostAggregator("a1", "a0:agg", 0.50f),
new QuantilePostAggregator("a2", "a2:agg", 0.98f),
new QuantilePostAggregator("a3", "a0:agg", 0.99f),
- new QuantilePostAggregator("a4", "a4:agg", 0.99f),
- new QuantilePostAggregator("a5", "a5:agg", 0.999f),
- new QuantilePostAggregator("a6", "a4:agg", 0.999f),
- new QuantilePostAggregator("a7", "a7:agg", 0.50f)
+ new QuantilePostAggregator("a4", "a4:agg", 0.97f),
+ new QuantilePostAggregator("a5", "a5:agg", 0.99f),
+ new QuantilePostAggregator("a6", "a6:agg", 0.999f),
+ new QuantilePostAggregator("a7", "a5:agg", 0.999f),
+ new QuantilePostAggregator("a8", "a8:agg", 0.50f)
))
.context(ImmutableMap.of(
"skipEmptyBuckets", true,
diff --git a/pom.xml b/pom.xml
index eb8e844bb700..33587ebb1395 100644
--- a/pom.xml
+++ b/pom.xml
@@ -61,7 +61,7 @@
2.11.0
1.9.0
- 1.11.0
+ 1.12.0
16.0.1
4.1.0
9.3.19.v20170502
diff --git a/sql/src/main/java/io/druid/sql/calcite/aggregation/Aggregation.java b/sql/src/main/java/io/druid/sql/calcite/aggregation/Aggregation.java
index 64d4fb1ac274..35824949076b 100644
--- a/sql/src/main/java/io/druid/sql/calcite/aggregation/Aggregation.java
+++ b/sql/src/main/java/io/druid/sql/calcite/aggregation/Aggregation.java
@@ -30,25 +30,27 @@
import io.druid.query.aggregation.FilteredAggregatorFactory;
import io.druid.query.aggregation.PostAggregator;
import io.druid.query.filter.DimFilter;
+import io.druid.segment.VirtualColumn;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
public class Aggregation
{
+ private final List virtualColumns;
private final List aggregatorFactories;
private final PostAggregator postAggregator;
- private final PostAggregatorFactory finalizingPostAggregatorFactory;
private Aggregation(
+ final List virtualColumns,
final List aggregatorFactories,
- final PostAggregator postAggregator,
- final PostAggregatorFactory finalizingPostAggregatorFactory
+ final PostAggregator postAggregator
)
{
+ this.virtualColumns = Preconditions.checkNotNull(virtualColumns, "virtualColumns");
this.aggregatorFactories = Preconditions.checkNotNull(aggregatorFactories, "aggregatorFactories");
this.postAggregator = postAggregator;
- this.finalizingPostAggregatorFactory = finalizingPostAggregatorFactory;
if (postAggregator == null) {
Preconditions.checkArgument(aggregatorFactories.size() == 1, "aggregatorFactories.size == 1");
@@ -62,16 +64,47 @@ private Aggregation(
}
}
}
+
+ // Verify that all "internal" aggregator names are prefixed by the output name of this aggregation.
+ // This is a sanity check to make sure callers are behaving as they should be.
+ final String name = postAggregator != null
+ ? postAggregator.getName()
+ : Iterables.getOnlyElement(aggregatorFactories).getName();
+
+ for (VirtualColumn virtualColumn : virtualColumns) {
+ if (!virtualColumn.getOutputName().startsWith(name)) {
+ throw new IAE("VirtualColumn[%s] not prefixed under[%s]", virtualColumn.getOutputName(), name);
+ }
+ }
+
+ for (AggregatorFactory aggregatorFactory : aggregatorFactories) {
+ if (!aggregatorFactory.getName().startsWith(name)) {
+ throw new IAE("Aggregator[%s] not prefixed under[%s]", aggregatorFactory.getName(), name);
+ }
+ }
+ }
+
+ public static Aggregation create(final List virtualColumns, final AggregatorFactory aggregatorFactory)
+ {
+ return new Aggregation(
+ virtualColumns,
+ ImmutableList.of(aggregatorFactory),
+ null
+ );
}
public static Aggregation create(final AggregatorFactory aggregatorFactory)
{
- return new Aggregation(ImmutableList.of(aggregatorFactory), null, null);
+ return new Aggregation(
+ ImmutableList.of(),
+ ImmutableList.of(aggregatorFactory),
+ null
+ );
}
public static Aggregation create(final PostAggregator postAggregator)
{
- return new Aggregation(ImmutableList.of(), postAggregator, null);
+ return new Aggregation(ImmutableList.of(), ImmutableList.of(), postAggregator);
}
public static Aggregation create(
@@ -79,20 +112,21 @@ public static Aggregation create(
final PostAggregator postAggregator
)
{
- return new Aggregation(aggregatorFactories, postAggregator, null);
+ return new Aggregation(ImmutableList.of(), aggregatorFactories, postAggregator);
}
- public static Aggregation createFinalizable(
+ public static Aggregation create(
+ final List virtualColumns,
final List aggregatorFactories,
- final PostAggregator postAggregator,
- final PostAggregatorFactory finalizingPostAggregatorFactory
+ final PostAggregator postAggregator
)
{
- return new Aggregation(
- aggregatorFactories,
- postAggregator,
- Preconditions.checkNotNull(finalizingPostAggregatorFactory, "finalizingPostAggregatorFactory")
- );
+ return new Aggregation(virtualColumns, aggregatorFactories, postAggregator);
+ }
+
+ public List getVirtualColumns()
+ {
+ return virtualColumns;
}
public List getAggregatorFactories()
@@ -105,11 +139,6 @@ public PostAggregator getPostAggregator()
return postAggregator;
}
- public PostAggregatorFactory getFinalizingPostAggregatorFactory()
- {
- return finalizingPostAggregatorFactory;
- }
-
public String getOutputName()
{
return postAggregator != null
@@ -124,7 +153,8 @@ public Aggregation filter(final DimFilter filter)
}
if (postAggregator != null) {
- // Verify that this Aggregation contains all inputs. If not, this "filter" call won't work right.
+ // Verify that this Aggregation contains all input to its postAggregator.
+ // If not, this "filter" call won't work right.
final Set dependentFields = postAggregator.getDependentFields();
final Set aggregatorNames = Sets.newHashSet();
for (AggregatorFactory aggregatorFactory : aggregatorFactories) {
@@ -138,20 +168,15 @@ public Aggregation filter(final DimFilter filter)
}
final List newAggregators = Lists.newArrayList();
-
for (AggregatorFactory agg : aggregatorFactories) {
newAggregators.add(new FilteredAggregatorFactory(agg, filter));
}
- return new Aggregation(
- newAggregators,
- postAggregator,
- finalizingPostAggregatorFactory
- );
+ return new Aggregation(virtualColumns, newAggregators, postAggregator);
}
@Override
- public boolean equals(Object o)
+ public boolean equals(final Object o)
{
if (this == o) {
return true;
@@ -159,38 +184,25 @@ public boolean equals(Object o)
if (o == null || getClass() != o.getClass()) {
return false;
}
-
- Aggregation that = (Aggregation) o;
-
- if (aggregatorFactories != null
- ? !aggregatorFactories.equals(that.aggregatorFactories)
- : that.aggregatorFactories != null) {
- return false;
- }
- if (postAggregator != null ? !postAggregator.equals(that.postAggregator) : that.postAggregator != null) {
- return false;
- }
- return finalizingPostAggregatorFactory != null
- ? finalizingPostAggregatorFactory.equals(that.finalizingPostAggregatorFactory)
- : that.finalizingPostAggregatorFactory == null;
+ final Aggregation that = (Aggregation) o;
+ return Objects.equals(virtualColumns, that.virtualColumns) &&
+ Objects.equals(aggregatorFactories, that.aggregatorFactories) &&
+ Objects.equals(postAggregator, that.postAggregator);
}
@Override
public int hashCode()
{
- int result = aggregatorFactories != null ? aggregatorFactories.hashCode() : 0;
- result = 31 * result + (postAggregator != null ? postAggregator.hashCode() : 0);
- result = 31 * result + (finalizingPostAggregatorFactory != null ? finalizingPostAggregatorFactory.hashCode() : 0);
- return result;
+ return Objects.hash(virtualColumns, aggregatorFactories, postAggregator);
}
@Override
public String toString()
{
return "Aggregation{" +
- "aggregatorFactories=" + aggregatorFactories +
+ "virtualColumns=" + virtualColumns +
+ ", aggregatorFactories=" + aggregatorFactories +
", postAggregator=" + postAggregator +
- ", finalizingPostAggregatorFactory=" + finalizingPostAggregatorFactory +
'}';
}
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/aggregation/ApproxCountDistinctSqlAggregator.java b/sql/src/main/java/io/druid/sql/calcite/aggregation/ApproxCountDistinctSqlAggregator.java
index 507496e33bae..fff3aa132773 100644
--- a/sql/src/main/java/io/druid/sql/calcite/aggregation/ApproxCountDistinctSqlAggregator.java
+++ b/sql/src/main/java/io/druid/sql/calcite/aggregation/ApproxCountDistinctSqlAggregator.java
@@ -23,17 +23,17 @@
import com.google.common.collect.Iterables;
import io.druid.java.util.common.ISE;
import io.druid.query.aggregation.AggregatorFactory;
-import io.druid.query.aggregation.PostAggregator;
import io.druid.query.aggregation.cardinality.CardinalityAggregatorFactory;
-import io.druid.query.aggregation.hyperloglog.HyperUniqueFinalizingPostAggregator;
import io.druid.query.aggregation.hyperloglog.HyperUniquesAggregatorFactory;
+import io.druid.query.dimension.DefaultDimensionSpec;
import io.druid.query.dimension.DimensionSpec;
import io.druid.query.filter.DimFilter;
+import io.druid.segment.VirtualColumn;
import io.druid.segment.column.ValueType;
+import io.druid.segment.virtual.ExpressionVirtualColumn;
+import io.druid.sql.calcite.expression.DruidExpression;
import io.druid.sql.calcite.expression.Expressions;
-import io.druid.sql.calcite.expression.RowExtraction;
import io.druid.sql.calcite.planner.Calcites;
-import io.druid.sql.calcite.planner.DruidOperatorTable;
import io.druid.sql.calcite.planner.PlannerContext;
import io.druid.sql.calcite.table.RowSignature;
import org.apache.calcite.rel.core.AggregateCall;
@@ -47,6 +47,7 @@
import org.apache.calcite.sql.type.ReturnTypes;
import org.apache.calcite.sql.type.SqlTypeName;
+import java.util.ArrayList;
import java.util.List;
public class ApproxCountDistinctSqlAggregator implements SqlAggregator
@@ -64,7 +65,6 @@ public SqlAggFunction calciteFunction()
public Aggregation toDruidAggregation(
final String name,
final RowSignature rowSignature,
- final DruidOperatorTable operatorTable,
final PlannerContext plannerContext,
final List existingAggregations,
final Project project,
@@ -77,46 +77,45 @@ public Aggregation toDruidAggregation(
project,
Iterables.getOnlyElement(aggregateCall.getArgList())
);
- final RowExtraction rex = Expressions.toRowExtraction(
+ final DruidExpression input = Expressions.toDruidExpression(
plannerContext,
- rowSignature.getRowOrder(),
+ rowSignature,
rexNode
);
- if (rex == null) {
+ if (input == null) {
return null;
}
+ final List virtualColumns = new ArrayList<>();
final AggregatorFactory aggregatorFactory;
- if (rowSignature.getColumnType(rex.getColumn()) == ValueType.COMPLEX) {
- aggregatorFactory = new HyperUniquesAggregatorFactory(name, rex.getColumn());
+ if (input.isDirectColumnAccess() && rowSignature.getColumnType(input.getDirectColumn()) == ValueType.COMPLEX) {
+ aggregatorFactory = new HyperUniquesAggregatorFactory(name, input.getDirectColumn());
} else {
final SqlTypeName sqlTypeName = rexNode.getType().getSqlTypeName();
- final ValueType outputType = Calcites.getValueTypeForSqlTypeName(sqlTypeName);
- if (outputType == null) {
+ final ValueType inputType = Calcites.getValueTypeForSqlTypeName(sqlTypeName);
+ if (inputType == null) {
throw new ISE("Cannot translate sqlTypeName[%s] to Druid type for field[%s]", sqlTypeName, name);
}
- final DimensionSpec dimensionSpec = rex.toDimensionSpec(rowSignature, null, ValueType.STRING);
- if (dimensionSpec == null) {
- return null;
+ final DimensionSpec dimensionSpec;
+
+ if (input.isSimpleExtraction()) {
+ dimensionSpec = input.getSimpleExtraction().toDimensionSpec(null, ValueType.STRING);
+ } else {
+ final ExpressionVirtualColumn virtualColumn = input.toVirtualColumn(
+ String.format("%s:v", name),
+ inputType,
+ plannerContext.getExprMacroTable()
+ );
+ dimensionSpec = new DefaultDimensionSpec(virtualColumn.getOutputName(), null, inputType);
+ virtualColumns.add(virtualColumn);
}
aggregatorFactory = new CardinalityAggregatorFactory(name, ImmutableList.of(dimensionSpec), false);
}
- return Aggregation.createFinalizable(
- ImmutableList.of(aggregatorFactory),
- null,
- new PostAggregatorFactory()
- {
- @Override
- public PostAggregator factorize(String outputName)
- {
- return new HyperUniqueFinalizingPostAggregator(outputName, name);
- }
- }
- ).filter(filter);
+ return Aggregation.create(virtualColumns, aggregatorFactory).filter(filter);
}
private static class ApproxCountDistinctSqlAggFunction extends SqlAggFunction
diff --git a/sql/src/main/java/io/druid/sql/calcite/aggregation/DimensionExpression.java b/sql/src/main/java/io/druid/sql/calcite/aggregation/DimensionExpression.java
new file mode 100644
index 000000000000..2a7ee4c49ad9
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/aggregation/DimensionExpression.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.aggregation;
+
+import com.google.common.collect.ImmutableList;
+import io.druid.math.expr.ExprMacroTable;
+import io.druid.query.dimension.DefaultDimensionSpec;
+import io.druid.query.dimension.DimensionSpec;
+import io.druid.segment.VirtualColumn;
+import io.druid.segment.column.ValueType;
+import io.druid.sql.calcite.expression.DruidExpression;
+
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.Objects;
+
+public class DimensionExpression
+{
+ private final String outputName;
+ private final DruidExpression expression;
+ private final ValueType outputType;
+
+ public DimensionExpression(
+ final String outputName,
+ final DruidExpression expression,
+ final ValueType outputType
+ )
+ {
+ this.outputName = outputName;
+ this.expression = expression;
+ this.outputType = outputType;
+ }
+
+ public String getOutputName()
+ {
+ return outputName;
+ }
+
+ public DruidExpression getDruidExpression()
+ {
+ return expression;
+ }
+
+ public ValueType getOutputType()
+ {
+ return outputType;
+ }
+
+ public DimensionSpec toDimensionSpec()
+ {
+ if (expression.isSimpleExtraction()) {
+ return expression.getSimpleExtraction().toDimensionSpec(outputName, outputType);
+ } else {
+ return new DefaultDimensionSpec(getVirtualColumnName(), getOutputName(), outputType);
+ }
+ }
+
+ public List getVirtualColumns(final ExprMacroTable macroTable)
+ {
+ if (expression.isSimpleExtraction()) {
+ return ImmutableList.of();
+ } else {
+ return ImmutableList.of(expression.toVirtualColumn(getVirtualColumnName(), outputType, macroTable));
+ }
+ }
+
+ @Nullable
+ public String getVirtualColumnName()
+ {
+ return expression.isSimpleExtraction() ? null : String.format("%s:v", outputName);
+ }
+
+ @Override
+ public boolean equals(final Object o)
+ {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final DimensionExpression that = (DimensionExpression) o;
+ return Objects.equals(outputName, that.outputName) &&
+ Objects.equals(expression, that.expression) &&
+ outputType == that.outputType;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(outputName, expression, outputType);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "DimensionExpression{" +
+ "outputName='" + outputName + '\'' +
+ ", expression=" + expression +
+ ", outputType=" + outputType +
+ '}';
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/aggregation/SqlAggregator.java b/sql/src/main/java/io/druid/sql/calcite/aggregation/SqlAggregator.java
index 2d333b427eac..9539762ce09d 100644
--- a/sql/src/main/java/io/druid/sql/calcite/aggregation/SqlAggregator.java
+++ b/sql/src/main/java/io/druid/sql/calcite/aggregation/SqlAggregator.java
@@ -20,7 +20,6 @@
package io.druid.sql.calcite.aggregation;
import io.druid.query.filter.DimFilter;
-import io.druid.sql.calcite.planner.DruidOperatorTable;
import io.druid.sql.calcite.planner.PlannerContext;
import io.druid.sql.calcite.table.RowSignature;
import org.apache.calcite.rel.core.AggregateCall;
@@ -47,7 +46,6 @@ public interface SqlAggregator
*
* @param name desired output name of the aggregation
* @param rowSignature signature of the rows being aggregated
- * @param operatorTable Operator table that can be used to convert sub-expressions
* @param plannerContext SQL planner context
* @param existingAggregations existing aggregations for this query; useful for re-using aggregations. May be safely
* ignored if you do not want to re-use existing aggregations.
@@ -61,7 +59,6 @@ public interface SqlAggregator
Aggregation toDruidAggregation(
final String name,
final RowSignature rowSignature,
- final DruidOperatorTable operatorTable,
final PlannerContext plannerContext,
final List existingAggregations,
final Project project,
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/CeilOperatorConversion.java b/sql/src/main/java/io/druid/sql/calcite/expression/CeilOperatorConversion.java
new file mode 100644
index 000000000000..10bca1f43b39
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/CeilOperatorConversion.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import com.google.common.collect.ImmutableList;
+import io.druid.java.util.common.granularity.PeriodGranularity;
+import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.avatica.util.TimeUnitRange;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+
+import java.util.stream.Collectors;
+
+public class CeilOperatorConversion implements SqlOperatorConversion
+{
+ @Override
+ public SqlOperator calciteOperator()
+ {
+ return SqlStdOperatorTable.CEIL;
+ }
+
+ @Override
+ public DruidExpression toDruidExpression(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
+ {
+ final RexCall call = (RexCall) rexNode;
+ final RexNode arg = call.getOperands().get(0);
+ final DruidExpression druidExpression = Expressions.toDruidExpression(
+ plannerContext,
+ rowSignature,
+ arg
+ );
+ if (druidExpression == null) {
+ return null;
+ } else if (call.getOperands().size() == 1) {
+ // CEIL(expr)
+ return druidExpression.map(
+ simpleExtraction -> null,
+ expression -> String.format("ceil(%s)", expression)
+ );
+ } else if (call.getOperands().size() == 2) {
+ // CEIL(expr TO timeUnit)
+ final RexLiteral flag = (RexLiteral) call.getOperands().get(1);
+ final TimeUnitRange timeUnit = (TimeUnitRange) flag.getValue();
+ final PeriodGranularity granularity = TimeUnits.toQueryGranularity(timeUnit, plannerContext.getTimeZone());
+ if (granularity == null) {
+ return null;
+ }
+
+ // Unlike FLOOR(expr TO timeUnit) there is no built-in extractionFn that can behave like timestamp_ceil.
+ // So there is no simple extraction for this operator.
+ return DruidExpression.fromFunctionCall(
+ "timestamp_ceil",
+ ImmutableList.of(
+ druidExpression.getExpression(),
+ DruidExpression.stringLiteral(granularity.getPeriod().toString()),
+ DruidExpression.numberLiteral(
+ granularity.getOrigin() == null ? null : granularity.getOrigin().getMillis()
+ ),
+ DruidExpression.stringLiteral(granularity.getTimeZone().toString())
+ ).stream().map(DruidExpression::fromExpression).collect(Collectors.toList())
+ );
+ } else {
+ // WTF? CEIL with 3 arguments?
+ return null;
+ }
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/DruidExpression.java b/sql/src/main/java/io/druid/sql/calcite/expression/DruidExpression.java
new file mode 100644
index 000000000000..e3cac19bafd7
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/DruidExpression.java
@@ -0,0 +1,213 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import com.google.common.base.Preconditions;
+import com.google.common.io.BaseEncoding;
+import com.google.common.primitives.Chars;
+import io.druid.math.expr.Expr;
+import io.druid.math.expr.ExprMacroTable;
+import io.druid.math.expr.Parser;
+import io.druid.segment.column.ValueType;
+import io.druid.segment.virtual.ExpressionVirtualColumn;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+
+/**
+ * Represents three kinds of expression-like concepts that native Druid queries support:
+ *
+ * (1) SimpleExtractions, which are direct column access, possibly with an extractionFn
+ * (2) native Druid expressions.
+ */
+public class DruidExpression
+{
+ // Must be sorted
+ private static final char[] SAFE_CHARS = " ,._-;:(){}[]<>!@#$%^&*`~?/".toCharArray();
+
+ static {
+ Arrays.sort(SAFE_CHARS);
+ }
+
+ private final SimpleExtraction simpleExtraction;
+ private final String expression;
+
+ private DruidExpression(final SimpleExtraction simpleExtraction, final String expression)
+ {
+ this.simpleExtraction = simpleExtraction;
+ this.expression = Preconditions.checkNotNull(expression);
+ }
+
+ public static DruidExpression of(final SimpleExtraction simpleExtraction, final String expression)
+ {
+ return new DruidExpression(simpleExtraction, expression);
+ }
+
+ public static DruidExpression fromColumn(final String column)
+ {
+ return new DruidExpression(SimpleExtraction.of(column, null), String.format("\"%s\"", escape(column)));
+ }
+
+ public static DruidExpression fromExpression(final String expression)
+ {
+ return new DruidExpression(null, expression);
+ }
+
+ public static DruidExpression fromFunctionCall(final String functionName, final List args)
+ {
+ return new DruidExpression(null, functionCall(functionName, args));
+ }
+
+ public static String numberLiteral(final Number n)
+ {
+ return n == null ? nullLiteral() : n.toString();
+ }
+
+ public static String stringLiteral(final String s)
+ {
+ return s == null ? nullLiteral() : "'" + escape(s) + "'";
+ }
+
+ public static String nullLiteral()
+ {
+ return "''";
+ }
+
+ public static String functionCall(final String functionName, final List args)
+ {
+ Preconditions.checkNotNull(functionName, "functionName");
+ Preconditions.checkNotNull(args, "args");
+
+ final StringBuilder builder = new StringBuilder(functionName);
+ builder.append("(");
+
+ for (int i = 0; i < args.size(); i++) {
+ final DruidExpression arg = Preconditions.checkNotNull(args.get(i), "arg #%s", i);
+ builder.append(arg.getExpression());
+ if (i < args.size() - 1) {
+ builder.append(",");
+ }
+ }
+
+ builder.append(")");
+
+ return builder.toString();
+ }
+
+ public static String functionCall(final String functionName, final DruidExpression... args)
+ {
+ return functionCall(functionName, Arrays.asList(args));
+ }
+
+ private static String escape(final String s)
+ {
+ final StringBuilder escaped = new StringBuilder();
+ for (int i = 0; i < s.length(); i++) {
+ final char c = s.charAt(i);
+ if (Character.isLetterOrDigit(c) || Arrays.binarySearch(SAFE_CHARS, c) >= 0) {
+ escaped.append(c);
+ } else {
+ escaped.append("\\u").append(BaseEncoding.base16().encode(Chars.toByteArray(c)));
+ }
+ }
+ return escaped.toString();
+ }
+
+ public String getExpression()
+ {
+ return expression;
+ }
+
+ public boolean isDirectColumnAccess()
+ {
+ return simpleExtraction != null && simpleExtraction.getExtractionFn() == null;
+ }
+
+ public String getDirectColumn()
+ {
+ return Preconditions.checkNotNull(simpleExtraction.getColumn());
+ }
+
+ public boolean isSimpleExtraction()
+ {
+ return simpleExtraction != null;
+ }
+
+ public Expr parse(final ExprMacroTable macroTable)
+ {
+ return Parser.parse(expression, macroTable);
+ }
+
+ public SimpleExtraction getSimpleExtraction()
+ {
+ return Preconditions.checkNotNull(simpleExtraction);
+ }
+
+ public ExpressionVirtualColumn toVirtualColumn(
+ final String name,
+ final ValueType outputType,
+ final ExprMacroTable macroTable
+ )
+ {
+ return new ExpressionVirtualColumn(name, expression, outputType, macroTable);
+ }
+
+ public DruidExpression map(
+ final Function extractionMap,
+ final Function expressionMap
+ )
+ {
+ return new DruidExpression(
+ simpleExtraction == null ? null : extractionMap.apply(simpleExtraction),
+ expressionMap.apply(expression)
+ );
+ }
+
+ @Override
+ public boolean equals(final Object o)
+ {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final DruidExpression that = (DruidExpression) o;
+ return Objects.equals(simpleExtraction, that.simpleExtraction) &&
+ Objects.equals(expression, that.expression);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(simpleExtraction, expression);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "DruidExpression{" +
+ "simpleExtraction=" + simpleExtraction +
+ ", expression='" + expression + '\'' +
+ '}';
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/Expressions.java b/sql/src/main/java/io/druid/sql/calcite/expression/Expressions.java
index 0e5db76f3fb1..f03c8901109f 100644
--- a/sql/src/main/java/io/druid/sql/calcite/expression/Expressions.java
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/Expressions.java
@@ -19,42 +19,33 @@
package io.druid.sql.calcite.expression;
-import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
-import com.google.common.io.BaseEncoding;
-import com.google.common.primitives.Chars;
-import io.druid.java.util.common.IAE;
import io.druid.java.util.common.ISE;
-import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.granularity.Granularity;
+import io.druid.java.util.common.granularity.PeriodGranularity;
import io.druid.math.expr.ExprType;
-import io.druid.query.aggregation.PostAggregator;
-import io.druid.query.aggregation.post.ArithmeticPostAggregator;
-import io.druid.query.aggregation.post.ConstantPostAggregator;
-import io.druid.query.aggregation.post.ExpressionPostAggregator;
-import io.druid.query.aggregation.post.FieldAccessPostAggregator;
import io.druid.query.extraction.ExtractionFn;
import io.druid.query.extraction.TimeFormatExtractionFn;
import io.druid.query.filter.AndDimFilter;
+import io.druid.query.filter.BoundDimFilter;
import io.druid.query.filter.DimFilter;
+import io.druid.query.filter.ExpressionDimFilter;
import io.druid.query.filter.LikeDimFilter;
import io.druid.query.filter.NotDimFilter;
import io.druid.query.filter.OrDimFilter;
import io.druid.query.ordering.StringComparator;
import io.druid.query.ordering.StringComparators;
import io.druid.segment.column.Column;
-import io.druid.sql.calcite.aggregation.PostAggregatorFactory;
import io.druid.sql.calcite.filtration.BoundRefKey;
import io.druid.sql.calcite.filtration.Bounds;
import io.druid.sql.calcite.filtration.Filtration;
import io.druid.sql.calcite.planner.Calcites;
-import io.druid.sql.calcite.planner.DruidOperatorTable;
import io.druid.sql.calcite.planner.PlannerContext;
import io.druid.sql.calcite.table.RowSignature;
-import org.apache.calcite.avatica.util.TimeUnitRange;
import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
import org.apache.calcite.rel.core.Project;
import org.apache.calcite.rex.RexCall;
@@ -62,41 +53,84 @@
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeFamily;
import org.apache.calcite.sql.type.SqlTypeName;
import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
import org.joda.time.Interval;
+import org.joda.time.Period;
-import java.util.Calendar;
+import javax.annotation.Nullable;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.function.Function;
/**
* A collection of functions for translating from Calcite expressions into Druid objects.
*/
public class Expressions
{
- private static final Map MATH_FUNCTIONS = ImmutableMap.builder()
- .put("ABS", "abs")
- .put("CEIL", "ceil")
- .put("EXP", "exp")
- .put("FLOOR", "floor")
- .put("LN", "log")
- .put("LOG10", "log10")
- .put("POWER", "pow")
- .put("SQRT", "sqrt")
+ private static final Map DIRECT_CONVERSIONS = ImmutableMap.builder()
+ .put(SqlStdOperatorTable.ABS, "abs")
+ .put(SqlStdOperatorTable.CASE, "case_searched")
+ .put(SqlStdOperatorTable.CHAR_LENGTH, "strlen")
+ .put(SqlStdOperatorTable.CHARACTER_LENGTH, "strlen")
+ .put(SqlStdOperatorTable.CONCAT, "concat")
+ .put(SqlStdOperatorTable.EXP, "exp")
+ .put(SqlStdOperatorTable.DIVIDE_INTEGER, "div")
+ .put(SqlStdOperatorTable.LIKE, "like")
+ .put(SqlStdOperatorTable.LN, "log")
+ .put(SqlStdOperatorTable.LOWER, "lower")
+ .put(SqlStdOperatorTable.LOG10, "log10")
+ .put(SqlStdOperatorTable.POWER, "pow")
+ .put(SqlStdOperatorTable.REPLACE, "replace")
+ .put(SqlStdOperatorTable.SQRT, "sqrt")
+ .put(SqlStdOperatorTable.TRIM, "trim")
+ .put(SqlStdOperatorTable.UPPER, "upper")
.build();
- private static final Map MATH_TYPES;
+ private static final Map UNARY_PREFIX_OPERATOR_MAP = ImmutableMap.builder()
+ .put(SqlStdOperatorTable.NOT, "!")
+ .put(SqlStdOperatorTable.UNARY_MINUS, "-")
+ .build();
+
+ private static final Map UNARY_SUFFIX_OPERATOR_MAP = ImmutableMap.builder()
+ .put(SqlStdOperatorTable.IS_NULL, "== ''")
+ .put(SqlStdOperatorTable.IS_NOT_NULL, "!= ''")
+ .put(SqlStdOperatorTable.IS_FALSE, "<= 0") // Matches Evals.asBoolean
+ .put(SqlStdOperatorTable.IS_NOT_TRUE, "<= 0") // Matches Evals.asBoolean
+ .put(SqlStdOperatorTable.IS_TRUE, "> 0") // Matches Evals.asBoolean
+ .put(SqlStdOperatorTable.IS_NOT_FALSE, "> 0") // Matches Evals.asBoolean
+ .build();
+
+ private static final Map BINARY_OPERATOR_MAP = ImmutableMap.builder()
+ .put(SqlStdOperatorTable.MULTIPLY, "*")
+ .put(SqlStdOperatorTable.MOD, "%")
+ .put(SqlStdOperatorTable.DIVIDE, "/")
+ .put(SqlStdOperatorTable.PLUS, "+")
+ .put(SqlStdOperatorTable.MINUS, "-")
+ .put(SqlStdOperatorTable.EQUALS, "==")
+ .put(SqlStdOperatorTable.NOT_EQUALS, "!=")
+ .put(SqlStdOperatorTable.GREATER_THAN, ">")
+ .put(SqlStdOperatorTable.GREATER_THAN_OR_EQUAL, ">=")
+ .put(SqlStdOperatorTable.LESS_THAN, "<")
+ .put(SqlStdOperatorTable.LESS_THAN_OR_EQUAL, "<=")
+ .put(SqlStdOperatorTable.AND, "&&")
+ .put(SqlStdOperatorTable.OR, "||")
+ .build();
+
+ private static final Map EXPRESSION_TYPES;
static {
final ImmutableMap.Builder builder = ImmutableMap.builder();
- for (SqlTypeName type : SqlTypeName.APPROX_TYPES) {
+ for (SqlTypeName type : SqlTypeName.FRACTIONAL_TYPES) {
builder.put(type, ExprType.DOUBLE);
}
- for (SqlTypeName type : SqlTypeName.EXACT_TYPES) {
+ for (SqlTypeName type : SqlTypeName.INT_TYPES) {
builder.put(type, ExprType.LONG);
}
@@ -104,7 +138,14 @@ public class Expressions
builder.put(type, ExprType.STRING);
}
- MATH_TYPES = builder.build();
+ // Booleans are treated as longs in Druid expressions, using two-value logic (positive = true, nonpositive = false).
+ builder.put(SqlTypeName.BOOLEAN, ExprType.LONG);
+
+ // Timestamps are treated as longs (millis since the epoch) in Druid expressions.
+ builder.put(SqlTypeName.TIMESTAMP, ExprType.LONG);
+ builder.put(SqlTypeName.DATE, ExprType.LONG);
+
+ EXPRESSION_TYPES = builder.build();
}
private Expressions()
@@ -136,264 +177,224 @@ public static RexNode fromFieldAccess(
}
/**
- * Translate a Calcite row-expression to a Druid row extraction. Note that this signature will probably need to
- * change once we support extractions from multiple columns.
+ * Translate a list of Calcite {@code RexNode} to Druid expressions.
*
* @param plannerContext SQL planner context
- * @param rowOrder order of fields in the Druid rows to be extracted from
- * @param expression expression meant to be applied on top of the rows
+ * @param rowSignature signature of the rows to be extracted from
+ * @param rexNodes list of Calcite expressions meant to be applied on top of the rows
*
- * @return RowExtraction or null if not possible
+ * @return list of Druid expressions in the same order as rexNodes, or null if not possible.
+ * If a non-null list is returned, all elements will be non-null.
*/
- public static RowExtraction toRowExtraction(
+ @Nullable
+ public static List toDruidExpressions(
final PlannerContext plannerContext,
- final List rowOrder,
- final RexNode expression
- )
- {
- if (expression.getKind() == SqlKind.INPUT_REF) {
- final RexInputRef ref = (RexInputRef) expression;
- final String columnName = rowOrder.get(ref.getIndex());
- if (columnName == null) {
- throw new ISE("WTF?! Expression referred to nonexistent index[%d]", ref.getIndex());
- }
-
- return RowExtraction.of(columnName, null);
- } else if (expression.getKind() == SqlKind.CAST) {
- final RexNode operand = ((RexCall) expression).getOperands().get(0);
- if (expression.getType().getSqlTypeName() == SqlTypeName.DATE
- && operand.getType().getSqlTypeName() == SqlTypeName.TIMESTAMP) {
- // Handling casting TIMESTAMP to DATE by flooring to DAY.
- return FloorExtractionOperator.applyTimestampFloor(
- toRowExtraction(plannerContext, rowOrder, operand),
- TimeUnits.toQueryGranularity(TimeUnitRange.DAY, plannerContext.getTimeZone())
- );
- } else {
- // Ignore other casts.
- // TODO(gianm): Probably not a good idea to ignore other CASTs like this.
- return toRowExtraction(plannerContext, rowOrder, ((RexCall) expression).getOperands().get(0));
- }
- } else {
- // Try conversion using a SqlExtractionOperator.
- final RowExtraction retVal;
-
- if (expression instanceof RexCall) {
- final SqlExtractionOperator extractionOperator = plannerContext.getOperatorTable().lookupExtractionOperator(
- expression.getKind(),
- ((RexCall) expression).getOperator().getName()
- );
-
- retVal = extractionOperator != null
- ? extractionOperator.convert(plannerContext, rowOrder, expression)
- : null;
- } else {
- retVal = null;
- }
-
- return retVal;
- }
- }
-
- /**
- * Translate a Calcite row-expression to a Druid PostAggregator. One day, when possible, this could be folded
- * into {@link #toRowExtraction(DruidOperatorTable, PlannerContext, List, RexNode)} .
- *
- * @param name name of the PostAggregator
- * @param rowOrder order of fields in the Druid rows to be extracted from
- * @param finalizingPostAggregatorFactories post-aggregators that should be used for specific entries in rowOrder.
- * May be empty, and individual values may be null. Missing or null values
- * will lead to creation of {@link FieldAccessPostAggregator}.
- * @param expression expression meant to be applied on top of the rows
- *
- * @return PostAggregator or null if not possible
- */
- public static PostAggregator toPostAggregator(
- final String name,
- final List rowOrder,
- final List finalizingPostAggregatorFactories,
- final RexNode expression,
- final PlannerContext plannerContext
+ final RowSignature rowSignature,
+ final List rexNodes
)
{
- final PostAggregator retVal;
-
- if (expression.getKind() == SqlKind.INPUT_REF) {
- final RexInputRef ref = (RexInputRef) expression;
- final PostAggregatorFactory finalizingPostAggregatorFactory = finalizingPostAggregatorFactories.get(ref.getIndex());
- retVal = finalizingPostAggregatorFactory != null
- ? finalizingPostAggregatorFactory.factorize(name)
- : new FieldAccessPostAggregator(name, rowOrder.get(ref.getIndex()));
- } else if (expression.getKind() == SqlKind.CAST) {
- // Ignore CAST when translating to PostAggregators and hope for the best. They are really loosey-goosey with
- // types internally and there isn't much we can do to respect
- // TODO(gianm): Probably not a good idea to ignore CAST like this.
- final RexNode operand = ((RexCall) expression).getOperands().get(0);
- retVal = toPostAggregator(name, rowOrder, finalizingPostAggregatorFactories, operand, plannerContext);
- } else if (expression.getKind() == SqlKind.LITERAL
- && SqlTypeName.NUMERIC_TYPES.contains(expression.getType().getSqlTypeName())) {
- retVal = new ConstantPostAggregator(name, (Number) RexLiteral.value(expression));
- } else if (expression.getKind() == SqlKind.TIMES
- || expression.getKind() == SqlKind.DIVIDE
- || expression.getKind() == SqlKind.PLUS
- || expression.getKind() == SqlKind.MINUS) {
- final String fnName = ImmutableMap.builder()
- .put(SqlKind.TIMES, "*")
- .put(SqlKind.DIVIDE, "quotient")
- .put(SqlKind.PLUS, "+")
- .put(SqlKind.MINUS, "-")
- .build().get(expression.getKind());
- final List operands = Lists.newArrayList();
- for (RexNode operand : ((RexCall) expression).getOperands()) {
- final PostAggregator translatedOperand = toPostAggregator(
- null,
- rowOrder,
- finalizingPostAggregatorFactories,
- operand,
- plannerContext
- );
- if (translatedOperand == null) {
- return null;
- }
- operands.add(translatedOperand);
- }
- retVal = new ArithmeticPostAggregator(name, fnName, operands);
- } else {
- // Try converting to a math expression.
- final String mathExpression = Expressions.toMathExpression(rowOrder, expression);
- if (mathExpression == null) {
- retVal = null;
- } else {
- retVal = new ExpressionPostAggregator(name, mathExpression, null, plannerContext.getExprMacroTable());
+ final List retVal = new ArrayList<>(rexNodes.size());
+ for (RexNode rexNode : rexNodes) {
+ final DruidExpression druidExpression = toDruidExpression(plannerContext, rowSignature, rexNode);
+ if (druidExpression == null) {
+ return null;
}
- }
- if (retVal != null && name != null && !name.equals(retVal.getName())) {
- throw new ISE("WTF?! Was about to return a PostAggregator with bad name, [%s] != [%s]", name, retVal.getName());
+ retVal.add(druidExpression);
}
-
return retVal;
}
/**
- * Translate a row-expression to a Druid math expression. One day, when possible, this could be folded into
- * {@link #toRowExtraction(DruidOperatorTable, PlannerContext, List, RexNode)}.
+ * Translate a Calcite {@code RexNode} to a Druid expressions.
*
- * @param rowOrder order of fields in the Druid rows to be extracted from
- * @param expression expression meant to be applied on top of the rows
+ * @param plannerContext SQL planner context
+ * @param rowSignature signature of the rows to be extracted from
+ * @param rexNode expression meant to be applied on top of the rows
*
- * @return expression referring to fields in rowOrder, or null if not possible
+ * @return rexNode referring to fields in rowOrder, or null if not possible
*/
- public static String toMathExpression(
- final List rowOrder,
- final RexNode expression
+ @Nullable
+ public static DruidExpression toDruidExpression(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
)
{
- final SqlKind kind = expression.getKind();
- final SqlTypeName sqlTypeName = expression.getType().getSqlTypeName();
+ final SqlKind kind = rexNode.getKind();
+ final SqlTypeName sqlTypeName = rexNode.getType().getSqlTypeName();
if (kind == SqlKind.INPUT_REF) {
// Translate field references.
- final RexInputRef ref = (RexInputRef) expression;
- final String columnName = rowOrder.get(ref.getIndex());
+ final RexInputRef ref = (RexInputRef) rexNode;
+ final String columnName = rowSignature.getRowOrder().get(ref.getIndex());
if (columnName == null) {
throw new ISE("WTF?! Expression referred to nonexistent index[%d]", ref.getIndex());
}
- return StringUtils.format("\"%s\"", escape(columnName));
+ return DruidExpression.fromColumn(columnName);
} else if (kind == SqlKind.CAST || kind == SqlKind.REINTERPRET) {
// Translate casts.
- final RexNode operand = ((RexCall) expression).getOperands().get(0);
- final String operandExpression = toMathExpression(rowOrder, operand);
+ final RexNode operand = ((RexCall) rexNode).getOperands().get(0);
+ final DruidExpression operandExpression = toDruidExpression(
+ plannerContext,
+ rowSignature,
+ operand
+ );
if (operandExpression == null) {
return null;
}
- final ExprType fromType = MATH_TYPES.get(operand.getType().getSqlTypeName());
- final ExprType toType = MATH_TYPES.get(sqlTypeName);
- if (fromType != toType) {
- return StringUtils.format("CAST(%s, '%s')", operandExpression, toType.toString());
+ final SqlTypeName fromType = operand.getType().getSqlTypeName();
+ final SqlTypeName toType = rexNode.getType().getSqlTypeName();
+
+ if (SqlTypeName.CHAR_TYPES.contains(fromType) && SqlTypeName.DATETIME_TYPES.contains(toType)) {
+ // Cast strings to datetimes by parsing them from SQL format.
+ final DruidExpression timestampExpression = DruidExpression.fromFunctionCall(
+ "timestamp_parse",
+ ImmutableList.of(
+ operandExpression,
+ DruidExpression.fromExpression(DruidExpression.stringLiteral(dateTimeFormatString(toType)))
+ )
+ );
+
+ if (toType == SqlTypeName.DATE) {
+ return TimeFloorOperatorConversion.applyTimestampFloor(
+ timestampExpression,
+ new PeriodGranularity(Period.days(1), null, plannerContext.getTimeZone())
+ );
+ } else {
+ return timestampExpression;
+ }
+ } else if (SqlTypeName.DATETIME_TYPES.contains(fromType) && SqlTypeName.CHAR_TYPES.contains(toType)) {
+ // Cast datetimes to strings by formatting them in SQL format.
+ return DruidExpression.fromFunctionCall(
+ "timestamp_format",
+ ImmutableList.of(
+ operandExpression,
+ DruidExpression.fromExpression(DruidExpression.stringLiteral(dateTimeFormatString(fromType)))
+ )
+ );
} else {
- return operandExpression;
- }
- } else if (kind == SqlKind.TIMES || kind == SqlKind.DIVIDE || kind == SqlKind.PLUS || kind == SqlKind.MINUS) {
- // Translate simple arithmetic.
- final List operands = ((RexCall) expression).getOperands();
- final String lhsExpression = toMathExpression(rowOrder, operands.get(0));
- final String rhsExpression = toMathExpression(rowOrder, operands.get(1));
- if (lhsExpression == null || rhsExpression == null) {
- return null;
- }
+ // Handle other casts.
+ final ExprType fromExprType = EXPRESSION_TYPES.get(fromType);
+ final ExprType toExprType = EXPRESSION_TYPES.get(toType);
- final String op = ImmutableMap.of(
- SqlKind.TIMES, "*",
- SqlKind.DIVIDE, "/",
- SqlKind.PLUS, "+",
- SqlKind.MINUS, "-"
- ).get(kind);
-
- return StringUtils.format("(%s %s %s)", lhsExpression, op, rhsExpression);
- } else if (kind == SqlKind.OTHER_FUNCTION) {
- final String calciteFunction = ((RexCall) expression).getOperator().getName();
- final String druidFunction = MATH_FUNCTIONS.get(calciteFunction);
- final List functionArgs = Lists.newArrayList();
-
- for (final RexNode operand : ((RexCall) expression).getOperands()) {
- final String operandExpression = toMathExpression(rowOrder, operand);
- if (operandExpression == null) {
+ if (fromExprType == null || toExprType == null) {
+ // We have no runtime type for these SQL types.
return null;
}
- functionArgs.add(operandExpression);
+
+ final DruidExpression typeCastExpression;
+
+ if (fromExprType != toExprType) {
+ // Ignore casts for simple extractions (use Function.identity) since it is ok in many cases.
+ typeCastExpression = operandExpression.map(
+ Function.identity(),
+ expression -> String.format("CAST(%s, '%s')", expression, toExprType.toString())
+ );
+ } else {
+ typeCastExpression = operandExpression;
+ }
+
+ if (toType == SqlTypeName.DATE) {
+ // Floor to day when casting to DATE.
+ return TimeFloorOperatorConversion.applyTimestampFloor(
+ typeCastExpression,
+ new PeriodGranularity(Period.days(1), null, plannerContext.getTimeZone())
+ );
+ } else {
+ return typeCastExpression;
+ }
}
+ } else if (rexNode instanceof RexCall) {
+ final SqlOperator operator = ((RexCall) rexNode).getOperator();
+
+ final SqlOperatorConversion conversion = plannerContext.getOperatorTable()
+ .lookupOperatorConversion(operator);
- if ("MOD".equals(calciteFunction)) {
- // Special handling for MOD, which is a function in Calcite but a binary operator in Druid.
- Preconditions.checkState(functionArgs.size() == 2, "WTF?! Expected 2 args for MOD.");
- return StringUtils.format("(%s %s %s)", functionArgs.get(0), "%", functionArgs.get(1));
+ if (conversion != null) {
+ return conversion.toDruidExpression(plannerContext, rowSignature, rexNode);
}
- if (druidFunction == null) {
+ final List operands = Expressions.toDruidExpressions(
+ plannerContext,
+ rowSignature,
+ ((RexCall) rexNode).getOperands()
+ );
+
+ if (operands == null) {
+ return null;
+ } else if (UNARY_PREFIX_OPERATOR_MAP.containsKey(operator)) {
+ return DruidExpression.fromExpression(
+ String.format(
+ "(%s %s)",
+ UNARY_PREFIX_OPERATOR_MAP.get(operator),
+ Iterables.getOnlyElement(operands).getExpression()
+ )
+ );
+ } else if (UNARY_SUFFIX_OPERATOR_MAP.containsKey(operator)) {
+ return DruidExpression.fromExpression(
+ String.format(
+ "(%s %s)",
+ Iterables.getOnlyElement(operands).getExpression(),
+ UNARY_SUFFIX_OPERATOR_MAP.get(operator)
+ )
+ );
+ } else if (BINARY_OPERATOR_MAP.containsKey(operator)) {
+ if (operands.size() != 2) {
+ throw new ISE("WTF?! Got binary operator[%s] with %s args?", kind, operands.size());
+ }
+ return DruidExpression.fromExpression(
+ String.format(
+ "(%s %s %s)",
+ operands.get(0).getExpression(),
+ BINARY_OPERATOR_MAP.get(operator),
+ operands.get(1).getExpression()
+ )
+ );
+ } else if (DIRECT_CONVERSIONS.containsKey(operator)) {
+ final String functionName = DIRECT_CONVERSIONS.get(operator);
+ return DruidExpression.fromExpression(DruidExpression.functionCall(functionName, operands));
+ } else {
return null;
}
-
- return StringUtils.format("%s(%s)", druidFunction, Joiner.on(", ").join(functionArgs));
} else if (kind == SqlKind.LITERAL) {
// Translate literal.
if (SqlTypeName.NUMERIC_TYPES.contains(sqlTypeName)) {
- // Include literal numbers as-is.
- return String.valueOf(RexLiteral.value(expression));
+ return DruidExpression.fromExpression(DruidExpression.numberLiteral((Number) RexLiteral.value(rexNode)));
+ } else if (SqlTypeFamily.INTERVAL_DAY_TIME == sqlTypeName.getFamily()) {
+ // Calcite represents DAY-TIME intervals in milliseconds.
+ final long milliseconds = ((Number) RexLiteral.value(rexNode)).longValue();
+ return DruidExpression.fromExpression(DruidExpression.numberLiteral(milliseconds));
+ } else if (SqlTypeFamily.INTERVAL_YEAR_MONTH == sqlTypeName.getFamily()) {
+ // Calcite represents YEAR-MONTH intervals in months.
+ final long months = ((Number) RexLiteral.value(rexNode)).longValue();
+ return DruidExpression.fromExpression(DruidExpression.numberLiteral(months));
} else if (SqlTypeName.STRING_TYPES.contains(sqlTypeName)) {
- // Quote literal strings.
- return "\'" + escape(RexLiteral.stringValue(expression)) + "\'";
+ return DruidExpression.fromExpression(DruidExpression.stringLiteral(RexLiteral.stringValue(rexNode)));
+ } else if (SqlTypeName.TIMESTAMP == sqlTypeName || SqlTypeName.DATE == sqlTypeName) {
+ if (RexLiteral.isNullLiteral(rexNode)) {
+ return DruidExpression.fromExpression(DruidExpression.nullLiteral());
+ } else {
+ return DruidExpression.fromExpression(
+ DruidExpression.numberLiteral(
+ Calcites.calciteDateTimeLiteralToJoda(rexNode, plannerContext.getTimeZone()).getMillis()
+ )
+ );
+ }
+ } else if (SqlTypeName.BOOLEAN == sqlTypeName) {
+ return DruidExpression.fromExpression(DruidExpression.numberLiteral(RexLiteral.booleanValue(rexNode) ? 1 : 0));
} else {
// Can't translate other literals.
return null;
}
} else {
- // Can't translate other kinds of expressions.
+ // Can't translate.
return null;
}
}
- /**
- * Translates "literal" (a TIMESTAMP or DATE literal) to milliseconds since the epoch using the provided
- * session time zone.
- *
- * @param literal TIMESTAMP or DATE literal
- * @param timeZone session time zone
- *
- * @return milliseconds time
- */
- public static long toMillisLiteral(final RexNode literal, final DateTimeZone timeZone)
- {
- final SqlTypeName typeName = literal.getType().getSqlTypeName();
- if (literal.getKind() != SqlKind.LITERAL || (typeName != SqlTypeName.TIMESTAMP && typeName != SqlTypeName.DATE)) {
- throw new IAE("Expected TIMESTAMP or DATE literal but got[%s:%s]", literal.getKind(), typeName);
- }
-
- final Calendar calendar = (Calendar) RexLiteral.value(literal);
- return Calcites.calciteTimestampToJoda(calendar.getTimeInMillis(), timeZone).getMillis();
- }
-
/**
* Translates "condition" to a Druid filter, or returns null if we cannot translate the condition.
*
@@ -412,7 +413,11 @@ public static DimFilter toFilter(
|| expression.getKind() == SqlKind.NOT) {
final List filters = Lists.newArrayList();
for (final RexNode rexNode : ((RexCall) expression).getOperands()) {
- final DimFilter nextFilter = toFilter(plannerContext, rowSignature, rexNode);
+ final DimFilter nextFilter = toFilter(
+ plannerContext,
+ rowSignature,
+ rexNode
+ );
if (nextFilter == null) {
return null;
}
@@ -439,45 +444,75 @@ public static DimFilter toFilter(
*
* @param plannerContext planner context
* @param rowSignature row signature of the dataSource to be filtered
- * @param expression Calcite row expression
+ * @param rexNode Calcite row expression
*/
private static DimFilter toLeafFilter(
final PlannerContext plannerContext,
final RowSignature rowSignature,
- final RexNode expression
+ final RexNode rexNode
)
{
- if (expression.isAlwaysTrue()) {
+ if (rexNode.isAlwaysTrue()) {
return Filtration.matchEverything();
- } else if (expression.isAlwaysFalse()) {
+ } else if (rexNode.isAlwaysFalse()) {
return Filtration.matchNothing();
}
- final SqlKind kind = expression.getKind();
+ final DimFilter simpleFilter = toSimpleLeafFilter(plannerContext, rowSignature, rexNode);
+ return simpleFilter != null ? simpleFilter : toExpressionLeafFilter(plannerContext, rowSignature, rexNode);
+ }
+
+ /**
+ * Translates to a simple leaf filter, meaning one that hits just a single column and is not an expression filter.
+ */
+ private static DimFilter toSimpleLeafFilter(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
+ {
+ final SqlKind kind = rexNode.getKind();
- if (kind == SqlKind.LIKE) {
- final List operands = ((RexCall) expression).getOperands();
- final RowExtraction rex = toRowExtraction(
+ if (kind == SqlKind.IS_TRUE || kind == SqlKind.IS_NOT_FALSE) {
+ return toSimpleLeafFilter(
plannerContext,
- rowSignature.getRowOrder(),
- operands.get(0)
+ rowSignature,
+ Iterables.getOnlyElement(((RexCall) rexNode).getOperands())
+ );
+ } else if (kind == SqlKind.IS_FALSE || kind == SqlKind.IS_NOT_TRUE) {
+ return new NotDimFilter(
+ toSimpleLeafFilter(
+ plannerContext,
+ rowSignature,
+ Iterables.getOnlyElement(((RexCall) rexNode).getOperands())
+ )
);
- if (rex == null || !rex.isFilterable(rowSignature)) {
+ } else if (kind == SqlKind.IS_NULL || kind == SqlKind.IS_NOT_NULL) {
+ final RexNode operand = Iterables.getOnlyElement(((RexCall) rexNode).getOperands());
+
+ // operand must be translatable to a SimpleExtraction to be simple-filterable
+ final DruidExpression druidExpression = toDruidExpression(plannerContext, rowSignature, operand);
+ if (druidExpression == null || !druidExpression.isSimpleExtraction()) {
return null;
}
- return new LikeDimFilter(
- rex.getColumn(),
- RexLiteral.stringValue(operands.get(1)),
- operands.size() > 2 ? RexLiteral.stringValue(operands.get(2)) : null,
- rex.getExtractionFn()
+
+ final BoundDimFilter equalFilter = Bounds.equalTo(
+ new BoundRefKey(
+ druidExpression.getSimpleExtraction().getColumn(),
+ druidExpression.getSimpleExtraction().getExtractionFn(),
+ StringComparators.LEXICOGRAPHIC
+ ),
+ ""
);
+
+ return kind == SqlKind.IS_NOT_NULL ? new NotDimFilter(equalFilter) : equalFilter;
} else if (kind == SqlKind.EQUALS
|| kind == SqlKind.NOT_EQUALS
|| kind == SqlKind.GREATER_THAN
|| kind == SqlKind.GREATER_THAN_OR_EQUAL
|| kind == SqlKind.LESS_THAN
|| kind == SqlKind.LESS_THAN_OR_EQUAL) {
- final List operands = ((RexCall) expression).getOperands();
+ final List operands = ((RexCall) rexNode).getOperands();
Preconditions.checkState(operands.size() == 2, "WTF?! Expected 2 operands, got[%,d]", operands.size());
boolean flip = false;
RexNode lhs = operands.get(0);
@@ -496,14 +531,14 @@ private static DimFilter toLeafFilter(
return null;
}
- // lhs must be translatable to a RowExtraction to be filterable
- final RowExtraction rex = toRowExtraction(plannerContext, rowSignature.getRowOrder(), lhs);
- if (rex == null || !rex.isFilterable(rowSignature)) {
+ // lhs must be translatable to a SimpleExtraction to be simple-filterable
+ final DruidExpression lhsExpression = toDruidExpression(plannerContext, rowSignature, lhs);
+ if (lhsExpression == null || !lhsExpression.isSimpleExtraction()) {
return null;
}
- final String column = rex.getColumn();
- final ExtractionFn extractionFn = rex.getExtractionFn();
+ final String column = lhsExpression.getSimpleExtraction().getColumn();
+ final ExtractionFn extractionFn = lhsExpression.getSimpleExtraction().getExtractionFn();
if (column.equals(Column.TIME_COLUMN_NAME) && extractionFn instanceof TimeFormatExtractionFn) {
// Check if we can strip the extractionFn and convert the filter to a direct filter on __time.
@@ -512,7 +547,7 @@ private static DimFilter toLeafFilter(
final Granularity granularity = ExtractionFns.toQueryGranularity(extractionFn);
if (granularity != null) {
// lhs is FLOOR(__time TO granularity); rhs must be a timestamp
- final long rhsMillis = toMillisLiteral(rhs, plannerContext.getTimeZone());
+ final long rhsMillis = Calcites.calciteDateTimeLiteralToJoda(rhs, plannerContext.getTimeZone()).getMillis();
final Interval rhsInterval = granularity.bucket(new DateTime(rhsMillis));
// Is rhs aligned on granularity boundaries?
@@ -554,18 +589,19 @@ private static DimFilter toLeafFilter(
} else if (SqlTypeName.CHAR_TYPES.contains(rhsLiteral.getTypeName())) {
val = String.valueOf(RexLiteral.stringValue(rhsLiteral));
} else if (SqlTypeName.TIMESTAMP == rhsLiteral.getTypeName() || SqlTypeName.DATE == rhsLiteral.getTypeName()) {
- val = String.valueOf(toMillisLiteral(rhsLiteral, plannerContext.getTimeZone()));
+ val = String.valueOf(
+ Calcites.calciteDateTimeLiteralToJoda(
+ rhsLiteral,
+ plannerContext.getTimeZone()
+ ).getMillis()
+ );
} else {
// Don't know how to filter on this kind of literal.
return null;
}
// Numeric lhs needs a numeric comparison.
- final boolean lhsIsNumeric = SqlTypeName.NUMERIC_TYPES.contains(lhs.getType().getSqlTypeName())
- || SqlTypeName.TIMESTAMP == lhs.getType().getSqlTypeName()
- || SqlTypeName.DATE == lhs.getType().getSqlTypeName();
- final StringComparator comparator = lhsIsNumeric ? StringComparators.NUMERIC : StringComparators.LEXICOGRAPHIC;
-
+ final StringComparator comparator = Calcites.getStringComparatorForSqlTypeName(lhs.getType().getSqlTypeName());
final BoundRefKey boundRefKey = new BoundRefKey(column, extractionFn, comparator);
final DimFilter filter;
@@ -587,22 +623,50 @@ private static DimFilter toLeafFilter(
}
return filter;
+ } else if (kind == SqlKind.LIKE) {
+ final List operands = ((RexCall) rexNode).getOperands();
+ final DruidExpression druidExpression = toDruidExpression(
+ plannerContext,
+ rowSignature,
+ operands.get(0)
+ );
+ if (druidExpression == null || !druidExpression.isSimpleExtraction()) {
+ return null;
+ }
+ return new LikeDimFilter(
+ druidExpression.getSimpleExtraction().getColumn(),
+ RexLiteral.stringValue(operands.get(1)),
+ operands.size() > 2 ? RexLiteral.stringValue(operands.get(2)) : null,
+ druidExpression.getSimpleExtraction().getExtractionFn()
+ );
} else {
return null;
}
}
- private static String escape(final String s)
+ /**
+ * Translates to an "expression" type leaf filter. Used as a fallback if we can't use a simple leaf filter.
+ */
+ private static DimFilter toExpressionLeafFilter(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
{
- final StringBuilder escaped = new StringBuilder();
- for (int i = 0; i < s.length(); i++) {
- final char c = s.charAt(i);
- if (Character.isLetterOrDigit(c) || Character.isWhitespace(c)) {
- escaped.append(c);
- } else {
- escaped.append("\\u").append(BaseEncoding.base16().encode(Chars.toByteArray(c)));
- }
+ final DruidExpression druidExpression = toDruidExpression(plannerContext, rowSignature, rexNode);
+ return druidExpression == null
+ ? null
+ : new ExpressionDimFilter(druidExpression.getExpression(), plannerContext.getExprMacroTable());
+ }
+
+ private static String dateTimeFormatString(final SqlTypeName sqlTypeName)
+ {
+ if (sqlTypeName == SqlTypeName.DATE) {
+ return "yyyy-MM-dd";
+ } else if (sqlTypeName == SqlTypeName.TIMESTAMP) {
+ return "yyyy-MM-dd HH:mm:ss";
+ } else {
+ throw new ISE("Unsupported DateTime type[%s]", sqlTypeName);
}
- return escaped.toString();
}
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/ExtractExtractionOperator.java b/sql/src/main/java/io/druid/sql/calcite/expression/ExtractExtractionOperator.java
deleted file mode 100644
index 90dd7dff10ea..000000000000
--- a/sql/src/main/java/io/druid/sql/calcite/expression/ExtractExtractionOperator.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Licensed to Metamarkets Group Inc. (Metamarkets) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
-
-import io.druid.java.util.common.granularity.Granularity;
-import io.druid.query.extraction.ExtractionFn;
-import io.druid.query.extraction.TimeFormatExtractionFn;
-import io.druid.sql.calcite.planner.PlannerContext;
-import org.apache.calcite.avatica.util.TimeUnitRange;
-import org.apache.calcite.rex.RexCall;
-import org.apache.calcite.rex.RexLiteral;
-import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.sql.SqlFunction;
-import org.apache.calcite.sql.fun.SqlStdOperatorTable;
-
-import java.util.List;
-
-public class ExtractExtractionOperator implements SqlExtractionOperator
-{
- @Override
- public SqlFunction calciteFunction()
- {
- return SqlStdOperatorTable.EXTRACT;
- }
-
- @Override
- public RowExtraction convert(
- final PlannerContext plannerContext,
- final List rowOrder,
- final RexNode expression
- )
- {
- // EXTRACT(timeUnit FROM expr)
- final RexCall call = (RexCall) expression;
- final RexLiteral flag = (RexLiteral) call.getOperands().get(0);
- final TimeUnitRange timeUnit = (TimeUnitRange) flag.getValue();
- final RexNode expr = call.getOperands().get(1);
-
- final RowExtraction rex = Expressions.toRowExtraction(plannerContext, rowOrder, expr);
- if (rex == null) {
- return null;
- }
-
- final String dateTimeFormat = TimeUnits.toDateTimeFormat(timeUnit);
- if (dateTimeFormat == null) {
- return null;
- }
-
- final ExtractionFn baseExtractionFn;
-
- if (call.getOperator().getName().equals("EXTRACT_DATE")) {
- // Expr will be in number of days since the epoch. Can't translate.
- return null;
- } else {
- // Expr will be in millis since the epoch
- baseExtractionFn = rex.getExtractionFn();
- }
-
- if (baseExtractionFn instanceof TimeFormatExtractionFn) {
- final TimeFormatExtractionFn baseTimeFormatFn = (TimeFormatExtractionFn) baseExtractionFn;
- final Granularity queryGranularity = ExtractionFns.toQueryGranularity(baseTimeFormatFn);
- if (queryGranularity != null) {
- // Combine EXTRACT(X FROM FLOOR(Y TO Z)) into a single extractionFn.
- return RowExtraction.of(
- rex.getColumn(),
- new TimeFormatExtractionFn(dateTimeFormat, plannerContext.getTimeZone(), null, queryGranularity, true)
- );
- }
- }
-
- return RowExtraction.of(
- rex.getColumn(),
- ExtractionFns.compose(
- new TimeFormatExtractionFn(dateTimeFormat, plannerContext.getTimeZone(), null, null, true),
- baseExtractionFn
- )
- );
- }
-}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/ExtractOperatorConversion.java b/sql/src/main/java/io/druid/sql/calcite/expression/ExtractOperatorConversion.java
new file mode 100644
index 000000000000..4f7c33429739
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/ExtractOperatorConversion.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import com.google.common.collect.ImmutableMap;
+import io.druid.query.expression.TimestampExtractExprMacro;
+import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.avatica.util.TimeUnitRange;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+
+import java.util.Map;
+
+public class ExtractOperatorConversion implements SqlOperatorConversion
+{
+ private static final Map EXTRACT_UNIT_MAP =
+ ImmutableMap.builder()
+ .put(TimeUnitRange.SECOND, TimestampExtractExprMacro.Unit.SECOND)
+ .put(TimeUnitRange.MINUTE, TimestampExtractExprMacro.Unit.MINUTE)
+ .put(TimeUnitRange.HOUR, TimestampExtractExprMacro.Unit.HOUR)
+ .put(TimeUnitRange.DAY, TimestampExtractExprMacro.Unit.DAY)
+ .put(TimeUnitRange.DOW, TimestampExtractExprMacro.Unit.DOW)
+ .put(TimeUnitRange.DOY, TimestampExtractExprMacro.Unit.DOY)
+ .put(TimeUnitRange.WEEK, TimestampExtractExprMacro.Unit.WEEK)
+ .put(TimeUnitRange.MONTH, TimestampExtractExprMacro.Unit.MONTH)
+ .put(TimeUnitRange.QUARTER, TimestampExtractExprMacro.Unit.QUARTER)
+ .put(TimeUnitRange.YEAR, TimestampExtractExprMacro.Unit.YEAR)
+ .build();
+
+ @Override
+ public SqlFunction calciteOperator()
+ {
+ return SqlStdOperatorTable.EXTRACT;
+ }
+
+ @Override
+ public DruidExpression toDruidExpression(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
+ {
+ // EXTRACT(timeUnit FROM arg)
+ final RexCall call = (RexCall) rexNode;
+ final RexLiteral flag = (RexLiteral) call.getOperands().get(0);
+ final TimeUnitRange calciteUnit = (TimeUnitRange) flag.getValue();
+ final RexNode arg = call.getOperands().get(1);
+
+ final DruidExpression input = Expressions.toDruidExpression(plannerContext, rowSignature, arg);
+ if (input == null) {
+ return null;
+ }
+
+ if (call.getOperator().getName().equals("EXTRACT_DATE")) {
+ // Arg will be in number of days since the epoch. Can't translate.
+ return null;
+ }
+
+ final TimestampExtractExprMacro.Unit druidUnit = EXTRACT_UNIT_MAP.get(calciteUnit);
+ if (druidUnit == null) {
+ // Don't know how to extract this time unit.
+ return null;
+ }
+
+ return TimeExtractOperatorConversion.applyTimeExtract(input, druidUnit, plannerContext.getTimeZone());
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/ExtractionFns.java b/sql/src/main/java/io/druid/sql/calcite/expression/ExtractionFns.java
index 66ceb296a512..9f25dadf8208 100644
--- a/sql/src/main/java/io/druid/sql/calcite/expression/ExtractionFns.java
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/ExtractionFns.java
@@ -69,14 +69,14 @@ public static ExtractionFn fromQueryGranularity(final Granularity queryGranulari
}
/**
- * Compose f and g, returning an ExtractionFn that computes f(g(x)). Null f or g are treated like identity functions.
+ * Cascade f and g, returning an ExtractionFn that computes g(f(x)). Null f or g are treated like identity functions.
*
* @param f function
* @param g function
*
* @return composed function, or null if both f and g were null
*/
- public static ExtractionFn compose(final ExtractionFn f, final ExtractionFn g)
+ public static ExtractionFn cascade(final ExtractionFn f, final ExtractionFn g)
{
if (f == null) {
// Treat null like identity.
@@ -88,18 +88,18 @@ public static ExtractionFn compose(final ExtractionFn f, final ExtractionFn g)
// Apply g, then f, unwrapping if they are already cascades.
- if (g instanceof CascadeExtractionFn) {
- extractionFns.addAll(Arrays.asList(((CascadeExtractionFn) g).getExtractionFns()));
- } else {
- extractionFns.add(g);
- }
-
if (f instanceof CascadeExtractionFn) {
extractionFns.addAll(Arrays.asList(((CascadeExtractionFn) f).getExtractionFns()));
} else {
extractionFns.add(f);
}
+ if (g instanceof CascadeExtractionFn) {
+ extractionFns.addAll(Arrays.asList(((CascadeExtractionFn) g).getExtractionFns()));
+ } else {
+ extractionFns.add(g);
+ }
+
return new CascadeExtractionFn(extractionFns.toArray(new ExtractionFn[extractionFns.size()]));
}
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/FloorExtractionOperator.java b/sql/src/main/java/io/druid/sql/calcite/expression/FloorOperatorConversion.java
similarity index 58%
rename from sql/src/main/java/io/druid/sql/calcite/expression/FloorExtractionOperator.java
rename to sql/src/main/java/io/druid/sql/calcite/expression/FloorOperatorConversion.java
index b386d4d7b7d5..822d6ad358e1 100644
--- a/sql/src/main/java/io/druid/sql/calcite/expression/FloorExtractionOperator.java
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/FloorOperatorConversion.java
@@ -19,68 +19,56 @@
package io.druid.sql.calcite.expression;
-import io.druid.java.util.common.granularity.Granularity;
-import io.druid.query.extraction.BucketExtractionFn;
+import io.druid.java.util.common.granularity.PeriodGranularity;
import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
import org.apache.calcite.avatica.util.TimeUnitRange;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
-import java.util.List;
-
-public class FloorExtractionOperator implements SqlExtractionOperator
+public class FloorOperatorConversion implements SqlOperatorConversion
{
- public static RowExtraction applyTimestampFloor(
- final RowExtraction rex,
- final Granularity queryGranularity
- )
- {
- if (rex == null || queryGranularity == null) {
- return null;
- }
-
- return RowExtraction.of(
- rex.getColumn(),
- ExtractionFns.compose(
- ExtractionFns.fromQueryGranularity(queryGranularity),
- rex.getExtractionFn()
- )
- );
- }
-
@Override
- public SqlFunction calciteFunction()
+ public SqlOperator calciteOperator()
{
return SqlStdOperatorTable.FLOOR;
}
@Override
- public RowExtraction convert(
+ public DruidExpression toDruidExpression(
final PlannerContext plannerContext,
- final List rowOrder,
- final RexNode expression
+ final RowSignature rowSignature,
+ final RexNode rexNode
)
{
- final RexCall call = (RexCall) expression;
+ final RexCall call = (RexCall) rexNode;
final RexNode arg = call.getOperands().get(0);
-
- final RowExtraction rex = Expressions.toRowExtraction(plannerContext, rowOrder, arg);
- if (rex == null) {
+ final DruidExpression druidExpression = Expressions.toDruidExpression(
+ plannerContext,
+ rowSignature,
+ arg
+ );
+ if (druidExpression == null) {
return null;
} else if (call.getOperands().size() == 1) {
// FLOOR(expr)
- return RowExtraction.of(
- rex.getColumn(),
- ExtractionFns.compose(new BucketExtractionFn(1.0, 0.0), rex.getExtractionFn())
+ return druidExpression.map(
+ simpleExtraction -> null, // BucketExtractionFn could do this, but it's lame since it returns strings.
+ expression -> String.format("floor(%s)", expression)
);
} else if (call.getOperands().size() == 2) {
// FLOOR(expr TO timeUnit)
final RexLiteral flag = (RexLiteral) call.getOperands().get(1);
final TimeUnitRange timeUnit = (TimeUnitRange) flag.getValue();
- return applyTimestampFloor(rex, TimeUnits.toQueryGranularity(timeUnit, plannerContext.getTimeZone()));
+ final PeriodGranularity granularity = TimeUnits.toQueryGranularity(timeUnit, plannerContext.getTimeZone());
+ if (granularity == null) {
+ return null;
+ }
+
+ return TimeFloorOperatorConversion.applyTimestampFloor(druidExpression, granularity);
} else {
// WTF? FLOOR with 3 arguments?
return null;
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/LookupExtractionOperator.java b/sql/src/main/java/io/druid/sql/calcite/expression/LookupExtractionOperator.java
deleted file mode 100644
index 07f648db97ae..000000000000
--- a/sql/src/main/java/io/druid/sql/calcite/expression/LookupExtractionOperator.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Licensed to Metamarkets Group Inc. (Metamarkets) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
-
-import com.google.inject.Inject;
-import io.druid.query.lookup.LookupReferencesManager;
-import io.druid.query.lookup.RegisteredLookupExtractionFn;
-import io.druid.sql.calcite.planner.PlannerContext;
-import org.apache.calcite.rex.RexCall;
-import org.apache.calcite.rex.RexLiteral;
-import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.sql.SqlFunction;
-import org.apache.calcite.sql.SqlFunctionCategory;
-import org.apache.calcite.sql.SqlKind;
-import org.apache.calcite.sql.type.OperandTypes;
-import org.apache.calcite.sql.type.ReturnTypes;
-import org.apache.calcite.sql.type.SqlTypeFamily;
-import org.apache.calcite.sql.type.SqlTypeName;
-
-import java.util.List;
-
-public class LookupExtractionOperator implements SqlExtractionOperator
-{
- private static final String NAME = "LOOKUP";
- private static final SqlFunction SQL_FUNCTION = new LookupSqlFunction();
-
- private final LookupReferencesManager lookupReferencesManager;
-
- @Inject
- public LookupExtractionOperator(final LookupReferencesManager lookupReferencesManager)
- {
- this.lookupReferencesManager = lookupReferencesManager;
- }
-
- @Override
- public SqlFunction calciteFunction()
- {
- return SQL_FUNCTION;
- }
-
- @Override
- public RowExtraction convert(
- final PlannerContext plannerContext,
- final List rowOrder,
- final RexNode expression
- )
- {
- final RexCall call = (RexCall) expression;
- final RowExtraction rex = Expressions.toRowExtraction(
- plannerContext,
- rowOrder,
- call.getOperands().get(0)
- );
- if (rex == null) {
- return null;
- }
-
- final String lookupName = RexLiteral.stringValue(call.getOperands().get(1));
- final RegisteredLookupExtractionFn extractionFn = new RegisteredLookupExtractionFn(
- lookupReferencesManager,
- lookupName,
- false,
- null,
- false,
- true
- );
-
- return RowExtraction.of(
- rex.getColumn(),
- ExtractionFns.compose(extractionFn, rex.getExtractionFn())
- );
- }
-
- private static class LookupSqlFunction extends SqlFunction
- {
- private static final String SIGNATURE = "'" + NAME + "(expression, lookupName)'\n";
-
- LookupSqlFunction()
- {
- super(
- NAME,
- SqlKind.OTHER_FUNCTION,
- ReturnTypes.explicit(SqlTypeName.VARCHAR),
- null,
- OperandTypes.and(
- OperandTypes.sequence(SIGNATURE, OperandTypes.CHARACTER, OperandTypes.LITERAL),
- OperandTypes.family(SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER)
- ),
- SqlFunctionCategory.STRING
- );
- }
- }
-}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/LookupOperatorConversion.java b/sql/src/main/java/io/druid/sql/calcite/expression/LookupOperatorConversion.java
new file mode 100644
index 000000000000..a6118b21e1b5
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/LookupOperatorConversion.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import com.google.inject.Inject;
+import io.druid.math.expr.Expr;
+import io.druid.query.lookup.LookupReferencesManager;
+import io.druid.query.lookup.RegisteredLookupExtractionFn;
+import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+public class LookupOperatorConversion implements SqlOperatorConversion
+{
+ private static final SqlFunction SQL_FUNCTION = OperatorConversions
+ .operatorBuilder("LOOKUP")
+ .operandTypes(SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER)
+ .returnType(SqlTypeName.VARCHAR)
+ .functionCategory(SqlFunctionCategory.STRING)
+ .build();
+
+ private final LookupReferencesManager lookupReferencesManager;
+
+ @Inject
+ public LookupOperatorConversion(final LookupReferencesManager lookupReferencesManager)
+ {
+ this.lookupReferencesManager = lookupReferencesManager;
+ }
+
+ @Override
+ public SqlFunction calciteOperator()
+ {
+ return SQL_FUNCTION;
+ }
+
+ @Override
+ public DruidExpression toDruidExpression(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
+ {
+ return OperatorConversions.functionCall(
+ plannerContext,
+ rowSignature,
+ rexNode,
+ calciteOperator().getName().toLowerCase(),
+ inputExpressions -> {
+ final DruidExpression arg = inputExpressions.get(0);
+ final Expr lookupNameExpr = inputExpressions.get(1).parse(plannerContext.getExprMacroTable());
+
+ if (arg.isSimpleExtraction() && lookupNameExpr.isLiteral()) {
+ return arg.getSimpleExtraction().cascade(
+ new RegisteredLookupExtractionFn(
+ lookupReferencesManager,
+ (String) lookupNameExpr.getLiteralValue(),
+ false,
+ null,
+ false,
+ true
+ )
+ );
+ } else {
+ return null;
+ }
+ }
+ );
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/MillisToTimestampOperatorConversion.java b/sql/src/main/java/io/druid/sql/calcite/expression/MillisToTimestampOperatorConversion.java
new file mode 100644
index 000000000000..59283907be27
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/MillisToTimestampOperatorConversion.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import com.google.common.collect.Iterables;
+import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+public class MillisToTimestampOperatorConversion implements SqlOperatorConversion
+{
+ private static final SqlFunction SQL_FUNCTION = OperatorConversions
+ .operatorBuilder("MILLIS_TO_TIMESTAMP")
+ .operandTypes(SqlTypeFamily.EXACT_NUMERIC)
+ .returnType(SqlTypeName.TIMESTAMP)
+ .functionCategory(SqlFunctionCategory.TIMEDATE)
+ .build();
+
+ @Override
+ public SqlOperator calciteOperator()
+ {
+ return SQL_FUNCTION;
+ }
+
+ @Override
+ public DruidExpression toDruidExpression(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
+ {
+ // Nothing to do, just leave the operand unchanged. Druid treats millis and timestamps the same internally.
+ final RexCall call = (RexCall) rexNode;
+ return Expressions.toDruidExpression(plannerContext, rowSignature, Iterables.getOnlyElement(call.getOperands()));
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/OperatorConversions.java b/sql/src/main/java/io/druid/sql/calcite/expression/OperatorConversions.java
new file mode 100644
index 000000000000..551367690e87
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/OperatorConversions.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import com.google.common.base.Preconditions;
+import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.type.OperandTypes;
+import org.apache.calcite.sql.type.ReturnTypes;
+import org.apache.calcite.sql.type.SqlReturnTypeInference;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Utilities for assisting in writing {@link SqlOperatorConversion} implementations.
+ */
+public class OperatorConversions
+{
+ public static DruidExpression functionCall(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode,
+ final String functionName
+ )
+ {
+ return functionCall(plannerContext, rowSignature, rexNode, functionName, null);
+ }
+
+ public static DruidExpression functionCall(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode,
+ final String functionName,
+ final Function, SimpleExtraction> simpleExtractionFunction
+ )
+ {
+ final RexCall call = (RexCall) rexNode;
+
+ final List druidExpressions = Expressions.toDruidExpressions(
+ plannerContext,
+ rowSignature,
+ call.getOperands()
+ );
+
+ if (druidExpressions == null) {
+ return null;
+ }
+
+ return DruidExpression.of(
+ simpleExtractionFunction == null ? null : simpleExtractionFunction.apply(druidExpressions),
+ DruidExpression.functionCall(functionName, druidExpressions)
+ );
+ }
+
+ public static OperatorBuilder operatorBuilder(final String name)
+ {
+ return new OperatorBuilder(name);
+ }
+
+ public static class OperatorBuilder
+ {
+ private String name;
+ private SqlKind kind = SqlKind.OTHER_FUNCTION;
+ private SqlReturnTypeInference returnTypeInference;
+ private SqlFunctionCategory functionCategory = SqlFunctionCategory.USER_DEFINED_FUNCTION;
+
+ // For operand type checking
+ private List operandTypes;
+ private int requiredOperands = Integer.MAX_VALUE;
+
+ private OperatorBuilder(final String name)
+ {
+ this.name = Preconditions.checkNotNull(name, "name");
+ }
+
+ public OperatorBuilder kind(final SqlKind kind)
+ {
+ this.kind = kind;
+ return this;
+ }
+
+ public OperatorBuilder returnType(final SqlTypeName typeName)
+ {
+ this.returnTypeInference = ReturnTypes.explicit(typeName);
+ return this;
+ }
+
+ public OperatorBuilder nullableReturnType(final SqlTypeName typeName)
+ {
+ this.returnTypeInference = ReturnTypes.explicit(
+ factory ->
+ factory.createTypeWithNullability(
+ factory.createSqlType(typeName),
+ true
+ )
+ );
+ return this;
+ }
+
+ public OperatorBuilder functionCategory(final SqlFunctionCategory functionCategory)
+ {
+ this.functionCategory = functionCategory;
+ return this;
+ }
+
+ public OperatorBuilder operandTypes(final SqlTypeFamily... operandTypes)
+ {
+ this.operandTypes = Arrays.asList(operandTypes);
+ return this;
+ }
+
+ public OperatorBuilder requiredOperands(final int requiredOperands)
+ {
+ this.requiredOperands = requiredOperands;
+ return this;
+ }
+
+ public SqlFunction build()
+ {
+ return new SqlFunction(
+ name,
+ kind,
+ Preconditions.checkNotNull(returnTypeInference, "returnTypeInference"),
+ null,
+ OperandTypes.family(
+ Preconditions.checkNotNull(operandTypes, "operandTypes"),
+ i -> i + 1 > requiredOperands
+ ),
+ functionCategory
+ );
+ }
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/RegexpExtractExtractionOperator.java b/sql/src/main/java/io/druid/sql/calcite/expression/RegexpExtractExtractionOperator.java
deleted file mode 100644
index 65a89332856e..000000000000
--- a/sql/src/main/java/io/druid/sql/calcite/expression/RegexpExtractExtractionOperator.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Licensed to Metamarkets Group Inc. (Metamarkets) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
-
-import io.druid.query.extraction.ExtractionFn;
-import io.druid.query.extraction.RegexDimExtractionFn;
-import io.druid.sql.calcite.planner.PlannerContext;
-import org.apache.calcite.rex.RexCall;
-import org.apache.calcite.rex.RexLiteral;
-import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.sql.SqlFunction;
-import org.apache.calcite.sql.SqlFunctionCategory;
-import org.apache.calcite.sql.SqlKind;
-import org.apache.calcite.sql.type.OperandTypes;
-import org.apache.calcite.sql.type.ReturnTypes;
-import org.apache.calcite.sql.type.SqlTypeFamily;
-import org.apache.calcite.sql.type.SqlTypeName;
-
-import java.util.List;
-
-public class RegexpExtractExtractionOperator implements SqlExtractionOperator
-{
- private static final String NAME = "REGEXP_EXTRACT";
- private static final SqlFunction SQL_FUNCTION = new RegexpExtractSqlFunction();
-
- @Override
- public SqlFunction calciteFunction()
- {
- return SQL_FUNCTION;
- }
-
- @Override
- public RowExtraction convert(
- final PlannerContext plannerContext,
- final List rowOrder,
- final RexNode expression
- )
- {
- final RexCall call = (RexCall) expression;
- final RowExtraction rex = Expressions.toRowExtraction(
- plannerContext,
- rowOrder,
- call.getOperands().get(0)
- );
- if (rex == null) {
- return null;
- }
-
- final String pattern = RexLiteral.stringValue(call.getOperands().get(1));
- final int index = call.getOperands().size() >= 3 ? RexLiteral.intValue(call.getOperands().get(2)) : 0;
- final ExtractionFn extractionFn = new RegexDimExtractionFn(pattern, index, true, null);
-
- return RowExtraction.of(
- rex.getColumn(),
- ExtractionFns.compose(extractionFn, rex.getExtractionFn())
- );
- }
-
- private static class RegexpExtractSqlFunction extends SqlFunction
- {
- private static final String SIGNATURE1 = "'" + NAME + "(subject, pattern)'\n";
- private static final String SIGNATURE2 = "'" + NAME + "(subject, pattern, index)'\n";
-
- RegexpExtractSqlFunction()
- {
- super(
- NAME,
- SqlKind.OTHER_FUNCTION,
- ReturnTypes.explicit(SqlTypeName.VARCHAR),
- null,
- OperandTypes.or(
- OperandTypes.and(
- OperandTypes.sequence(SIGNATURE1, OperandTypes.CHARACTER, OperandTypes.LITERAL),
- OperandTypes.family(SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER)
- ),
- OperandTypes.and(
- OperandTypes.sequence(SIGNATURE2, OperandTypes.CHARACTER, OperandTypes.LITERAL, OperandTypes.LITERAL),
- OperandTypes.family(SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER, SqlTypeFamily.INTEGER)
- )
- ),
- SqlFunctionCategory.STRING
- );
- }
- }
-}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/RegexpExtractOperatorConversion.java b/sql/src/main/java/io/druid/sql/calcite/expression/RegexpExtractOperatorConversion.java
new file mode 100644
index 000000000000..8879e8313a2c
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/RegexpExtractOperatorConversion.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import io.druid.math.expr.Expr;
+import io.druid.query.extraction.RegexDimExtractionFn;
+import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+public class RegexpExtractOperatorConversion implements SqlOperatorConversion
+{
+ private static final SqlFunction SQL_FUNCTION = OperatorConversions
+ .operatorBuilder("REGEXP_EXTRACT")
+ .operandTypes(SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER, SqlTypeFamily.INTEGER)
+ .requiredOperands(2)
+ .returnType(SqlTypeName.VARCHAR)
+ .functionCategory(SqlFunctionCategory.STRING)
+ .build();
+
+ private static final int DEFAULT_INDEX = 0;
+
+ @Override
+ public SqlFunction calciteOperator()
+ {
+ return SQL_FUNCTION;
+ }
+
+ @Override
+ public DruidExpression toDruidExpression(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
+ {
+ return OperatorConversions.functionCall(
+ plannerContext,
+ rowSignature,
+ rexNode,
+ calciteOperator().getName().toLowerCase(),
+ inputExpressions -> {
+ final DruidExpression arg = inputExpressions.get(0);
+ final Expr patternExpr = inputExpressions.get(1).parse(plannerContext.getExprMacroTable());
+ final Expr indexExpr = inputExpressions.size() > 2
+ ? inputExpressions.get(2).parse(plannerContext.getExprMacroTable())
+ : null;
+
+ if (arg.isSimpleExtraction() && patternExpr.isLiteral() && (indexExpr == null || indexExpr.isLiteral())) {
+ return arg.getSimpleExtraction().cascade(
+ new RegexDimExtractionFn(
+ (String) patternExpr.getLiteralValue(),
+ indexExpr == null ? DEFAULT_INDEX : ((Number) indexExpr.getLiteralValue()).intValue(),
+ true,
+ null
+ )
+ );
+ } else {
+ return null;
+ }
+ }
+ );
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/RowExtraction.java b/sql/src/main/java/io/druid/sql/calcite/expression/RowExtraction.java
deleted file mode 100644
index 42e1aa0f0e71..000000000000
--- a/sql/src/main/java/io/druid/sql/calcite/expression/RowExtraction.java
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Licensed to Metamarkets Group Inc. (Metamarkets) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
-
-import com.google.common.base.Preconditions;
-import io.druid.java.util.common.StringUtils;
-import io.druid.query.dimension.DefaultDimensionSpec;
-import io.druid.query.dimension.DimensionSpec;
-import io.druid.query.dimension.ExtractionDimensionSpec;
-import io.druid.query.extraction.ExtractionFn;
-import io.druid.segment.column.Column;
-import io.druid.segment.column.ValueType;
-import io.druid.segment.filter.Filters;
-import io.druid.sql.calcite.rel.DruidQueryBuilder;
-import io.druid.sql.calcite.table.RowSignature;
-
-/**
- * Represents an extraction of a value from a Druid row. Can be used for grouping, filtering, etc.
- *
- * Currently this is a column plus an extractionFn, but it's expected that as time goes on, this will become more
- * general and allow for variously-typed extractions from multiple columns.
- */
-public class RowExtraction
-{
- private final String column;
- private final ExtractionFn extractionFn;
-
- public RowExtraction(String column, ExtractionFn extractionFn)
- {
- this.column = Preconditions.checkNotNull(column, "column");
- this.extractionFn = extractionFn;
- }
-
- public static RowExtraction of(String column, ExtractionFn extractionFn)
- {
- return new RowExtraction(column, extractionFn);
- }
-
- public static RowExtraction fromDimensionSpec(final DimensionSpec dimensionSpec)
- {
- if (dimensionSpec instanceof ExtractionDimensionSpec) {
- return RowExtraction.of(
- dimensionSpec.getDimension(),
- ((ExtractionDimensionSpec) dimensionSpec).getExtractionFn()
- );
- } else if (dimensionSpec instanceof DefaultDimensionSpec) {
- return RowExtraction.of(dimensionSpec.getDimension(), null);
- } else {
- return null;
- }
- }
-
- public static RowExtraction fromQueryBuilder(
- final DruidQueryBuilder queryBuilder,
- final int fieldNumber
- )
- {
- final String fieldName = queryBuilder.getRowOrder().get(fieldNumber);
-
- if (queryBuilder.getGrouping() != null) {
- for (DimensionSpec dimensionSpec : queryBuilder.getGrouping().getDimensions()) {
- if (dimensionSpec.getOutputName().equals(fieldName)) {
- return RowExtraction.fromDimensionSpec(dimensionSpec);
- }
- }
-
- return null;
- } else if (queryBuilder.getSelectProjection() != null) {
- for (DimensionSpec dimensionSpec : queryBuilder.getSelectProjection().getDimensions()) {
- if (dimensionSpec.getOutputName().equals(fieldName)) {
- return RowExtraction.fromDimensionSpec(dimensionSpec);
- }
- }
-
- for (String metricName : queryBuilder.getSelectProjection().getMetrics()) {
- if (metricName.equals(fieldName)) {
- return RowExtraction.of(metricName, null);
- }
- }
-
- return null;
- } else {
- // No select projection or grouping.
- return RowExtraction.of(queryBuilder.getRowOrder().get(fieldNumber), null);
- }
- }
-
- public String getColumn()
- {
- return column;
- }
-
- public ExtractionFn getExtractionFn()
- {
- return extractionFn;
- }
-
- /**
- * Check if this extraction can be used to build a filter on a Druid dataSource. This method exists because we can't
- * filter on floats (yet) and things like DruidFilterRule need to check for that.
- *
- * @param rowSignature row signature of the dataSource
- *
- * @return whether or not this extraction is filterable
- */
- public boolean isFilterable(final RowSignature rowSignature)
- {
- return Filters.FILTERABLE_TYPES.contains(rowSignature.getColumnType(column));
- }
-
- public DimensionSpec toDimensionSpec(
- final RowSignature rowSignature,
- final String outputName,
- final ValueType outputType
- )
- {
- Preconditions.checkNotNull(outputType, "outputType");
-
- final ValueType columnType = rowSignature.getColumnType(column);
- if (columnType == null) {
- return null;
- }
-
- if (columnType == ValueType.STRING || (column.equals(Column.TIME_COLUMN_NAME) && extractionFn != null)) {
- return extractionFn == null
- ? new DefaultDimensionSpec(column, outputName, outputType)
- : new ExtractionDimensionSpec(column, outputName, outputType, extractionFn);
- } else if (columnType == ValueType.LONG || columnType == ValueType.FLOAT) {
- if (extractionFn == null) {
- return new DefaultDimensionSpec(column, outputName, outputType);
- } else {
- return new ExtractionDimensionSpec(column, outputName, outputType, extractionFn);
- }
- } else {
- // Can't create dimensionSpecs for non-string, non-numeric columns
- return null;
- }
- }
-
- @Override
- public boolean equals(Object o)
- {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- RowExtraction that = (RowExtraction) o;
-
- if (column != null ? !column.equals(that.column) : that.column != null) {
- return false;
- }
- return extractionFn != null ? extractionFn.equals(that.extractionFn) : that.extractionFn == null;
-
- }
-
- @Override
- public int hashCode()
- {
- int result = column != null ? column.hashCode() : 0;
- result = 31 * result + (extractionFn != null ? extractionFn.hashCode() : 0);
- return result;
- }
-
- @Override
- public String toString()
- {
- if (extractionFn != null) {
- return StringUtils.format("%s(%s)", extractionFn, column);
- } else {
- return column;
- }
- }
-}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/SimpleExtraction.java b/sql/src/main/java/io/druid/sql/calcite/expression/SimpleExtraction.java
new file mode 100644
index 000000000000..b51841b8f1dd
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/SimpleExtraction.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import com.google.common.base.Preconditions;
+import io.druid.query.dimension.DefaultDimensionSpec;
+import io.druid.query.dimension.DimensionSpec;
+import io.druid.query.dimension.ExtractionDimensionSpec;
+import io.druid.query.extraction.ExtractionFn;
+import io.druid.segment.column.ValueType;
+
+/**
+ * Represents a "simple" extraction of a value from a Druid row, which is defined as a column plus an extractionFn.
+ * This is useful since identifying simple extractions and treating them specially can allow Druid to perform
+ * additional optimizations.
+ */
+public class SimpleExtraction
+{
+ private final String column;
+ private final ExtractionFn extractionFn;
+
+ public SimpleExtraction(String column, ExtractionFn extractionFn)
+ {
+ this.column = Preconditions.checkNotNull(column, "column");
+ this.extractionFn = extractionFn;
+ }
+
+ public static SimpleExtraction of(String column, ExtractionFn extractionFn)
+ {
+ return new SimpleExtraction(column, extractionFn);
+ }
+
+ public String getColumn()
+ {
+ return column;
+ }
+
+ public ExtractionFn getExtractionFn()
+ {
+ return extractionFn;
+ }
+
+ public SimpleExtraction cascade(final ExtractionFn nextExtractionFn)
+ {
+ return new SimpleExtraction(
+ column,
+ ExtractionFns.cascade(extractionFn, Preconditions.checkNotNull(nextExtractionFn, "nextExtractionFn"))
+ );
+ }
+
+ public DimensionSpec toDimensionSpec(
+ final String outputName,
+ final ValueType outputType
+ )
+ {
+ Preconditions.checkNotNull(outputType, "outputType");
+ return extractionFn == null
+ ? new DefaultDimensionSpec(column, outputName, outputType)
+ : new ExtractionDimensionSpec(column, outputName, outputType, extractionFn);
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ SimpleExtraction that = (SimpleExtraction) o;
+
+ if (column != null ? !column.equals(that.column) : that.column != null) {
+ return false;
+ }
+ return extractionFn != null ? extractionFn.equals(that.extractionFn) : that.extractionFn == null;
+
+ }
+
+ @Override
+ public int hashCode()
+ {
+ int result = column != null ? column.hashCode() : 0;
+ result = 31 * result + (extractionFn != null ? extractionFn.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString()
+ {
+ if (extractionFn != null) {
+ return String.format("%s(%s)", extractionFn, column);
+ } else {
+ return column;
+ }
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/SqlExtractionOperator.java b/sql/src/main/java/io/druid/sql/calcite/expression/SqlOperatorConversion.java
similarity index 61%
rename from sql/src/main/java/io/druid/sql/calcite/expression/SqlExtractionOperator.java
rename to sql/src/main/java/io/druid/sql/calcite/expression/SqlOperatorConversion.java
index 258990d9253d..8a5605f87933 100644
--- a/sql/src/main/java/io/druid/sql/calcite/expression/SqlExtractionOperator.java
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/SqlOperatorConversion.java
@@ -20,34 +20,33 @@
package io.druid.sql.calcite.expression;
import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlOperator;
-import java.util.List;
-
-public interface SqlExtractionOperator
+public interface SqlOperatorConversion
{
/**
- * Returns the SQL operator corresponding to this aggregation function. Should be a singleton.
+ * Returns the SQL operator corresponding to this function. Should be a singleton.
*
* @return operator
*/
- SqlFunction calciteFunction();
+ SqlOperator calciteOperator();
/**
- * Returns the Druid {@link RowExtraction} corresponding to a SQL {@code RexNode}.
+ * Translate a Calcite {@code RexNode} to a Druid expression.
*
* @param plannerContext SQL planner context
- * @param rowOrder order of fields in the Druid rows to be extracted from
- * @param expression expression meant to be applied on top of the table
+ * @param rowSignature signature of the rows to be extracted from
+ * @param rexNode expression meant to be applied on top of the rows
*
- * @return (columnName, extractionFn) or null
+ * @return Druid expression, or null if translation is not possible
*
- * @see Expressions#toRowExtraction(PlannerContext, List, RexNode)
+ * @see Expressions#toDruidExpression(PlannerContext, RowSignature, RexNode)
*/
- RowExtraction convert(
+ DruidExpression toDruidExpression(
PlannerContext plannerContext,
- List rowOrder,
- RexNode expression
+ RowSignature rowSignature,
+ RexNode rexNode
);
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/SubstringExtractionOperator.java b/sql/src/main/java/io/druid/sql/calcite/expression/SubstringOperatorConversion.java
similarity index 60%
rename from sql/src/main/java/io/druid/sql/calcite/expression/SubstringExtractionOperator.java
rename to sql/src/main/java/io/druid/sql/calcite/expression/SubstringOperatorConversion.java
index 65757e239a6d..6da872b84477 100644
--- a/sql/src/main/java/io/druid/sql/calcite/expression/SubstringExtractionOperator.java
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/SubstringOperatorConversion.java
@@ -21,51 +21,55 @@
import io.druid.query.extraction.SubstringDimExtractionFn;
import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
-import java.util.List;
-
-public class SubstringExtractionOperator implements SqlExtractionOperator
+public class SubstringOperatorConversion implements SqlOperatorConversion
{
@Override
- public SqlFunction calciteFunction()
+ public SqlOperator calciteOperator()
{
return SqlStdOperatorTable.SUBSTRING;
}
@Override
- public RowExtraction convert(
+ public DruidExpression toDruidExpression(
final PlannerContext plannerContext,
- final List rowOrder,
- final RexNode expression
+ final RowSignature rowSignature,
+ final RexNode rexNode
)
{
- final RexCall call = (RexCall) expression;
- final RowExtraction arg = Expressions.toRowExtraction(
+ // Can't simply pass-through operands, since SQL standard args don't match what Druid's expression language wants.
+ // SQL is 1-indexed, Druid is 0-indexed.
+
+ final RexCall call = (RexCall) rexNode;
+ final DruidExpression input = Expressions.toDruidExpression(
plannerContext,
- rowOrder,
+ rowSignature,
call.getOperands().get(0)
);
- if (arg == null) {
+ if (input == null) {
return null;
}
final int index = RexLiteral.intValue(call.getOperands().get(1)) - 1;
- final Integer length;
+ final int length;
if (call.getOperands().size() > 2) {
length = RexLiteral.intValue(call.getOperands().get(2));
} else {
- length = null;
+ length = -1;
}
- return RowExtraction.of(
- arg.getColumn(),
- ExtractionFns.compose(
- new SubstringDimExtractionFn(index, length),
- arg.getExtractionFn()
+ return input.map(
+ simpleExtraction -> simpleExtraction.cascade(new SubstringDimExtractionFn(index, length < 0 ? null : length)),
+ expression -> String.format(
+ "substring(%s, %s, %s)",
+ expression,
+ DruidExpression.numberLiteral(index),
+ DruidExpression.numberLiteral(length)
)
);
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/TimeArithmeticOperatorConversion.java b/sql/src/main/java/io/druid/sql/calcite/expression/TimeArithmeticOperatorConversion.java
new file mode 100644
index 000000000000..aaa119b2e1a3
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/TimeArithmeticOperatorConversion.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import com.google.common.base.Preconditions;
+import io.druid.java.util.common.IAE;
+import io.druid.java.util.common.ISE;
+import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+
+import java.util.List;
+
+/**
+ * Base class for a number of time arithmetic related operators.
+ */
+public abstract class TimeArithmeticOperatorConversion implements SqlOperatorConversion
+{
+ private final SqlOperator operator;
+ private final int direction;
+
+ public TimeArithmeticOperatorConversion(final SqlOperator operator, final int direction)
+ {
+ this.operator = operator;
+ this.direction = direction;
+ Preconditions.checkArgument(direction > 0 || direction < 0);
+ }
+
+ @Override
+ public SqlOperator calciteOperator()
+ {
+ return operator;
+ }
+
+ @Override
+ public DruidExpression toDruidExpression(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
+ {
+ final RexCall call = (RexCall) rexNode;
+ final List operands = call.getOperands();
+ if (operands.size() != 2) {
+ throw new IAE("Expected 2 args, got %s", operands.size());
+ }
+
+ final RexNode timeRexNode = operands.get(0);
+ final RexNode shiftRexNode = operands.get(1);
+
+ final DruidExpression timeExpr = Expressions.toDruidExpression(plannerContext, rowSignature, timeRexNode);
+ final DruidExpression shiftExpr = Expressions.toDruidExpression(plannerContext, rowSignature, shiftRexNode);
+
+ if (timeExpr == null || shiftExpr == null) {
+ return null;
+ }
+
+ if (shiftRexNode.getType().getFamily() == SqlTypeFamily.INTERVAL_YEAR_MONTH) {
+ // timestamp_expr { + | - } (year-month interval)
+ // Period is a value in months.
+ return DruidExpression.fromExpression(
+ DruidExpression.functionCall(
+ "timestamp_shift",
+ timeExpr,
+ shiftExpr.map(
+ simpleExtraction -> null,
+ expression -> String.format("concat('P', %s, 'M')", expression)
+ ),
+ DruidExpression.fromExpression(DruidExpression.numberLiteral(direction > 0 ? 1 : -1))
+ )
+ );
+ } else if (shiftRexNode.getType().getFamily() == SqlTypeFamily.INTERVAL_DAY_TIME) {
+ // timestamp_expr { + | - } (day-time interval)
+ // Period is a value in milliseconds. Ignore time zone.
+ return DruidExpression.fromExpression(
+ String.format(
+ "(%s %s %s)",
+ timeExpr.getExpression(),
+ direction > 0 ? "+" : "-",
+ shiftExpr.getExpression()
+ )
+ );
+ } else {
+ // Shouldn't happen if subclasses are behaving.
+ throw new ISE("Got unexpected type period type family[%s]", shiftRexNode.getType().getFamily());
+ }
+ }
+
+ public static class TimePlusIntervalOperatorConversion extends TimeArithmeticOperatorConversion
+ {
+ public TimePlusIntervalOperatorConversion()
+ {
+ super(SqlStdOperatorTable.DATETIME_PLUS, 1);
+ }
+ }
+
+ public static class TimeMinusIntervalOperatorConversion extends TimeArithmeticOperatorConversion
+ {
+ public TimeMinusIntervalOperatorConversion()
+ {
+ super(SqlStdOperatorTable.MINUS_DATE, -1);
+ }
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/TimeExtractOperatorConversion.java b/sql/src/main/java/io/druid/sql/calcite/expression/TimeExtractOperatorConversion.java
new file mode 100644
index 000000000000..c1d7537b9ed1
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/TimeExtractOperatorConversion.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import com.google.common.collect.ImmutableMap;
+import io.druid.query.expression.TimestampExtractExprMacro;
+import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.joda.time.DateTimeZone;
+
+import java.util.Map;
+
+public class TimeExtractOperatorConversion implements SqlOperatorConversion
+{
+ private static final SqlFunction SQL_FUNCTION = OperatorConversions
+ .operatorBuilder("TIME_EXTRACT")
+ .operandTypes(SqlTypeFamily.TIMESTAMP, SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER)
+ .requiredOperands(1)
+ .returnType(SqlTypeName.BIGINT)
+ .functionCategory(SqlFunctionCategory.TIMEDATE)
+ .build();
+
+ // Note that QUARTER is not supported here.
+ private static final Map EXTRACT_FORMAT_MAP =
+ ImmutableMap.builder()
+ .put(TimestampExtractExprMacro.Unit.SECOND, "s")
+ .put(TimestampExtractExprMacro.Unit.MINUTE, "m")
+ .put(TimestampExtractExprMacro.Unit.HOUR, "H")
+ .put(TimestampExtractExprMacro.Unit.DAY, "d")
+ .put(TimestampExtractExprMacro.Unit.DOW, "e")
+ .put(TimestampExtractExprMacro.Unit.DOY, "D")
+ .put(TimestampExtractExprMacro.Unit.WEEK, "w")
+ .put(TimestampExtractExprMacro.Unit.MONTH, "M")
+ .put(TimestampExtractExprMacro.Unit.YEAR, "Y")
+ .build();
+
+ public static DruidExpression applyTimeExtract(
+ final DruidExpression timeExpression,
+ final TimestampExtractExprMacro.Unit unit,
+ final DateTimeZone timeZone
+ )
+ {
+ return timeExpression.map(
+ simpleExtraction -> {
+ final String formatString = EXTRACT_FORMAT_MAP.get(unit);
+ if (formatString == null) {
+ return null;
+ } else {
+ return TimeFormatOperatorConversion.applyTimestampFormat(
+ simpleExtraction,
+ formatString,
+ timeZone
+ );
+ }
+ },
+ expression -> String.format(
+ "timestamp_extract(%s,%s,%s)",
+ expression,
+ DruidExpression.stringLiteral(unit.name()),
+ DruidExpression.stringLiteral(timeZone.getID())
+ )
+ );
+ }
+
+ @Override
+ public SqlFunction calciteOperator()
+ {
+ return SQL_FUNCTION;
+ }
+
+ @Override
+ public DruidExpression toDruidExpression(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
+ {
+ final RexCall call = (RexCall) rexNode;
+ final RexNode timeArg = call.getOperands().get(0);
+ final DruidExpression timeExpression = Expressions.toDruidExpression(plannerContext, rowSignature, timeArg);
+ if (timeExpression == null) {
+ return null;
+ }
+
+ final TimestampExtractExprMacro.Unit unit = TimestampExtractExprMacro.Unit.valueOf(
+ RexLiteral.stringValue(call.getOperands().get(1)).toUpperCase()
+ );
+
+ final DateTimeZone timeZone = call.getOperands().size() > 2 && !RexLiteral.isNullLiteral(call.getOperands().get(2))
+ ? DateTimeZone.forID(RexLiteral.stringValue(call.getOperands().get(2)))
+ : plannerContext.getTimeZone();
+
+ return applyTimeExtract(timeExpression, unit, timeZone);
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/TimeFloorOperatorConversion.java b/sql/src/main/java/io/druid/sql/calcite/expression/TimeFloorOperatorConversion.java
new file mode 100644
index 000000000000..5871d190be49
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/TimeFloorOperatorConversion.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import io.druid.java.util.common.granularity.PeriodGranularity;
+import io.druid.sql.calcite.planner.Calcites;
+import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Period;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class TimeFloorOperatorConversion implements SqlOperatorConversion
+{
+ private static final SqlFunction SQL_FUNCTION = OperatorConversions
+ .operatorBuilder("TIME_FLOOR")
+ .operandTypes(SqlTypeFamily.TIMESTAMP, SqlTypeFamily.CHARACTER, SqlTypeFamily.TIMESTAMP, SqlTypeFamily.CHARACTER)
+ .requiredOperands(2)
+ .returnType(SqlTypeName.TIMESTAMP)
+ .functionCategory(SqlFunctionCategory.TIMEDATE)
+ .build();
+
+ public static DruidExpression applyTimestampFloor(
+ final DruidExpression input,
+ final PeriodGranularity granularity
+ )
+ {
+ Preconditions.checkNotNull(input, "input");
+ Preconditions.checkNotNull(granularity, "granularity");
+
+ return input.map(
+ simpleExtraction -> simpleExtraction.cascade(ExtractionFns.fromQueryGranularity(granularity)),
+ expression -> DruidExpression.functionCall(
+ "timestamp_floor",
+ ImmutableList.of(
+ expression,
+ DruidExpression.stringLiteral(granularity.getPeriod().toString()),
+ DruidExpression.numberLiteral(
+ granularity.getOrigin() == null ? null : granularity.getOrigin().getMillis()
+ ),
+ DruidExpression.stringLiteral(granularity.getTimeZone().toString())
+ ).stream().map(DruidExpression::fromExpression).collect(Collectors.toList())
+ )
+ );
+ }
+
+ @Override
+ public SqlOperator calciteOperator()
+ {
+ return SQL_FUNCTION;
+ }
+
+ @Override
+ public DruidExpression toDruidExpression(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
+ {
+ final RexCall call = (RexCall) rexNode;
+ final List operands = call.getOperands();
+ final List druidExpressions = Expressions.toDruidExpressions(
+ plannerContext,
+ rowSignature,
+ operands
+ );
+
+ if (druidExpressions == null) {
+ return null;
+ } else if (operands.get(1).isA(SqlKind.LITERAL)
+ && (operands.size() <= 2 || operands.get(2).isA(SqlKind.LITERAL))
+ && (operands.size() <= 3 || operands.get(3).isA(SqlKind.LITERAL))) {
+ // Granularity is a literal. Special case since we can use an extractionFn here.
+ final Period period = new Period(RexLiteral.stringValue(operands.get(1)));
+ final DateTime origin =
+ operands.size() > 2 && !RexLiteral.isNullLiteral(operands.get(2))
+ ? Calcites.calciteDateTimeLiteralToJoda(operands.get(2), plannerContext.getTimeZone())
+ : null;
+ final DateTimeZone timeZone =
+ operands.size() > 3 && !RexLiteral.isNullLiteral(operands.get(3))
+ ? DateTimeZone.forID(RexLiteral.stringValue(operands.get(3)))
+ : plannerContext.getTimeZone();
+ final PeriodGranularity granularity = new PeriodGranularity(period, origin, timeZone);
+ return applyTimestampFloor(druidExpressions.get(0), granularity);
+ } else {
+ // Granularity is dynamic
+ return DruidExpression.fromFunctionCall("timestamp_floor", druidExpressions);
+ }
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/TimeFormatOperatorConversion.java b/sql/src/main/java/io/druid/sql/calcite/expression/TimeFormatOperatorConversion.java
new file mode 100644
index 000000000000..899f47df46d8
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/TimeFormatOperatorConversion.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import io.druid.java.util.common.granularity.Granularity;
+import io.druid.query.extraction.ExtractionFn;
+import io.druid.query.extraction.TimeFormatExtractionFn;
+import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.joda.time.DateTimeZone;
+
+import java.util.stream.Collectors;
+
+public class TimeFormatOperatorConversion implements SqlOperatorConversion
+{
+ private static final SqlFunction SQL_FUNCTION = OperatorConversions
+ .operatorBuilder("TIME_FORMAT")
+ .operandTypes(SqlTypeFamily.TIMESTAMP, SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER)
+ .requiredOperands(1)
+ .returnType(SqlTypeName.VARCHAR)
+ .functionCategory(SqlFunctionCategory.TIMEDATE)
+ .build();
+
+ public static SimpleExtraction applyTimestampFormat(
+ final SimpleExtraction simpleExtraction,
+ final String pattern,
+ final DateTimeZone timeZone
+ )
+ {
+ Preconditions.checkNotNull(simpleExtraction, "simpleExtraction");
+ Preconditions.checkNotNull(pattern, "pattern");
+ Preconditions.checkNotNull(timeZone, "timeZone");
+
+ final ExtractionFn baseExtractionFn = simpleExtraction.getExtractionFn();
+
+ if (baseExtractionFn instanceof TimeFormatExtractionFn) {
+ final TimeFormatExtractionFn baseTimeFormatFn = (TimeFormatExtractionFn) baseExtractionFn;
+ final Granularity queryGranularity = ExtractionFns.toQueryGranularity(baseTimeFormatFn);
+ if (queryGranularity != null) {
+ // Combine EXTRACT(X FROM FLOOR(Y TO Z)) into a single extractionFn.
+ return SimpleExtraction.of(
+ simpleExtraction.getColumn(),
+ new TimeFormatExtractionFn(pattern, timeZone, null, queryGranularity, true)
+ );
+ }
+ }
+
+ return simpleExtraction.cascade(new TimeFormatExtractionFn(pattern, timeZone, null, null, true));
+ }
+
+ @Override
+ public SqlOperator calciteOperator()
+ {
+ return SQL_FUNCTION;
+ }
+
+ @Override
+ public DruidExpression toDruidExpression(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
+ {
+ final RexCall call = (RexCall) rexNode;
+ final RexNode timeArg = call.getOperands().get(0);
+ final DruidExpression timeExpression = Expressions.toDruidExpression(plannerContext, rowSignature, timeArg);
+ if (timeExpression == null) {
+ return null;
+ }
+
+ final String pattern = call.getOperands().size() > 1 && !RexLiteral.isNullLiteral(call.getOperands().get(1))
+ ? RexLiteral.stringValue(call.getOperands().get(1))
+ : "yyyy-MM-dd'T'HH:mm:ss.SSSZZ";
+ final DateTimeZone timeZone = call.getOperands().size() > 2 && !RexLiteral.isNullLiteral(call.getOperands().get(2))
+ ? DateTimeZone.forID(RexLiteral.stringValue(call.getOperands().get(2)))
+ : plannerContext.getTimeZone();
+
+ return timeExpression.map(
+ simpleExtraction -> applyTimestampFormat(simpleExtraction, pattern, timeZone),
+ expression -> DruidExpression.functionCall(
+ "timestamp_format",
+ ImmutableList.of(
+ expression,
+ DruidExpression.stringLiteral(pattern),
+ DruidExpression.stringLiteral(timeZone.getID())
+ ).stream().map(DruidExpression::fromExpression).collect(Collectors.toList())
+ )
+ );
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/CharacterLengthExtractionOperator.java b/sql/src/main/java/io/druid/sql/calcite/expression/TimeParseOperatorConversion.java
similarity index 53%
rename from sql/src/main/java/io/druid/sql/calcite/expression/CharacterLengthExtractionOperator.java
rename to sql/src/main/java/io/druid/sql/calcite/expression/TimeParseOperatorConversion.java
index b8f1cfd94b0d..42daebc7beb3 100644
--- a/sql/src/main/java/io/druid/sql/calcite/expression/CharacterLengthExtractionOperator.java
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/TimeParseOperatorConversion.java
@@ -19,43 +19,38 @@
package io.druid.sql.calcite.expression;
-import io.druid.query.extraction.StrlenExtractionFn;
import io.druid.sql.calcite.planner.PlannerContext;
-import org.apache.calcite.rex.RexCall;
+import io.druid.sql.calcite.table.RowSignature;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlFunction;
-import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
-import java.util.List;
-
-public class CharacterLengthExtractionOperator implements SqlExtractionOperator
+public class TimeParseOperatorConversion implements SqlOperatorConversion
{
+ private static final SqlFunction SQL_FUNCTION = OperatorConversions
+ .operatorBuilder("TIME_PARSE")
+ .operandTypes(SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER, SqlTypeFamily.CHARACTER)
+ .requiredOperands(1)
+ .nullableReturnType(SqlTypeName.TIMESTAMP)
+ .functionCategory(SqlFunctionCategory.TIMEDATE)
+ .build();
+
@Override
- public SqlFunction calciteFunction()
+ public SqlOperator calciteOperator()
{
- return SqlStdOperatorTable.CHAR_LENGTH;
+ return SQL_FUNCTION;
}
@Override
- public RowExtraction convert(
+ public DruidExpression toDruidExpression(
final PlannerContext plannerContext,
- final List rowOrder,
- final RexNode expression
+ final RowSignature rowSignature,
+ final RexNode rexNode
)
{
- final RexCall call = (RexCall) expression;
- final RowExtraction arg = Expressions.toRowExtraction(
- plannerContext,
- rowOrder,
- call.getOperands().get(0)
- );
- if (arg == null) {
- return null;
- }
-
- return RowExtraction.of(
- arg.getColumn(),
- ExtractionFns.compose(StrlenExtractionFn.instance(), arg.getExtractionFn())
- );
+ return OperatorConversions.functionCall(plannerContext, rowSignature, rexNode, "timestamp_parse");
}
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/TimeShiftOperatorConversion.java b/sql/src/main/java/io/druid/sql/calcite/expression/TimeShiftOperatorConversion.java
new file mode 100644
index 000000000000..cc6dd6c94e78
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/TimeShiftOperatorConversion.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+public class TimeShiftOperatorConversion implements SqlOperatorConversion
+{
+ private static final SqlFunction SQL_FUNCTION = OperatorConversions
+ .operatorBuilder("TIME_SHIFT")
+ .operandTypes(SqlTypeFamily.TIMESTAMP, SqlTypeFamily.CHARACTER, SqlTypeFamily.INTEGER, SqlTypeFamily.CHARACTER)
+ .requiredOperands(3)
+ .returnType(SqlTypeName.TIMESTAMP)
+ .functionCategory(SqlFunctionCategory.TIMEDATE)
+ .build();
+
+ @Override
+ public SqlOperator calciteOperator()
+ {
+ return SQL_FUNCTION;
+ }
+
+ @Override
+ public DruidExpression toDruidExpression(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
+ {
+ return OperatorConversions.functionCall(plannerContext, rowSignature, rexNode, "timestamp_shift");
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/TimeUnits.java b/sql/src/main/java/io/druid/sql/calcite/expression/TimeUnits.java
index 5e029a4556b6..1390d340db47 100644
--- a/sql/src/main/java/io/druid/sql/calcite/expression/TimeUnits.java
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/TimeUnits.java
@@ -20,7 +20,6 @@
package io.druid.sql.calcite.expression;
import com.google.common.collect.ImmutableMap;
-import io.druid.java.util.common.granularity.Granularity;
import io.druid.java.util.common.granularity.PeriodGranularity;
import org.apache.calcite.avatica.util.TimeUnitRange;
import org.joda.time.DateTimeZone;
@@ -41,17 +40,6 @@ public class TimeUnits
.put(TimeUnitRange.YEAR, Period.years(1))
.build();
- // Note that QUARTER is not supported here.
- private static final Map EXTRACT_FORMAT_MAP = ImmutableMap.builder()
- .put(TimeUnitRange.SECOND, "s")
- .put(TimeUnitRange.MINUTE, "m")
- .put(TimeUnitRange.HOUR, "H")
- .put(TimeUnitRange.DAY, "d")
- .put(TimeUnitRange.WEEK, "w")
- .put(TimeUnitRange.MONTH, "M")
- .put(TimeUnitRange.YEAR, "Y")
- .build();
-
/**
* Returns the Druid QueryGranularity corresponding to a Calcite TimeUnitRange, or null if there is none.
*
@@ -60,7 +48,7 @@ public class TimeUnits
*
* @return queryGranularity, or null
*/
- public static Granularity toQueryGranularity(final TimeUnitRange timeUnitRange, final DateTimeZone timeZone)
+ public static PeriodGranularity toQueryGranularity(final TimeUnitRange timeUnitRange, final DateTimeZone timeZone)
{
final Period period = PERIOD_MAP.get(timeUnitRange);
if (period == null) {
@@ -69,16 +57,4 @@ public static Granularity toQueryGranularity(final TimeUnitRange timeUnitRange,
return new PeriodGranularity(period, null, timeZone);
}
-
- /**
- * Returns the Joda format string corresponding to extracting on a Calcite TimeUnitRange, or null if there is none.
- *
- * @param timeUnitRange time unit
- *
- * @return queryGranularity, or null
- */
- public static String toDateTimeFormat(final TimeUnitRange timeUnitRange)
- {
- return EXTRACT_FORMAT_MAP.get(timeUnitRange);
- }
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/expression/TimestampToMillisOperatorConversion.java b/sql/src/main/java/io/druid/sql/calcite/expression/TimestampToMillisOperatorConversion.java
new file mode 100644
index 000000000000..47954442b19e
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/expression/TimestampToMillisOperatorConversion.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.expression;
+
+import com.google.common.collect.Iterables;
+import io.druid.sql.calcite.planner.PlannerContext;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlFunction;
+import org.apache.calcite.sql.SqlFunctionCategory;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+public class TimestampToMillisOperatorConversion implements SqlOperatorConversion
+{
+ private static final SqlFunction SQL_FUNCTION = OperatorConversions
+ .operatorBuilder("TIMESTAMP_TO_MILLIS")
+ .operandTypes(SqlTypeFamily.TIMESTAMP)
+ .returnType(SqlTypeName.BIGINT)
+ .functionCategory(SqlFunctionCategory.TIMEDATE)
+ .build();
+
+ @Override
+ public SqlOperator calciteOperator()
+ {
+ return SQL_FUNCTION;
+ }
+
+ @Override
+ public DruidExpression toDruidExpression(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
+ {
+ // Nothing to do, just leave the operand unchanged. Druid treats millis and timestamps the same internally.
+ final RexCall call = (RexCall) rexNode;
+ return Expressions.toDruidExpression(plannerContext, rowSignature, Iterables.getOnlyElement(call.getOperands()));
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/filtration/ConvertBoundsToSelectors.java b/sql/src/main/java/io/druid/sql/calcite/filtration/ConvertBoundsToSelectors.java
index 15585bf093df..ddd3dd790629 100644
--- a/sql/src/main/java/io/druid/sql/calcite/filtration/ConvertBoundsToSelectors.java
+++ b/sql/src/main/java/io/druid/sql/calcite/filtration/ConvertBoundsToSelectors.java
@@ -23,7 +23,7 @@
import io.druid.query.filter.DimFilter;
import io.druid.query.filter.SelectorDimFilter;
import io.druid.query.ordering.StringComparator;
-import io.druid.sql.calcite.expression.RowExtraction;
+import io.druid.sql.calcite.expression.SimpleExtraction;
import io.druid.sql.calcite.table.RowSignature;
public class ConvertBoundsToSelectors extends BottomUpTransform
@@ -46,7 +46,7 @@ public DimFilter process(DimFilter filter)
if (filter instanceof BoundDimFilter) {
final BoundDimFilter bound = (BoundDimFilter) filter;
final StringComparator naturalStringComparator = sourceRowSignature.naturalStringComparator(
- RowExtraction.of(bound.getDimension(), bound.getExtractionFn())
+ SimpleExtraction.of(bound.getDimension(), bound.getExtractionFn())
);
if (bound.hasUpperBound()
diff --git a/sql/src/main/java/io/druid/sql/calcite/filtration/ConvertSelectorsToIns.java b/sql/src/main/java/io/druid/sql/calcite/filtration/ConvertSelectorsToIns.java
index 4d7572b84a24..ebac4e14bed5 100644
--- a/sql/src/main/java/io/druid/sql/calcite/filtration/ConvertSelectorsToIns.java
+++ b/sql/src/main/java/io/druid/sql/calcite/filtration/ConvertSelectorsToIns.java
@@ -26,7 +26,7 @@
import io.druid.query.filter.InDimFilter;
import io.druid.query.filter.OrDimFilter;
import io.druid.query.filter.SelectorDimFilter;
-import io.druid.sql.calcite.expression.RowExtraction;
+import io.druid.sql.calcite.expression.SimpleExtraction;
import io.druid.sql.calcite.table.RowSignature;
import java.util.List;
@@ -62,7 +62,7 @@ public DimFilter process(DimFilter filter)
final BoundRefKey boundRefKey = BoundRefKey.from(
selector,
sourceRowSignature.naturalStringComparator(
- RowExtraction.of(selector.getDimension(), selector.getExtractionFn())
+ SimpleExtraction.of(selector.getDimension(), selector.getExtractionFn())
)
);
List filterList = selectors.get(boundRefKey);
diff --git a/sql/src/main/java/io/druid/sql/calcite/filtration/Filtration.java b/sql/src/main/java/io/druid/sql/calcite/filtration/Filtration.java
index a1596484cc2e..42dbaef0ab1d 100644
--- a/sql/src/main/java/io/druid/sql/calcite/filtration/Filtration.java
+++ b/sql/src/main/java/io/druid/sql/calcite/filtration/Filtration.java
@@ -23,9 +23,9 @@
import com.google.common.collect.ImmutableList;
import io.druid.common.utils.JodaUtils;
import io.druid.java.util.common.ISE;
-import io.druid.js.JavaScriptConfig;
+import io.druid.math.expr.ExprMacroTable;
import io.druid.query.filter.DimFilter;
-import io.druid.query.filter.JavaScriptDimFilter;
+import io.druid.query.filter.ExpressionDimFilter;
import io.druid.query.spec.MultipleIntervalSegmentSpec;
import io.druid.query.spec.QuerySegmentSpec;
import io.druid.sql.calcite.table.RowSignature;
@@ -36,11 +36,11 @@
public class Filtration
{
private static final Interval ETERNITY = new Interval(JodaUtils.MIN_INSTANT, JodaUtils.MAX_INSTANT);
- private static final DimFilter MATCH_NOTHING = new JavaScriptDimFilter(
- "dummy", "function(x){return false;}", null, JavaScriptConfig.getEnabledInstance()
+ private static final DimFilter MATCH_NOTHING = new ExpressionDimFilter(
+ "1 == 2", ExprMacroTable.nil()
);
- private static final DimFilter MATCH_EVERYTHING = new JavaScriptDimFilter(
- "dummy", "function(x){return true;}", null, JavaScriptConfig.getEnabledInstance()
+ private static final DimFilter MATCH_EVERYTHING = new ExpressionDimFilter(
+ "1 == 1", ExprMacroTable.nil()
);
// 1) If "dimFilter" is null, it should be ignored and not affect filtration.
diff --git a/sql/src/main/java/io/druid/sql/calcite/planner/Calcites.java b/sql/src/main/java/io/druid/sql/calcite/planner/Calcites.java
index 5856deaaba6d..84bf943c8a33 100644
--- a/sql/src/main/java/io/druid/sql/calcite/planner/Calcites.java
+++ b/sql/src/main/java/io/druid/sql/calcite/planner/Calcites.java
@@ -21,7 +21,10 @@
import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Chars;
+import io.druid.java.util.common.IAE;
import io.druid.java.util.common.StringUtils;
+import io.druid.query.ordering.StringComparator;
+import io.druid.query.ordering.StringComparators;
import io.druid.segment.column.ValueType;
import io.druid.sql.calcite.schema.DruidSchema;
import io.druid.sql.calcite.schema.InformationSchema;
@@ -30,6 +33,7 @@
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.schema.Schema;
import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.util.ConversionUtil;
import org.joda.time.DateTime;
@@ -39,12 +43,16 @@
import java.nio.charset.Charset;
import java.util.Calendar;
import java.util.Locale;
+import java.util.NavigableSet;
+import java.util.TimeZone;
+import java.util.TreeSet;
/**
* Utility functions for Calcite.
*/
public class Calcites
{
+ private static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone("GMT");
private static final Charset DEFAULT_CHARSET = Charset.forName(ConversionUtil.NATIVE_UTF16_CHARSET_NAME);
private Calcites()
@@ -102,11 +110,12 @@ public static String escapeStringLiteral(final String s)
public static ValueType getValueTypeForSqlTypeName(SqlTypeName sqlTypeName)
{
- if (SqlTypeName.APPROX_TYPES.contains(sqlTypeName)) {
+ if (SqlTypeName.FRACTIONAL_TYPES.contains(sqlTypeName)) {
return ValueType.FLOAT;
} else if (SqlTypeName.TIMESTAMP == sqlTypeName
|| SqlTypeName.DATE == sqlTypeName
- || SqlTypeName.EXACT_TYPES.contains(sqlTypeName)) {
+ || SqlTypeName.BOOLEAN == sqlTypeName
+ || SqlTypeName.INT_TYPES.contains(sqlTypeName)) {
return ValueType.LONG;
} else if (SqlTypeName.CHAR_TYPES.contains(sqlTypeName)) {
return ValueType.STRING;
@@ -117,6 +126,16 @@ public static ValueType getValueTypeForSqlTypeName(SqlTypeName sqlTypeName)
}
}
+ public static StringComparator getStringComparatorForSqlTypeName(SqlTypeName sqlTypeName)
+ {
+ final ValueType valueType = getValueTypeForSqlTypeName(sqlTypeName);
+ if (valueType == ValueType.LONG || valueType == ValueType.FLOAT) {
+ return StringComparators.NUMERIC;
+ } else {
+ return StringComparators.LEXICOGRAPHIC;
+ }
+ }
+
/**
* Calcite expects "TIMESTAMP" types to be an instant that has the expected local time fields if printed as UTC.
*
@@ -156,10 +175,31 @@ public static int jodaToCalciteDate(final DateTime dateTime, final DateTimeZone
public static Calendar jodaToCalciteCalendarLiteral(final DateTime dateTime, final DateTimeZone timeZone)
{
final Calendar calendar = Calendar.getInstance(Locale.ENGLISH);
+ calendar.setTimeZone(GMT_TIME_ZONE);
calendar.setTimeInMillis(Calcites.jodaToCalciteTimestamp(dateTime, timeZone));
return calendar;
}
+ /**
+ * Translates "literal" (a TIMESTAMP or DATE literal) to milliseconds since the epoch using the provided
+ * session time zone.
+ *
+ * @param literal TIMESTAMP or DATE literal
+ * @param timeZone session time zone
+ *
+ * @return milliseconds time
+ */
+ public static DateTime calciteDateTimeLiteralToJoda(final RexNode literal, final DateTimeZone timeZone)
+ {
+ final SqlTypeName typeName = literal.getType().getSqlTypeName();
+ if (literal.getKind() != SqlKind.LITERAL || (typeName != SqlTypeName.TIMESTAMP && typeName != SqlTypeName.DATE)) {
+ throw new IAE("Expected TIMESTAMP or DATE literal but got[%s:%s]", literal.getKind(), typeName);
+ }
+
+ final Calendar calendar = (Calendar) RexLiteral.value(literal);
+ return calciteTimestampToJoda(calendar.getTimeInMillis(), timeZone);
+ }
+
/**
* The inverse of {@link #jodaToCalciteTimestamp(DateTime, DateTimeZone)}.
*
@@ -198,4 +238,10 @@ public static boolean isIntLiteral(final RexNode rexNode)
{
return rexNode instanceof RexLiteral && SqlTypeName.INT_TYPES.contains(rexNode.getType().getSqlTypeName());
}
+
+ public static boolean anyStartsWith(final TreeSet set, final String prefix)
+ {
+ final NavigableSet headSet = set.headSet(prefix, true);
+ return !headSet.isEmpty() && headSet.first().startsWith(prefix);
+ }
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/planner/DruidConformance.java b/sql/src/main/java/io/druid/sql/calcite/planner/DruidConformance.java
new file mode 100644
index 000000000000..235cd83c1121
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/planner/DruidConformance.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.planner;
+
+import org.apache.calcite.sql.validate.SqlAbstractConformance;
+
+/**
+ * Implementation of Calcite {@code SqlConformance} for Druid.
+ */
+public class DruidConformance extends SqlAbstractConformance
+{
+ private static final DruidConformance INSTANCE = new DruidConformance();
+
+ private DruidConformance()
+ {
+ // Singleton.
+ }
+
+ public static DruidConformance instance()
+ {
+ return INSTANCE;
+ }
+
+ @Override
+ public boolean isBangEqualAllowed()
+ {
+ // For x != y (as an alternative to x <> y)
+ return true;
+ }
+
+ @Override
+ public boolean isSortByOrdinal()
+ {
+ // For ORDER BY 1
+ return true;
+ }
+
+ @Override
+ public boolean isSortByAlias()
+ {
+ // For ORDER BY columnAlias (where columnAlias is a "column AS columnAlias")
+ return true;
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/planner/DruidConvertletTable.java b/sql/src/main/java/io/druid/sql/calcite/planner/DruidConvertletTable.java
index 4f370c99cde0..54b2350e10ab 100644
--- a/sql/src/main/java/io/druid/sql/calcite/planner/DruidConvertletTable.java
+++ b/sql/src/main/java/io/druid/sql/calcite/planner/DruidConvertletTable.java
@@ -67,7 +67,7 @@ public SqlRexConvertlet get(SqlCall call)
{
if (call.getKind() == SqlKind.EXTRACT && call.getOperandList().get(1).getKind() != SqlKind.LITERAL) {
// Avoid using the standard convertlet for EXTRACT(TIMEUNIT FROM col), since we want to handle it directly
- // in ExtractExtractionOperator.
+ // in ExtractOperationConversion.
return BYPASS_CONVERTLET;
} else {
final SqlRexConvertlet convertlet = table.get(call.getOperator());
diff --git a/sql/src/main/java/io/druid/sql/calcite/planner/DruidOperatorTable.java b/sql/src/main/java/io/druid/sql/calcite/planner/DruidOperatorTable.java
index 9e1aab354f00..73f98da4fdab 100644
--- a/sql/src/main/java/io/druid/sql/calcite/planner/DruidOperatorTable.java
+++ b/sql/src/main/java/io/druid/sql/calcite/planner/DruidOperatorTable.java
@@ -24,10 +24,10 @@
import io.druid.java.util.common.ISE;
import io.druid.java.util.common.StringUtils;
import io.druid.sql.calcite.aggregation.SqlAggregator;
-import io.druid.sql.calcite.expression.SqlExtractionOperator;
+import io.druid.sql.calcite.expression.SqlOperatorConversion;
+import org.apache.calcite.sql.SqlAggFunction;
import org.apache.calcite.sql.SqlFunctionCategory;
import org.apache.calcite.sql.SqlIdentifier;
-import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlOperatorTable;
import org.apache.calcite.sql.SqlSyntax;
@@ -43,12 +43,12 @@ public class DruidOperatorTable implements SqlOperatorTable
private static final SqlStdOperatorTable STANDARD_TABLE = SqlStdOperatorTable.instance();
private final Map aggregators;
- private final Map extractionOperators;
+ private final Map extractionOperators;
@Inject
public DruidOperatorTable(
final Set aggregators,
- final Set extractionOperators
+ final Set extractionOperators
)
{
this.aggregators = Maps.newHashMap();
@@ -61,24 +61,29 @@ public DruidOperatorTable(
}
}
- for (SqlExtractionOperator extractionFunction : extractionOperators) {
- final String lcname = StringUtils.toLowerCase(extractionFunction.calciteFunction().getName());
+ for (SqlOperatorConversion extractionFunction : extractionOperators) {
+ final String lcname = StringUtils.toLowerCase(extractionFunction.calciteOperator().getName());
if (this.aggregators.containsKey(lcname) || this.extractionOperators.put(lcname, extractionFunction) != null) {
throw new ISE("Cannot have two operators with name[%s]", lcname);
}
}
}
- public SqlAggregator lookupAggregator(final String opName)
+ public SqlAggregator lookupAggregator(final SqlAggFunction aggFunction)
{
- return aggregators.get(StringUtils.toLowerCase(opName));
+ final SqlAggregator sqlAggregator = aggregators.get(StringUtils.toLowerCase(aggFunction.getName()));
+ if (sqlAggregator != null && sqlAggregator.calciteFunction().equals(aggFunction)) {
+ return sqlAggregator;
+ } else {
+ return null;
+ }
}
- public SqlExtractionOperator lookupExtractionOperator(final SqlKind kind, final String opName)
+ public SqlOperatorConversion lookupOperatorConversion(final SqlOperator operator)
{
- final SqlExtractionOperator extractionOperator = extractionOperators.get(StringUtils.toLowerCase(opName));
- if (extractionOperator != null && extractionOperator.calciteFunction().getKind() == kind) {
- return extractionOperator;
+ final SqlOperatorConversion operatorConversion = extractionOperators.get(StringUtils.toLowerCase(operator.getName()));
+ if (operatorConversion != null && operatorConversion.calciteOperator().equals(operator)) {
+ return operatorConversion;
} else {
return null;
}
@@ -98,10 +103,9 @@ public void lookupOperatorOverloads(
operatorList.add(aggregator.calciteFunction());
}
- final SqlExtractionOperator extractionFunction =
- extractionOperators.get(StringUtils.toLowerCase(opName.getSimple()));
+ final SqlOperatorConversion extractionFunction = extractionOperators.get(StringUtils.toLowerCase(opName.getSimple()));
if (extractionFunction != null) {
- operatorList.add(extractionFunction.calciteFunction());
+ operatorList.add(extractionFunction.calciteOperator());
}
}
@@ -115,8 +119,8 @@ public List getOperatorList()
for (SqlAggregator aggregator : aggregators.values()) {
retVal.add(aggregator.calciteFunction());
}
- for (SqlExtractionOperator extractionFunction : extractionOperators.values()) {
- retVal.add(extractionFunction.calciteFunction());
+ for (SqlOperatorConversion extractionFunction : extractionOperators.values()) {
+ retVal.add(extractionFunction.calciteOperator());
}
retVal.addAll(STANDARD_TABLE.getOperatorList());
return retVal;
diff --git a/sql/src/main/java/io/druid/sql/calcite/planner/DruidRexExecutor.java b/sql/src/main/java/io/druid/sql/calcite/planner/DruidRexExecutor.java
new file mode 100644
index 000000000000..33a056dc75d7
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/planner/DruidRexExecutor.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.planner;
+
+import com.google.common.collect.ImmutableMap;
+import io.druid.math.expr.Expr;
+import io.druid.math.expr.ExprEval;
+import io.druid.math.expr.ExprType;
+import io.druid.math.expr.Parser;
+import io.druid.sql.calcite.expression.DruidExpression;
+import io.druid.sql.calcite.expression.Expressions;
+import io.druid.sql.calcite.table.RowSignature;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexExecutor;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.joda.time.DateTime;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * A Calcite {@code RexExecutor} that reduces Calcite expressions by evaluating them using Druid's own built-in
+ * expressions. This ensures that constant reduction is done in a manner consistent with the query runtime.
+ */
+public class DruidRexExecutor implements RexExecutor
+{
+ private static final RowSignature EMPTY_ROW_SIGNATURE = RowSignature.builder().build();
+
+ private final PlannerContext plannerContext;
+
+ public DruidRexExecutor(final PlannerContext plannerContext)
+ {
+ this.plannerContext = plannerContext;
+ }
+
+ @Override
+ public void reduce(
+ final RexBuilder rexBuilder,
+ final List constExps,
+ final List reducedValues
+ )
+ {
+ for (RexNode constExp : constExps) {
+ final DruidExpression druidExpression = Expressions.toDruidExpression(
+ plannerContext,
+ EMPTY_ROW_SIGNATURE,
+ constExp
+ );
+
+ if (druidExpression == null) {
+ reducedValues.add(constExp);
+ } else {
+ final SqlTypeName sqlTypeName = constExp.getType().getSqlTypeName();
+ final Expr expr = Parser.parse(druidExpression.getExpression(), plannerContext.getExprMacroTable());
+ final ExprEval exprResult = expr.eval(Parser.withMap(ImmutableMap.of()));
+ final Object literalValue;
+
+ if (sqlTypeName == SqlTypeName.BOOLEAN) {
+ literalValue = exprResult.asBoolean();
+ } else if (sqlTypeName == SqlTypeName.DATE || sqlTypeName == SqlTypeName.TIMESTAMP) {
+ literalValue = Calcites.jodaToCalciteCalendarLiteral(
+ new DateTime(exprResult.asLong()),
+ plannerContext.getTimeZone()
+ );
+ } else if (SqlTypeName.NUMERIC_TYPES.contains(sqlTypeName)) {
+ literalValue = exprResult.type() == ExprType.LONG
+ ? new BigDecimal(exprResult.asLong())
+ : new BigDecimal(exprResult.asDouble());
+ } else {
+ literalValue = exprResult.value();
+ }
+
+ reducedValues.add(rexBuilder.makeLiteral(literalValue, constExp.getType(), true));
+ }
+ }
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/planner/PlannerFactory.java b/sql/src/main/java/io/druid/sql/calcite/planner/PlannerFactory.java
index a0391e08ecde..6314f530d2f5 100644
--- a/sql/src/main/java/io/druid/sql/calcite/planner/PlannerFactory.java
+++ b/sql/src/main/java/io/druid/sql/calcite/planner/PlannerFactory.java
@@ -31,9 +31,7 @@
import org.apache.calcite.plan.ConventionTraitDef;
import org.apache.calcite.rel.RelCollationTraitDef;
import org.apache.calcite.rel.type.RelDataTypeSystem;
-import org.apache.calcite.rex.RexExecutorImpl;
import org.apache.calcite.schema.SchemaPlus;
-import org.apache.calcite.schema.Schemas;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.tools.FrameworkConfig;
import org.apache.calcite.tools.Frameworks;
@@ -48,6 +46,7 @@ public class PlannerFactory
.setUnquotedCasing(Casing.UNCHANGED)
.setQuotedCasing(Casing.UNCHANGED)
.setQuoting(Quoting.DOUBLE_QUOTE)
+ .setConformance(DruidConformance.instance())
.build();
private final SchemaPlus rootSchema;
@@ -87,7 +86,7 @@ public DruidPlanner createPlanner(final Map queryContext)
.convertletTable(new DruidConvertletTable(plannerContext))
.operatorTable(operatorTable)
.programs(Rules.programs(plannerContext, queryMaker))
- .executor(new RexExecutorImpl(Schemas.createDataContext(null)))
+ .executor(new DruidRexExecutor(plannerContext))
.context(Contexts.EMPTY_CONTEXT)
.typeSystem(RelDataTypeSystem.DEFAULT)
.defaultSchema(rootSchema.getSubSchema(DruidSchema.NAME))
diff --git a/sql/src/main/java/io/druid/sql/calcite/planner/Rules.java b/sql/src/main/java/io/druid/sql/calcite/planner/Rules.java
index a29d133217b2..71788457e110 100644
--- a/sql/src/main/java/io/druid/sql/calcite/planner/Rules.java
+++ b/sql/src/main/java/io/druid/sql/calcite/planner/Rules.java
@@ -21,6 +21,7 @@
import com.google.common.collect.ImmutableList;
import io.druid.sql.calcite.rel.QueryMaker;
+import io.druid.sql.calcite.rule.CaseFilteredAggregatorRule;
import io.druid.sql.calcite.rule.DruidFilterRule;
import io.druid.sql.calcite.rule.DruidRelToBindableRule;
import io.druid.sql.calcite.rule.DruidRelToDruidRule;
@@ -215,6 +216,7 @@ private static List baseRuleSet(
}
rules.add(SortCollapseRule.instance());
+ rules.add(CaseFilteredAggregatorRule.instance());
// Druid-specific rules.
rules.add(new DruidTableScanRule(plannerContext, queryMaker));
diff --git a/sql/src/main/java/io/druid/sql/calcite/rel/DruidNestedGroupBy.java b/sql/src/main/java/io/druid/sql/calcite/rel/DruidNestedGroupBy.java
index fc7838681f73..0cf203bf6d8d 100644
--- a/sql/src/main/java/io/druid/sql/calcite/rel/DruidNestedGroupBy.java
+++ b/sql/src/main/java/io/druid/sql/calcite/rel/DruidNestedGroupBy.java
@@ -130,13 +130,7 @@ public QueryDataSource asDataSource()
if (queryDataSource == null) {
return null;
} else {
- return new QueryDataSource(
- queryBuilder.toGroupByQuery(
- queryDataSource,
- sourceRel.getOutputRowSignature(),
- getPlannerContext().getQueryContext()
- )
- );
+ return new QueryDataSource(queryBuilder.toGroupByQuery(queryDataSource, getPlannerContext()));
}
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/rel/DruidQueryBuilder.java b/sql/src/main/java/io/druid/sql/calcite/rel/DruidQueryBuilder.java
index da0f72c45d19..3bae621ceb98 100644
--- a/sql/src/main/java/io/druid/sql/calcite/rel/DruidQueryBuilder.java
+++ b/sql/src/main/java/io/druid/sql/calcite/rel/DruidQueryBuilder.java
@@ -27,7 +27,9 @@
import io.druid.java.util.common.ISE;
import io.druid.java.util.common.granularity.Granularities;
import io.druid.java.util.common.granularity.Granularity;
+import io.druid.math.expr.ExprMacroTable;
import io.druid.query.DataSource;
+import io.druid.query.dimension.DefaultDimensionSpec;
import io.druid.query.dimension.DimensionSpec;
import io.druid.query.filter.DimFilter;
import io.druid.query.groupby.GroupByQuery;
@@ -43,12 +45,16 @@
import io.druid.query.topn.NumericTopNMetricSpec;
import io.druid.query.topn.TopNMetricSpec;
import io.druid.query.topn.TopNQuery;
+import io.druid.segment.VirtualColumn;
import io.druid.segment.VirtualColumns;
import io.druid.segment.column.Column;
import io.druid.segment.column.ValueType;
+import io.druid.sql.calcite.aggregation.Aggregation;
+import io.druid.sql.calcite.aggregation.DimensionExpression;
import io.druid.sql.calcite.expression.ExtractionFns;
import io.druid.sql.calcite.filtration.Filtration;
import io.druid.sql.calcite.planner.Calcites;
+import io.druid.sql.calcite.planner.PlannerContext;
import io.druid.sql.calcite.table.RowSignature;
import org.apache.calcite.plan.RelTrait;
import org.apache.calcite.rel.RelCollations;
@@ -58,20 +64,25 @@
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.sql.type.SqlTypeName;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
public class DruidQueryBuilder
{
+ private final RowSignature sourceRowSignature;
+ private final RowSignature outputRowSignature;
private final DimFilter filter;
private final SelectProjection selectProjection;
private final Grouping grouping;
private final DimFilter having;
private final DefaultLimitSpec limitSpec;
private final RelDataType rowType;
- private final RowSignature outputRowSignature;
private DruidQueryBuilder(
+ final RowSignature sourceRowSignature,
final DimFilter filter,
final SelectProjection selectProjection,
final Grouping grouping,
@@ -81,6 +92,7 @@ private DruidQueryBuilder(
final List rowOrder
)
{
+ this.sourceRowSignature = sourceRowSignature;
this.filter = filter;
this.selectProjection = selectProjection;
this.grouping = grouping;
@@ -110,16 +122,29 @@ private DruidQueryBuilder(
this.outputRowSignature = rowSignatureBuilder.build();
}
- public static DruidQueryBuilder fullScan(final RowSignature rowSignature, final RelDataTypeFactory relDataTypeFactory)
+ public static DruidQueryBuilder fullScan(
+ final RowSignature rowSignature,
+ final RelDataTypeFactory relDataTypeFactory
+ )
{
final RelDataType rowType = rowSignature.getRelDataType(relDataTypeFactory);
final List rowOrder = rowSignature.getRowOrder();
- return new DruidQueryBuilder(null, null, null, null, null, rowType, rowOrder);
+ return new DruidQueryBuilder(
+ rowSignature,
+ null,
+ null,
+ null,
+ null,
+ null,
+ rowType,
+ rowOrder
+ );
}
public DruidQueryBuilder withFilter(final DimFilter newFilter)
{
return new DruidQueryBuilder(
+ sourceRowSignature,
newFilter,
selectProjection,
grouping,
@@ -135,19 +160,15 @@ public DruidQueryBuilder withSelectProjection(final SelectProjection newProjecti
Preconditions.checkState(selectProjection == null, "cannot project twice");
Preconditions.checkState(grouping == null, "cannot project after grouping");
Preconditions.checkNotNull(newProjection, "newProjection");
- Preconditions.checkState(
- newProjection.getProject().getChildExps().size() == newRowOrder.size(),
- "project size[%,d] != rowOrder size[%,d]",
- newProjection.getProject().getChildExps().size(),
- newRowOrder.size()
- );
+
return new DruidQueryBuilder(
+ sourceRowSignature,
filter,
newProjection,
grouping,
having,
limitSpec,
- newProjection.getProject().getRowType(),
+ newProjection.getCalciteProject().getRowType(),
newRowOrder
);
}
@@ -163,7 +184,16 @@ public DruidQueryBuilder withGrouping(
Preconditions.checkState(limitSpec == null, "cannot add grouping after limitSpec");
Preconditions.checkNotNull(newGrouping, "newGrouping");
// Set selectProjection to null now that we're grouping. Grouping subsumes select projection.
- return new DruidQueryBuilder(filter, null, newGrouping, having, limitSpec, newRowType, newRowOrder);
+ return new DruidQueryBuilder(
+ sourceRowSignature,
+ filter,
+ null,
+ newGrouping,
+ having,
+ limitSpec,
+ newRowType,
+ newRowOrder
+ );
}
public DruidQueryBuilder withAdjustedGrouping(
@@ -175,7 +205,16 @@ public DruidQueryBuilder withAdjustedGrouping(
// Like withGrouping, but without any sanity checks. It's assumed that callers will pass something that makes sense.
// This is used when adjusting the Grouping while pushing down a post-Aggregate Project or Sort.
Preconditions.checkNotNull(newGrouping, "newGrouping");
- return new DruidQueryBuilder(filter, null, newGrouping, having, limitSpec, newRowType, newRowOrder);
+ return new DruidQueryBuilder(
+ sourceRowSignature,
+ filter,
+ null,
+ newGrouping,
+ having,
+ limitSpec,
+ newRowType,
+ newRowOrder
+ );
}
public DruidQueryBuilder withHaving(final DimFilter newHaving)
@@ -185,6 +224,7 @@ public DruidQueryBuilder withHaving(final DimFilter newHaving)
Preconditions.checkState(grouping != null, "cannot add having before grouping");
Preconditions.checkNotNull(newHaving, "newHaving");
return new DruidQueryBuilder(
+ sourceRowSignature,
filter,
selectProjection,
grouping,
@@ -200,6 +240,7 @@ public DruidQueryBuilder withLimitSpec(final DefaultLimitSpec newLimitSpec)
Preconditions.checkState(limitSpec == null, "cannot add limitSpec twice");
Preconditions.checkNotNull(newLimitSpec, "newLimitSpec");
return new DruidQueryBuilder(
+ sourceRowSignature,
filter,
selectProjection,
grouping,
@@ -210,6 +251,25 @@ public DruidQueryBuilder withLimitSpec(final DefaultLimitSpec newLimitSpec)
);
}
+ public VirtualColumns getVirtualColumns(final ExprMacroTable macroTable)
+ {
+ final List retVal = new ArrayList<>();
+
+ if (grouping != null) {
+ for (DimensionExpression dimensionExpression : grouping.getDimensions()) {
+ retVal.addAll(dimensionExpression.getVirtualColumns(macroTable));
+ }
+
+ for (Aggregation aggregation : grouping.getAggregations()) {
+ retVal.addAll(aggregation.getVirtualColumns());
+ }
+ } else if (selectProjection != null) {
+ retVal.addAll(selectProjection.getVirtualColumns());
+ }
+
+ return VirtualColumns.create(retVal);
+ }
+
public DimFilter getFilter()
{
return filter;
@@ -280,16 +340,14 @@ public RelTrait[] getRelTraits()
/**
* Return this query as a Timeseries query, or null if this query is not compatible with Timeseries.
*
- * @param dataSource data source to query
- * @param sourceRowSignature row signature of the dataSource
- * @param context query context
+ * @param dataSource data source to query
+ * @param plannerContext planner context
*
* @return query or null
*/
public TimeseriesQuery toTimeseriesQuery(
final DataSource dataSource,
- final RowSignature sourceRowSignature,
- final Map context
+ final PlannerContext plannerContext
)
{
if (grouping == null || having != null) {
@@ -297,7 +355,7 @@ public TimeseriesQuery toTimeseriesQuery(
}
final Granularity queryGranularity;
- final List dimensions = grouping.getDimensions();
+ final List dimensions = grouping.getDimensionSpecs();
if (dimensions.isEmpty()) {
queryGranularity = Granularities.ALL;
@@ -336,13 +394,13 @@ public TimeseriesQuery toTimeseriesQuery(
final Map theContext = Maps.newHashMap();
theContext.put("skipEmptyBuckets", true);
- theContext.putAll(context);
+ theContext.putAll(plannerContext.getQueryContext());
return new TimeseriesQuery(
dataSource,
filtration.getQuerySegmentSpec(),
descending,
- VirtualColumns.EMPTY,
+ getVirtualColumns(plannerContext.getExprMacroTable()),
filtration.getDimFilter(),
queryGranularity,
grouping.getAggregatorFactories(),
@@ -354,33 +412,29 @@ public TimeseriesQuery toTimeseriesQuery(
/**
* Return this query as a TopN query, or null if this query is not compatible with TopN.
*
- * @param dataSource data source to query
- * @param sourceRowSignature row signature of the dataSource
- * @param context query context
- * @param maxTopNLimit maxTopNLimit from a PlannerConfig
- * @param useApproximateTopN from a PlannerConfig
+ * @param dataSource data source to query
+ * @param plannerContext planner context
*
* @return query or null
*/
public TopNQuery toTopNQuery(
final DataSource dataSource,
- final RowSignature sourceRowSignature,
- final Map context,
- final int maxTopNLimit,
- final boolean useApproximateTopN
+ final PlannerContext plannerContext
)
{
// Must have GROUP BY one column, ORDER BY zero or one column, limit less than maxTopNLimit, and no HAVING.
final boolean topNOk = grouping != null
&& grouping.getDimensions().size() == 1
&& limitSpec != null
- && (limitSpec.getColumns().size() <= 1 && limitSpec.getLimit() <= maxTopNLimit)
+ && (limitSpec.getColumns().size() <= 1
+ && limitSpec.getLimit() <= plannerContext.getPlannerConfig().getMaxTopNLimit())
&& having == null;
+
if (!topNOk) {
return null;
}
- final DimensionSpec dimensionSpec = Iterables.getOnlyElement(grouping.getDimensions());
+ final DimensionSpec dimensionSpec = Iterables.getOnlyElement(grouping.getDimensionSpecs());
final OrderByColumnSpec limitColumn;
if (limitSpec.getColumns().isEmpty()) {
limitColumn = new OrderByColumnSpec(
@@ -402,7 +456,7 @@ public TopNQuery toTopNQuery(
topNMetricSpec = limitColumn.getDirection() == OrderByColumnSpec.Direction.ASCENDING
? baseMetricSpec
: new InvertedTopNMetricSpec(baseMetricSpec);
- } else if (useApproximateTopN) {
+ } else if (plannerContext.getPlannerConfig().isUseApproximateTopN()) {
// ORDER BY metric
final NumericTopNMetricSpec baseMetricSpec = new NumericTopNMetricSpec(limitColumn.getDimension());
topNMetricSpec = limitColumn.getDirection() == OrderByColumnSpec.Direction.ASCENDING
@@ -416,8 +470,8 @@ public TopNQuery toTopNQuery(
return new TopNQuery(
dataSource,
- VirtualColumns.EMPTY,
- Iterables.getOnlyElement(grouping.getDimensions()),
+ getVirtualColumns(plannerContext.getExprMacroTable()),
+ dimensionSpec,
topNMetricSpec,
limitSpec.getLimit(),
filtration.getQuerySegmentSpec(),
@@ -425,23 +479,21 @@ public TopNQuery toTopNQuery(
Granularities.ALL,
grouping.getAggregatorFactories(),
grouping.getPostAggregators(),
- context
+ plannerContext.getQueryContext()
);
}
/**
* Return this query as a GroupBy query, or null if this query is not compatible with GroupBy.
*
- * @param dataSource data source to query
- * @param sourceRowSignature row signature of the dataSource
- * @param context query context
+ * @param dataSource data source to query
+ * @param plannerContext planner context
*
* @return query or null
*/
public GroupByQuery toGroupByQuery(
final DataSource dataSource,
- final RowSignature sourceRowSignature,
- final Map context
+ final PlannerContext plannerContext
)
{
if (grouping == null) {
@@ -453,31 +505,29 @@ public GroupByQuery toGroupByQuery(
return new GroupByQuery(
dataSource,
filtration.getQuerySegmentSpec(),
- VirtualColumns.EMPTY,
+ getVirtualColumns(plannerContext.getExprMacroTable()),
filtration.getDimFilter(),
Granularities.ALL,
- grouping.getDimensions(),
+ grouping.getDimensionSpecs(),
grouping.getAggregatorFactories(),
grouping.getPostAggregators(),
having != null ? new DimFilterHavingSpec(having) : null,
limitSpec,
- context
+ plannerContext.getQueryContext()
);
}
/**
* Return this query as a Select query, or null if this query is not compatible with Select.
*
- * @param dataSource data source to query
- * @param sourceRowSignature row signature of the dataSource
- * @param context query context
+ * @param dataSource data source to query
+ * @param plannerContext planner context
*
* @return query or null
*/
public SelectQuery toSelectQuery(
final DataSource dataSource,
- final RowSignature sourceRowSignature,
- final Map context
+ final PlannerContext plannerContext
)
{
if (grouping != null) {
@@ -502,22 +552,46 @@ public SelectQuery toSelectQuery(
descending = false;
}
+ // We need to ask for dummy columns to prevent Select from returning all of them.
+ String dummyColumn = "dummy";
+ while (sourceRowSignature.getColumnType(dummyColumn) != null
+ || getRowOrder().contains(dummyColumn)) {
+ dummyColumn = dummyColumn + "_";
+ }
+
+ final List metrics = new ArrayList<>();
+
+ if (selectProjection != null) {
+ metrics.addAll(selectProjection.getDirectColumns());
+ metrics.addAll(selectProjection.getVirtualColumns()
+ .stream()
+ .map(VirtualColumn::getOutputName)
+ .collect(Collectors.toList()));
+ } else {
+ // No projection, rowOrder should reference direct columns.
+ metrics.addAll(getRowOrder());
+ }
+
+ if (metrics.isEmpty()) {
+ metrics.add(dummyColumn);
+ }
+
return new SelectQuery(
dataSource,
filtration.getQuerySegmentSpec(),
descending,
filtration.getDimFilter(),
Granularities.ALL,
- selectProjection != null ? selectProjection.getDimensions() : ImmutableList.of(),
- selectProjection != null ? selectProjection.getMetrics() : ImmutableList.of(),
- null,
+ ImmutableList.of(new DefaultDimensionSpec(dummyColumn, dummyColumn)),
+ metrics.stream().sorted().distinct().collect(Collectors.toList()),
+ getVirtualColumns(plannerContext.getExprMacroTable()),
new PagingSpec(null, 0) /* dummy -- will be replaced */,
- context
+ plannerContext.getQueryContext()
);
}
@Override
- public boolean equals(Object o)
+ public boolean equals(final Object o)
{
if (this == o) {
return true;
@@ -525,44 +599,30 @@ public boolean equals(Object o)
if (o == null || getClass() != o.getClass()) {
return false;
}
-
- DruidQueryBuilder that = (DruidQueryBuilder) o;
-
- if (filter != null ? !filter.equals(that.filter) : that.filter != null) {
- return false;
- }
- if (selectProjection != null ? !selectProjection.equals(that.selectProjection) : that.selectProjection != null) {
- return false;
- }
- if (grouping != null ? !grouping.equals(that.grouping) : that.grouping != null) {
- return false;
- }
- if (having != null ? !having.equals(that.having) : that.having != null) {
- return false;
- }
- if (limitSpec != null ? !limitSpec.equals(that.limitSpec) : that.limitSpec != null) {
- return false;
- }
- if (rowType != null ? !rowType.equals(that.rowType) : that.rowType != null) {
- return false;
- }
- return outputRowSignature != null
- ? outputRowSignature.equals(that.outputRowSignature)
- : that.outputRowSignature == null;
-
+ final DruidQueryBuilder that = (DruidQueryBuilder) o;
+ return Objects.equals(sourceRowSignature, that.sourceRowSignature) &&
+ Objects.equals(outputRowSignature, that.outputRowSignature) &&
+ Objects.equals(filter, that.filter) &&
+ Objects.equals(selectProjection, that.selectProjection) &&
+ Objects.equals(grouping, that.grouping) &&
+ Objects.equals(having, that.having) &&
+ Objects.equals(limitSpec, that.limitSpec) &&
+ Objects.equals(rowType, that.rowType);
}
@Override
public int hashCode()
{
- int result = filter != null ? filter.hashCode() : 0;
- result = 31 * result + (selectProjection != null ? selectProjection.hashCode() : 0);
- result = 31 * result + (grouping != null ? grouping.hashCode() : 0);
- result = 31 * result + (having != null ? having.hashCode() : 0);
- result = 31 * result + (limitSpec != null ? limitSpec.hashCode() : 0);
- result = 31 * result + (rowType != null ? rowType.hashCode() : 0);
- result = 31 * result + (outputRowSignature != null ? outputRowSignature.hashCode() : 0);
- return result;
+ return Objects.hash(
+ sourceRowSignature,
+ outputRowSignature,
+ filter,
+ selectProjection,
+ grouping,
+ having,
+ limitSpec,
+ rowType
+ );
}
@Override
@@ -574,6 +634,7 @@ public String toString()
", grouping=" + grouping +
", having=" + having +
", limitSpec=" + limitSpec +
+ ", rowType=" + rowType +
", outputRowSignature=" + outputRowSignature +
'}';
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/rel/DruidQueryRel.java b/sql/src/main/java/io/druid/sql/calcite/rel/DruidQueryRel.java
index fa59c2cba000..fb49416ded25 100644
--- a/sql/src/main/java/io/druid/sql/calcite/rel/DruidQueryRel.java
+++ b/sql/src/main/java/io/druid/sql/calcite/rel/DruidQueryRel.java
@@ -24,6 +24,7 @@
import io.druid.java.util.common.guava.Sequence;
import io.druid.query.QueryDataSource;
import io.druid.query.groupby.GroupByQuery;
+import io.druid.segment.VirtualColumns;
import io.druid.sql.calcite.filtration.Filtration;
import io.druid.sql.calcite.planner.PlannerContext;
import io.druid.sql.calcite.table.DruidTable;
@@ -91,11 +92,7 @@ public static DruidQueryRel fullScan(
@Override
public QueryDataSource asDataSource()
{
- final GroupByQuery groupByQuery = getQueryBuilder().toGroupByQuery(
- druidTable.getDataSource(),
- druidTable.getRowSignature(),
- getPlannerContext().getQueryContext()
- );
+ final GroupByQuery groupByQuery = getQueryBuilder().toGroupByQuery(druidTable.getDataSource(), getPlannerContext());
if (groupByQuery == null) {
// QueryDataSources must currently embody groupBy queries. This will thrown an exception if the query
@@ -188,6 +185,10 @@ public RelWriter explainTerms(final RelWriter pw)
pw.item("dataSource", druidTable.getDataSource());
if (queryBuilder != null) {
final Filtration filtration = Filtration.create(queryBuilder.getFilter()).optimize(getSourceRowSignature());
+ final VirtualColumns virtualColumns = queryBuilder.getVirtualColumns(getPlannerContext().getExprMacroTable());
+ if (!virtualColumns.isEmpty()) {
+ pw.item("virtualColumns", virtualColumns);
+ }
if (!filtration.getIntervals().equals(ImmutableList.of(Filtration.eternity()))) {
pw.item("intervals", filtration.getIntervals());
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/rel/DruidSemiJoin.java b/sql/src/main/java/io/druid/sql/calcite/rel/DruidSemiJoin.java
index aa67e22e9c7c..65fafeeee9a1 100644
--- a/sql/src/main/java/io/druid/sql/calcite/rel/DruidSemiJoin.java
+++ b/sql/src/main/java/io/druid/sql/calcite/rel/DruidSemiJoin.java
@@ -31,9 +31,12 @@
import io.druid.query.filter.AndDimFilter;
import io.druid.query.filter.BoundDimFilter;
import io.druid.query.filter.DimFilter;
+import io.druid.query.filter.ExpressionDimFilter;
import io.druid.query.filter.OrDimFilter;
-import io.druid.sql.calcite.expression.RowExtraction;
-import io.druid.sql.calcite.planner.PlannerConfig;
+import io.druid.segment.VirtualColumn;
+import io.druid.segment.virtual.ExpressionVirtualColumn;
+import io.druid.sql.calcite.expression.DruidExpression;
+import io.druid.sql.calcite.planner.PlannerContext;
import io.druid.sql.calcite.table.RowSignature;
import org.apache.calcite.interpreter.BindableConvention;
import org.apache.calcite.plan.RelOptCluster;
@@ -44,6 +47,7 @@
import org.apache.calcite.rel.metadata.RelMetadataQuery;
import org.apache.calcite.rel.type.RelDataType;
+import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@@ -51,7 +55,7 @@ public class DruidSemiJoin extends DruidRel
{
private final DruidRel> left;
private final DruidRel> right;
- private final List leftRowExtractions;
+ private final List leftExpressions;
private final List rightKeys;
private final int maxSemiJoinRowsInMemory;
@@ -60,7 +64,7 @@ private DruidSemiJoin(
final RelTraitSet traitSet,
final DruidRel left,
final DruidRel right,
- final List leftRowExtractions,
+ final List leftExpressions,
final List rightKeys,
final int maxSemiJoinRowsInMemory
)
@@ -68,7 +72,7 @@ private DruidSemiJoin(
super(cluster, traitSet, left.getQueryMaker());
this.left = left;
this.right = right;
- this.leftRowExtractions = ImmutableList.copyOf(leftRowExtractions);
+ this.leftExpressions = ImmutableList.copyOf(leftExpressions);
this.rightKeys = ImmutableList.copyOf(rightKeys);
this.maxSemiJoinRowsInMemory = maxSemiJoinRowsInMemory;
}
@@ -78,17 +82,29 @@ public static DruidSemiJoin from(
final DruidRel right,
final List leftKeys,
final List rightKeys,
- final PlannerConfig plannerConfig
+ final PlannerContext plannerContext
)
{
- final ImmutableList.Builder listBuilder = ImmutableList.builder();
+ final ImmutableList.Builder listBuilder = ImmutableList.builder();
for (Integer key : leftKeys) {
- final RowExtraction rex = RowExtraction.fromQueryBuilder(left.getQueryBuilder(), key);
- if (rex == null) {
- // Can't figure out what to filter the left-hand side on...
- return null;
+ final String columnName = left.getQueryBuilder().getRowOrder().get(key);
+
+ final VirtualColumn leftVirtualColumn = left.getQueryBuilder()
+ .getVirtualColumns(plannerContext.getExprMacroTable())
+ .getVirtualColumn(columnName);
+
+ if (leftVirtualColumn != null) {
+ // VirtualColumns not allowed to remain in "left" since we have no way of forcing later rules to include them.
+ // See if we can get rid of this virtual column reference, otherwise give up.
+ if (leftVirtualColumn instanceof ExpressionVirtualColumn) {
+ final ExpressionVirtualColumn expressionColumn = (ExpressionVirtualColumn) leftVirtualColumn;
+ listBuilder.add(DruidExpression.fromExpression(expressionColumn.getExpression()));
+ } else {
+ return null;
+ }
+ } else {
+ listBuilder.add(DruidExpression.fromColumn(columnName));
}
- listBuilder.add(rex);
}
return new DruidSemiJoin(
@@ -98,7 +114,7 @@ public static DruidSemiJoin from(
right,
listBuilder.build(),
rightKeys,
- plannerConfig.getMaxSemiJoinRowsInMemory()
+ plannerContext.getPlannerConfig().getMaxSemiJoinRowsInMemory()
);
}
@@ -122,7 +138,7 @@ public DruidSemiJoin withQueryBuilder(final DruidQueryBuilder newQueryBuilder)
getTraitSet().plusAll(newQueryBuilder.getRelTraits()),
left.withQueryBuilder(newQueryBuilder),
right,
- leftRowExtractions,
+ leftExpressions,
rightKeys,
maxSemiJoinRowsInMemory
);
@@ -143,7 +159,7 @@ public DruidSemiJoin asBindable()
getTraitSet().replace(BindableConvention.INSTANCE),
left,
right,
- leftRowExtractions,
+ leftExpressions,
rightKeys,
maxSemiJoinRowsInMemory
);
@@ -157,7 +173,7 @@ public DruidSemiJoin asDruidConvention()
getTraitSet().replace(DruidConvention.instance()),
left,
right,
- leftRowExtractions,
+ leftExpressions,
rightKeys,
maxSemiJoinRowsInMemory
);
@@ -190,7 +206,7 @@ protected RelDataType deriveRowType()
public RelWriter explainTerms(RelWriter pw)
{
return pw
- .item("leftRowExtractions", leftRowExtractions)
+ .item("leftExpressions", leftExpressions)
.item("leftQuery", left.getQueryBuilder())
.item("rightKeys", rightKeys)
.item("rightQuery", right.getQueryBuilder());
@@ -210,13 +226,12 @@ private DruidRel> getLeftRelWithFilter()
{
// Build list of acceptable values from right side.
final Set> valuess = Sets.newHashSet();
- final List filters = Lists.newArrayList();
- right.runQuery().accumulate(
- null,
- new Accumulator()
+ final List filters = right.runQuery().accumulate(
+ new ArrayList<>(),
+ new Accumulator, Object[]>()
{
@Override
- public Object accumulate(final Object dummyValue, final Object[] row)
+ public List accumulate(final List theFilters, final Object[] row)
{
final List values = Lists.newArrayListWithCapacity(rightKeys.size());
@@ -234,22 +249,36 @@ public Object accumulate(final Object dummyValue, final Object[] row)
if (valuess.add(values)) {
final List bounds = Lists.newArrayList();
for (int i = 0; i < values.size(); i++) {
- bounds.add(
- new BoundDimFilter(
- leftRowExtractions.get(i).getColumn(),
- values.get(i),
- values.get(i),
- false,
- false,
- null,
- leftRowExtractions.get(i).getExtractionFn(),
- getSourceRowSignature().naturalStringComparator(leftRowExtractions.get(i))
- )
- );
+ final DruidExpression leftExpression = leftExpressions.get(i);
+ if (leftExpression.isSimpleExtraction()) {
+ bounds.add(
+ new BoundDimFilter(
+ leftExpression.getSimpleExtraction().getColumn(),
+ values.get(i),
+ values.get(i),
+ false,
+ false,
+ null,
+ leftExpression.getSimpleExtraction().getExtractionFn(),
+ getSourceRowSignature().naturalStringComparator(leftExpression.getSimpleExtraction())
+ )
+ );
+ } else {
+ bounds.add(
+ new ExpressionDimFilter(
+ String.format(
+ "(%s == %s)",
+ leftExpression.getExpression(),
+ DruidExpression.stringLiteral(values.get(i))
+ ),
+ getPlannerContext().getExprMacroTable()
+ )
+ );
+ }
}
- filters.add(new AndDimFilter(bounds));
+ theFilters.add(new AndDimFilter(bounds));
}
- return null;
+ return theFilters;
}
}
);
@@ -257,7 +286,7 @@ public Object accumulate(final Object dummyValue, final Object[] row)
valuess.clear();
if (!filters.isEmpty()) {
- // Add a filter to the left side. Use OR of singleton Bound filters so they can be simplified later.
+ // Add a filter to the left side.
final DimFilter semiJoinFilter = new OrDimFilter(filters);
final DimFilter newFilter = left.getQueryBuilder().getFilter() == null
? semiJoinFilter
diff --git a/sql/src/main/java/io/druid/sql/calcite/rel/Grouping.java b/sql/src/main/java/io/druid/sql/calcite/rel/Grouping.java
index ae6152b08776..32a441fcaf15 100644
--- a/sql/src/main/java/io/druid/sql/calcite/rel/Grouping.java
+++ b/sql/src/main/java/io/druid/sql/calcite/rel/Grouping.java
@@ -27,17 +27,19 @@
import io.druid.query.aggregation.PostAggregator;
import io.druid.query.dimension.DimensionSpec;
import io.druid.sql.calcite.aggregation.Aggregation;
+import io.druid.sql.calcite.aggregation.DimensionExpression;
import java.util.List;
import java.util.Set;
+import java.util.stream.Collectors;
public class Grouping
{
- private final List dimensions;
+ private final List dimensions;
private final List aggregations;
private Grouping(
- final List dimensions,
+ final List dimensions,
final List aggregations
)
{
@@ -46,9 +48,9 @@ private Grouping(
// Verify no collisions.
final Set seen = Sets.newHashSet();
- for (DimensionSpec dimensionSpec : dimensions) {
- if (!seen.add(dimensionSpec.getOutputName())) {
- throw new ISE("Duplicate field name: %s", dimensionSpec.getOutputName());
+ for (DimensionExpression dimensionExpression : dimensions) {
+ if (!seen.add(dimensionExpression.getOutputName())) {
+ throw new ISE("Duplicate field name: %s", dimensionExpression.getOutputName());
}
}
for (Aggregation aggregation : aggregations) {
@@ -58,20 +60,20 @@ private Grouping(
}
}
if (aggregation.getPostAggregator() != null && !seen.add(aggregation.getPostAggregator().getName())) {
- throw new ISE("Duplicate field name in rowOrder: %s", aggregation.getPostAggregator().getName());
+ throw new ISE("Duplicate field name: %s", aggregation.getPostAggregator().getName());
}
}
}
public static Grouping create(
- final List dimensions,
+ final List dimensions,
final List aggregations
)
{
return new Grouping(dimensions, aggregations);
}
- public List getDimensions()
+ public List getDimensions()
{
return dimensions;
}
@@ -81,6 +83,11 @@ public List getAggregations()
return aggregations;
}
+ public List getDimensionSpecs()
+ {
+ return dimensions.stream().map(DimensionExpression::toDimensionSpec).collect(Collectors.toList());
+ }
+
public List getAggregatorFactories()
{
final List retVal = Lists.newArrayList();
diff --git a/sql/src/main/java/io/druid/sql/calcite/rel/QueryMaker.java b/sql/src/main/java/io/druid/sql/calcite/rel/QueryMaker.java
index 5be2b554cea4..4e6f091dd2c9 100644
--- a/sql/src/main/java/io/druid/sql/calcite/rel/QueryMaker.java
+++ b/sql/src/main/java/io/druid/sql/calcite/rel/QueryMaker.java
@@ -30,12 +30,12 @@
import io.druid.java.util.common.ISE;
import io.druid.java.util.common.guava.Sequence;
import io.druid.java.util.common.guava.Sequences;
+import io.druid.math.expr.Evals;
import io.druid.query.DataSource;
import io.druid.query.QueryDataSource;
import io.druid.query.QueryPlus;
import io.druid.query.QuerySegmentWalker;
import io.druid.query.Result;
-import io.druid.query.dimension.DimensionSpec;
import io.druid.query.groupby.GroupByQuery;
import io.druid.query.select.EventHolder;
import io.druid.query.select.PagingSpec;
@@ -95,12 +95,7 @@ public Sequence runQuery(
)
{
if (dataSource instanceof QueryDataSource) {
- final GroupByQuery outerQuery = queryBuilder.toGroupByQuery(
- dataSource,
- sourceRowSignature,
- plannerContext.getQueryContext()
- );
-
+ final GroupByQuery outerQuery = queryBuilder.toGroupByQuery(dataSource, plannerContext);
if (outerQuery == null) {
// Bug in the planner rules. They shouldn't allow this to happen.
throw new IllegalStateException("Can't use QueryDataSource without an outer groupBy query!");
@@ -109,40 +104,22 @@ public Sequence runQuery(
return executeGroupBy(queryBuilder, outerQuery);
}
- final TimeseriesQuery timeseriesQuery = queryBuilder.toTimeseriesQuery(
- dataSource,
- sourceRowSignature,
- plannerContext.getQueryContext()
- );
- if (timeseriesQuery != null) {
- return executeTimeseries(queryBuilder, timeseriesQuery);
+ final TimeseriesQuery tsQuery = queryBuilder.toTimeseriesQuery(dataSource, plannerContext);
+ if (tsQuery != null) {
+ return executeTimeseries(queryBuilder, tsQuery);
}
- final TopNQuery topNQuery = queryBuilder.toTopNQuery(
- dataSource,
- sourceRowSignature,
- plannerContext.getQueryContext(),
- plannerContext.getPlannerConfig().getMaxTopNLimit(),
- plannerContext.getPlannerConfig().isUseApproximateTopN()
- );
+ final TopNQuery topNQuery = queryBuilder.toTopNQuery(dataSource, plannerContext);
if (topNQuery != null) {
return executeTopN(queryBuilder, topNQuery);
}
- final GroupByQuery groupByQuery = queryBuilder.toGroupByQuery(
- dataSource,
- sourceRowSignature,
- plannerContext.getQueryContext()
- );
+ final GroupByQuery groupByQuery = queryBuilder.toGroupByQuery(dataSource, plannerContext);
if (groupByQuery != null) {
return executeGroupBy(queryBuilder, groupByQuery);
}
- final SelectQuery selectQuery = queryBuilder.toSelectQuery(
- dataSource,
- sourceRowSignature,
- plannerContext.getQueryContext()
- );
+ final SelectQuery selectQuery = queryBuilder.toSelectQuery(dataSource, plannerContext);
if (selectQuery != null) {
return executeSelect(queryBuilder, selectQuery);
}
@@ -277,8 +254,10 @@ private Sequence executeTimeseries(
);
final List fieldList = queryBuilder.getRowType().getFieldList();
- final List dimensions = queryBuilder.getGrouping().getDimensions();
- final String timeOutputName = dimensions.isEmpty() ? null : Iterables.getOnlyElement(dimensions).getOutputName();
+ final String timeOutputName = queryBuilder.getGrouping().getDimensions().isEmpty()
+ ? null
+ : Iterables.getOnlyElement(queryBuilder.getGrouping().getDimensions())
+ .getOutputName();
Hook.QUERY_PLAN.run(query);
@@ -459,6 +438,14 @@ private Object coerce(final Object value, final SqlTypeName sqlType)
}
return Calcites.jodaToCalciteTimestamp(dateTime, plannerContext.getTimeZone());
+ } else if (sqlType == SqlTypeName.BOOLEAN) {
+ if (value instanceof String) {
+ coercedValue = Evals.asBoolean(((String) value));
+ } else if (value instanceof Number) {
+ coercedValue = Evals.asBoolean(((Number) value).longValue());
+ } else {
+ throw new ISE("Cannot coerce[%s] to %s", value.getClass().getName(), sqlType);
+ }
} else if (sqlType == SqlTypeName.INTEGER) {
if (value instanceof String) {
coercedValue = Ints.tryParse((String) value);
diff --git a/sql/src/main/java/io/druid/sql/calcite/rel/SelectProjection.java b/sql/src/main/java/io/druid/sql/calcite/rel/SelectProjection.java
index 048897609832..fceaee278849 100644
--- a/sql/src/main/java/io/druid/sql/calcite/rel/SelectProjection.java
+++ b/sql/src/main/java/io/druid/sql/calcite/rel/SelectProjection.java
@@ -19,62 +19,46 @@
package io.druid.sql.calcite.rel;
-import com.google.common.collect.Sets;
-import io.druid.java.util.common.ISE;
-import io.druid.query.dimension.DimensionSpec;
-import io.druid.segment.column.Column;
+import io.druid.segment.VirtualColumn;
import org.apache.calcite.rel.core.Project;
import java.util.List;
-import java.util.Set;
+import java.util.Objects;
public class SelectProjection
{
- private final Project project;
- private final List dimensions;
- private final List metrics;
+ private final Project calciteProject;
+ private final List directColumns;
+ private final List virtualColumns;
public SelectProjection(
- final Project project,
- final List dimensions,
- final List metrics
+ final Project calciteProject,
+ final List directColumns,
+ final List virtualColumns
)
{
- this.project = project;
- this.dimensions = dimensions;
- this.metrics = metrics;
-
- // Verify no collisions. Start with TIME_COLUMN_NAME because QueryMaker.executeSelect hard-codes it.
- final Set seen = Sets.newHashSet(Column.TIME_COLUMN_NAME);
- for (DimensionSpec dimensionSpec : dimensions) {
- if (!seen.add(dimensionSpec.getOutputName())) {
- throw new ISE("Duplicate field name: %s", dimensionSpec.getOutputName());
- }
- }
- for (String fieldName : metrics) {
- if (!seen.add(fieldName)) {
- throw new ISE("Duplicate field name: %s", fieldName);
- }
- }
+ this.calciteProject = calciteProject;
+ this.directColumns = directColumns;
+ this.virtualColumns = virtualColumns;
}
- public Project getProject()
+ public Project getCalciteProject()
{
- return project;
+ return calciteProject;
}
- public List getDimensions()
+ public List getDirectColumns()
{
- return dimensions;
+ return directColumns;
}
- public List getMetrics()
+ public List getVirtualColumns()
{
- return metrics;
+ return virtualColumns;
}
@Override
- public boolean equals(Object o)
+ public boolean equals(final Object o)
{
if (this == o) {
return true;
@@ -82,35 +66,25 @@ public boolean equals(Object o)
if (o == null || getClass() != o.getClass()) {
return false;
}
-
- SelectProjection that = (SelectProjection) o;
-
- if (project != null ? !project.equals(that.project) : that.project != null) {
- return false;
- }
- if (dimensions != null ? !dimensions.equals(that.dimensions) : that.dimensions != null) {
- return false;
- }
- return metrics != null ? metrics.equals(that.metrics) : that.metrics == null;
-
+ final SelectProjection that = (SelectProjection) o;
+ return Objects.equals(calciteProject, that.calciteProject) &&
+ Objects.equals(directColumns, that.directColumns) &&
+ Objects.equals(virtualColumns, that.virtualColumns);
}
@Override
public int hashCode()
{
- int result = project != null ? project.hashCode() : 0;
- result = 31 * result + (dimensions != null ? dimensions.hashCode() : 0);
- result = 31 * result + (metrics != null ? metrics.hashCode() : 0);
- return result;
+ return Objects.hash(calciteProject, directColumns, virtualColumns);
}
@Override
public String toString()
{
return "SelectProjection{" +
- "project=" + project +
- ", dimensions=" + dimensions +
- ", metrics=" + metrics +
+ "calciteProject=" + calciteProject +
+ ", directColumns=" + directColumns +
+ ", virtualColumns=" + virtualColumns +
'}';
}
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/rule/CaseFilteredAggregatorRule.java b/sql/src/main/java/io/druid/sql/calcite/rule/CaseFilteredAggregatorRule.java
new file mode 100644
index 000000000000..4e11736ddedb
--- /dev/null
+++ b/sql/src/main/java/io/druid/sql/calcite/rule/CaseFilteredAggregatorRule.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed to Metamarkets Group Inc. (Metamarkets) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. Metamarkets 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 io.druid.sql.calcite.rule;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import io.druid.sql.calcite.planner.Calcites;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Aggregate;
+import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.tools.RelBuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Rule that converts CASE-style filtered aggregation into true filtered aggregations.
+ */
+public class CaseFilteredAggregatorRule extends RelOptRule
+{
+ private static final CaseFilteredAggregatorRule INSTANCE = new CaseFilteredAggregatorRule();
+
+ private CaseFilteredAggregatorRule()
+ {
+ super(operand(Aggregate.class, operand(Project.class, any())));
+ }
+
+ public static CaseFilteredAggregatorRule instance()
+ {
+ return INSTANCE;
+ }
+
+ @Override
+ public boolean matches(final RelOptRuleCall call)
+ {
+ final Aggregate aggregate = call.rel(0);
+ final Project project = call.rel(1);
+
+ if (aggregate.indicator || aggregate.getGroupSets().size() != 1) {
+ return false;
+ }
+
+ for (AggregateCall aggregateCall : aggregate.getAggCallList()) {
+ if (isNonDistinctOneArgAggregateCall(aggregateCall)
+ && isThreeArgCase(project.getChildExps().get(Iterables.getOnlyElement(aggregateCall.getArgList())))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onMatch(RelOptRuleCall call)
+ {
+ final Aggregate aggregate = call.rel(0);
+ final Project project = call.rel(1);
+ final RexBuilder rexBuilder = aggregate.getCluster().getRexBuilder();
+ final List newCalls = new ArrayList<>(aggregate.getAggCallList().size());
+ final List newProjects = new ArrayList<>(project.getChildExps());
+ final List newCasts = new ArrayList<>(aggregate.getAggCallList().size());
+ final RelDataTypeFactory typeFactory = aggregate.getCluster().getTypeFactory();
+
+ for (AggregateCall aggregateCall : aggregate.getAggCallList()) {
+ AggregateCall newCall = null;
+
+ if (isNonDistinctOneArgAggregateCall(aggregateCall)) {
+ final RexNode rexNode = project.getChildExps().get(Iterables.getOnlyElement(aggregateCall.getArgList()));
+
+ // Styles supported:
+ //
+ // A1: AGG(CASE WHEN x = 'foo' THEN cnt END) => operands (x = 'foo', cnt, null)
+ // A2: SUM(CASE WHEN x = 'foo' THEN cnt ELSE 0 END) => operands (x = 'foo', cnt, 0); must be SUM
+ // B: SUM(CASE WHEN x = 'foo' THEN 1 ELSE 0 END) => operands (x = 'foo', 1, 0)
+ // C: COUNT(CASE WHEN x = 'foo' THEN 'dummy' END) => operands (x = 'foo', 'dummy', null)
+ //
+ // If the null and non-null args are switched, "flip" is set, which negates the filter.
+
+ if (isThreeArgCase(rexNode)) {
+ final RexCall caseCall = (RexCall) rexNode;
+
+ final boolean flip = RexLiteral.isNullLiteral(caseCall.getOperands().get(1))
+ && !RexLiteral.isNullLiteral(caseCall.getOperands().get(2));
+ final RexNode arg1 = caseCall.getOperands().get(flip ? 2 : 1);
+ final RexNode arg2 = caseCall.getOperands().get(flip ? 1 : 2);
+
+ // Operand 1: Filter
+ final RexNode filter;
+ final RelDataType booleanType = typeFactory.createSqlType(SqlTypeName.BOOLEAN);
+ final RexNode filterFromCase = rexBuilder.makeCall(
+ booleanType,
+ flip ? SqlStdOperatorTable.IS_FALSE : SqlStdOperatorTable.IS_TRUE,
+ ImmutableList.of(caseCall.getOperands().get(0))
+ );
+
+ if (aggregateCall.filterArg >= 0) {
+ filter = rexBuilder.makeCall(
+ booleanType,
+ SqlStdOperatorTable.AND,
+ ImmutableList.of(project.getProjects().get(aggregateCall.filterArg), filterFromCase)
+ );
+ } else {
+ filter = filterFromCase;
+ }
+
+ if (aggregateCall.getAggregation().getKind() == SqlKind.COUNT
+ && arg1 instanceof RexLiteral
+ && !RexLiteral.isNullLiteral(arg1)
+ && RexLiteral.isNullLiteral(arg2)) {
+ // Case C
+ newProjects.add(filter);
+ newCall = AggregateCall.create(
+ SqlStdOperatorTable.COUNT,
+ false,
+ ImmutableList.of(),
+ newProjects.size() - 1,
+ aggregateCall.getType(),
+ aggregateCall.getName()
+ );
+ } else if (aggregateCall.getAggregation().getKind() == SqlKind.SUM
+ && Calcites.isIntLiteral(arg1) && RexLiteral.intValue(arg1) == 1
+ && Calcites.isIntLiteral(arg2) && RexLiteral.intValue(arg2) == 0) {
+ // Case B
+ newProjects.add(filter);
+ newCall = AggregateCall.create(
+ SqlStdOperatorTable.COUNT,
+ false,
+ ImmutableList.of(),
+ newProjects.size() - 1,
+ typeFactory.createSqlType(SqlTypeName.BIGINT),
+ aggregateCall.getName()
+ );
+ } else if (RexLiteral.isNullLiteral(arg2) /* Case A1 */
+ || (aggregateCall.getAggregation().getKind() == SqlKind.SUM
+ && Calcites.isIntLiteral(arg2)
+ && RexLiteral.intValue(arg2) == 0) /* Case A2 */) {
+ newProjects.add(arg1);
+ newProjects.add(filter);
+ newCall = AggregateCall.create(
+ aggregateCall.getAggregation(),
+ false,
+ ImmutableList.of(newProjects.size() - 2),
+ newProjects.size() - 1,
+ aggregateCall.getType(),
+ aggregateCall.getName()
+ );
+ }
+ }
+ }
+
+ newCalls.add(newCall == null ? aggregateCall : newCall);
+
+ // Possibly CAST the new aggregator to an appropriate type.
+ final int i = newCasts.size();
+ final RelDataType oldType = aggregate.getRowType().getFieldList().get(i).getType();
+ if (newCall == null) {
+ newCasts.add(rexBuilder.makeInputRef(oldType, i));
+ } else {
+ newCasts.add(rexBuilder.makeCast(oldType, rexBuilder.makeInputRef(newCall.getType(), i)));
+ }
+ }
+
+ if (!newCalls.equals(aggregate.getAggCallList())) {
+ final RelBuilder relBuilder = call
+ .builder()
+ .push(project.getInput())
+ .project(newProjects);
+
+ final RelBuilder.GroupKey groupKey = relBuilder.groupKey(
+ aggregate.getGroupSet(),
+ aggregate.indicator,
+ aggregate.getGroupSets()
+ );
+
+ final RelNode newAggregate = relBuilder.aggregate(groupKey, newCalls).project(newCasts).build();
+
+ call.transformTo(newAggregate);
+ call.getPlanner().setImportance(aggregate, 0.0);
+ }
+ }
+
+ private static boolean isNonDistinctOneArgAggregateCall(final AggregateCall aggregateCall)
+ {
+ return aggregateCall.getArgList().size() == 1 && !aggregateCall.isDistinct();
+ }
+
+ private static boolean isThreeArgCase(final RexNode rexNode)
+ {
+ return rexNode.getKind() == SqlKind.CASE && ((RexCall) rexNode).getOperands().size() == 3;
+ }
+}
diff --git a/sql/src/main/java/io/druid/sql/calcite/rule/DruidSemiJoinRule.java b/sql/src/main/java/io/druid/sql/calcite/rule/DruidSemiJoinRule.java
index 5dc6a1f272dc..1aae13be2ec7 100644
--- a/sql/src/main/java/io/druid/sql/calcite/rule/DruidSemiJoinRule.java
+++ b/sql/src/main/java/io/druid/sql/calcite/rule/DruidSemiJoinRule.java
@@ -20,7 +20,7 @@
package io.druid.sql.calcite.rule;
import com.google.common.base.Predicate;
-import io.druid.query.dimension.DimensionSpec;
+import io.druid.sql.calcite.aggregation.DimensionExpression;
import io.druid.sql.calcite.planner.PlannerConfig;
import io.druid.sql.calcite.rel.DruidRel;
import io.druid.sql.calcite.rel.DruidSemiJoin;
@@ -119,8 +119,8 @@ public void onMatch(RelOptRuleCall call)
final JoinInfo joinInfo = join.analyzeCondition();
final List rightDimsOut = new ArrayList<>();
- for (DimensionSpec dimensionSpec : right.getQueryBuilder().getGrouping().getDimensions()) {
- rightDimsOut.add(right.getOutputRowSignature().getRowOrder().indexOf(dimensionSpec.getOutputName()));
+ for (DimensionExpression dimension : right.getQueryBuilder().getGrouping().getDimensions()) {
+ rightDimsOut.add(right.getOutputRowSignature().getRowOrder().indexOf(dimension.getOutputName()));
}
if (!joinInfo.isEqui() || !joinInfo.rightSet().equals(ImmutableBitSet.of(rightDimsOut))) {
@@ -130,7 +130,6 @@ public void onMatch(RelOptRuleCall call)
}
final RelBuilder relBuilder = call.builder();
- final PlannerConfig plannerConfig = left.getPlannerContext().getPlannerConfig();
if (join.getJoinType() == JoinRelType.LEFT) {
// Join can be eliminated since the right-hand side cannot have any effect (nothing is being selected,
@@ -142,7 +141,7 @@ public void onMatch(RelOptRuleCall call)
right,
joinInfo.leftKeys,
joinInfo.rightKeys,
- plannerConfig
+ left.getPlannerContext()
);
if (druidSemiJoin == null) {
@@ -150,6 +149,7 @@ public void onMatch(RelOptRuleCall call)
}
// Check maxQueryCount.
+ final PlannerConfig plannerConfig = left.getPlannerContext().getPlannerConfig();
if (plannerConfig.getMaxQueryCount() > 0 && druidSemiJoin.getQueryCount() > plannerConfig.getMaxQueryCount()) {
return;
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/rule/GroupByRules.java b/sql/src/main/java/io/druid/sql/calcite/rule/GroupByRules.java
index ba791141939e..71dcd82590fe 100644
--- a/sql/src/main/java/io/druid/sql/calcite/rule/GroupByRules.java
+++ b/sql/src/main/java/io/druid/sql/calcite/rule/GroupByRules.java
@@ -36,22 +36,23 @@
import io.druid.query.aggregation.LongSumAggregatorFactory;
import io.druid.query.aggregation.PostAggregator;
import io.druid.query.aggregation.post.ArithmeticPostAggregator;
+import io.druid.query.aggregation.post.ExpressionPostAggregator;
import io.druid.query.aggregation.post.FieldAccessPostAggregator;
-import io.druid.query.dimension.DimensionSpec;
import io.druid.query.filter.AndDimFilter;
import io.druid.query.filter.DimFilter;
-import io.druid.query.filter.NotDimFilter;
import io.druid.query.groupby.orderby.DefaultLimitSpec;
import io.druid.query.groupby.orderby.OrderByColumnSpec;
import io.druid.query.ordering.StringComparator;
import io.druid.query.ordering.StringComparators;
+import io.druid.segment.VirtualColumn;
import io.druid.segment.column.ValueType;
import io.druid.sql.calcite.aggregation.Aggregation;
import io.druid.sql.calcite.aggregation.ApproxCountDistinctSqlAggregator;
-import io.druid.sql.calcite.aggregation.PostAggregatorFactory;
+import io.druid.sql.calcite.aggregation.DimensionExpression;
import io.druid.sql.calcite.aggregation.SqlAggregator;
+import io.druid.sql.calcite.expression.DruidExpression;
import io.druid.sql.calcite.expression.Expressions;
-import io.druid.sql.calcite.expression.RowExtraction;
+import io.druid.sql.calcite.expression.SimpleExtraction;
import io.druid.sql.calcite.filtration.Filtration;
import io.druid.sql.calcite.planner.Calcites;
import io.druid.sql.calcite.planner.PlannerContext;
@@ -67,16 +68,22 @@
import org.apache.calcite.rel.core.Filter;
import org.apache.calcite.rel.core.Project;
import org.apache.calcite.rel.core.Sort;
-import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexInputRef;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.util.ImmutableBitSet;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Function;
+import java.util.stream.Collectors;
public class GroupByRules
{
@@ -99,64 +106,6 @@ public static List rules()
);
}
- /**
- * Used to represent inputs to aggregators. Ideally this should be folded into {@link RowExtraction}, but we
- * can't do that until RowExtractions are a bit more versatile.
- */
- private static class FieldOrExpression
- {
- private final String fieldName;
- private final String expression;
-
- public FieldOrExpression(String fieldName, String expression)
- {
- this.fieldName = fieldName;
- this.expression = expression;
- Preconditions.checkArgument(fieldName == null ^ expression == null, "must have either fieldName or expression");
- }
-
- public static FieldOrExpression fromRexNode(
- final PlannerContext plannerContext,
- final List rowOrder,
- final RexNode rexNode
- )
- {
- final RowExtraction rex = Expressions.toRowExtraction(plannerContext, rowOrder, rexNode);
- if (rex != null && rex.getExtractionFn() == null) {
- // This was a simple field access.
- return fieldName(rex.getColumn());
- }
-
- // Try as a math expression.
- final String mathExpression = Expressions.toMathExpression(rowOrder, rexNode);
- if (mathExpression != null) {
- return expression(mathExpression);
- }
-
- return null;
- }
-
- public static FieldOrExpression fieldName(final String fieldName)
- {
- return new FieldOrExpression(fieldName, null);
- }
-
- public static FieldOrExpression expression(final String expression)
- {
- return new FieldOrExpression(null, expression);
- }
-
- public String getFieldName()
- {
- return fieldName;
- }
-
- public String getExpression()
- {
- return expression;
- }
- }
-
public static class DruidAggregateRule extends RelOptRule
{
private DruidAggregateRule()
@@ -177,12 +126,7 @@ public void onMatch(RelOptRuleCall call)
{
final Aggregate aggregate = call.rel(0);
final DruidRel druidRel = call.rel(1);
- final DruidRel newDruidRel = GroupByRules.applyAggregate(
- druidRel,
- null,
- null,
- aggregate
- );
+ final DruidRel newDruidRel = GroupByRules.applyAggregate(druidRel, null, null, aggregate);
if (newDruidRel != null) {
call.transformTo(newDruidRel);
}
@@ -211,12 +155,7 @@ public void onMatch(RelOptRuleCall call)
final Aggregate aggregate = call.rel(0);
final Project project = call.rel(1);
final DruidRel druidRel = call.rel(2);
- final DruidRel newDruidRel = GroupByRules.applyAggregate(
- druidRel,
- null,
- project,
- aggregate
- );
+ final DruidRel newDruidRel = GroupByRules.applyAggregate(druidRel, null, project, aggregate);
if (newDruidRel != null) {
call.transformTo(newDruidRel);
}
@@ -354,6 +293,11 @@ private static boolean canApplyAggregate(
* Applies a filter -> project -> aggregate chain to a druidRel. Do not call this method unless
* {@link #canApplyAggregate(DruidRel, Filter, Project, Aggregate)} returns true.
*
+ * @param druidRel base rel to apply aggregation on top of
+ * @param filter0 filter that should be applied before aggregating
+ * @param project0 projection that should be applied before aggregating
+ * @param aggregate aggregation to apply
+ *
* @return new rel, or null if the chain cannot be applied
*/
private static DruidRel applyAggregate(
@@ -366,13 +310,21 @@ private static DruidRel applyAggregate(
Preconditions.checkState(canApplyAggregate(druidRel, filter0, project0, aggregate), "Cannot applyAggregate.");
final RowSignature sourceRowSignature;
+ final TreeSet reservedSourceRowNames = new TreeSet<>();
final boolean isNestedQuery = druidRel.getQueryBuilder().getGrouping() != null;
if (isNestedQuery) {
// Nested groupBy; source row signature is the output signature of druidRel.
sourceRowSignature = druidRel.getOutputRowSignature();
+ reservedSourceRowNames.addAll(sourceRowSignature.getRowOrder());
} else {
sourceRowSignature = druidRel.getSourceRowSignature();
+ reservedSourceRowNames.addAll(sourceRowSignature.getRowOrder());
+ reservedSourceRowNames.addAll(
+ Arrays.stream(druidRel.getQueryBuilder()
+ .getVirtualColumns(druidRel.getPlannerContext().getExprMacroTable())
+ .getVirtualColumns()).map(VirtualColumn::getOutputName).collect(Collectors.toList())
+ );
}
// Filter that should be applied before aggregating.
@@ -400,12 +352,12 @@ private static DruidRel applyAggregate(
project = project0;
} else if (druidRel.getQueryBuilder().getSelectProjection() != null && !isNestedQuery) {
// We're going to replace the existing druidRel, so inherit its projection.
- project = druidRel.getQueryBuilder().getSelectProjection().getProject();
+ project = druidRel.getQueryBuilder().getSelectProjection().getCalciteProject();
} else {
project = null;
}
- final List dimensions = Lists.newArrayList();
+ final List dimensions = Lists.newArrayList();
final List aggregations = Lists.newArrayList();
final List rowOrder = Lists.newArrayList();
@@ -414,19 +366,27 @@ private static DruidRel applyAggregate(
int dimOutputNameCounter = 0;
for (int i : groupSet) {
+ // Dimension might need to create virtual columns. Avoid giving it a name that would lead to colliding columns.
+ String dimOutputNameCurrent = "d" + dimOutputNameCounter++;
+ while (Calcites.anyStartsWith(reservedSourceRowNames, dimOutputNameCurrent + ":")) {
+ dimOutputNameCurrent = "d" + dimOutputNameCounter;
+ }
+
+ reservedSourceRowNames.add(dimOutputNameCurrent + ":");
+
if (project != null && project.getChildExps().get(i) instanceof RexLiteral) {
// Ignore literals in GROUP BY, so a user can write e.g. "GROUP BY 'dummy'" to group everything into a single
// row. Add dummy rowOrder entry so NULLs come out. This is not strictly correct but it works as long as
// nobody actually expects to see the literal.
- rowOrder.add(dimOutputName(dimOutputNameCounter++));
+ rowOrder.add(dimOutputNameCurrent);
} else {
final RexNode rexNode = Expressions.fromFieldAccess(sourceRowSignature, project, i);
- final RowExtraction rex = Expressions.toRowExtraction(
+ final DruidExpression druidExpression = Expressions.toDruidExpression(
druidRel.getPlannerContext(),
- sourceRowSignature.getRowOrder(),
+ sourceRowSignature,
rexNode
);
- if (rex == null) {
+ if (druidExpression == null) {
return null;
}
@@ -434,31 +394,36 @@ private static DruidRel applyAggregate(
final ValueType outputType = Calcites.getValueTypeForSqlTypeName(sqlTypeName);
if (outputType == null) {
throw new ISE("Cannot translate sqlTypeName[%s] to Druid type for field[%s]", sqlTypeName, rowOrder.get(i));
- }
-
- final DimensionSpec dimensionSpec = rex.toDimensionSpec(
- sourceRowSignature,
- dimOutputName(dimOutputNameCounter++),
- outputType
- );
- if (dimensionSpec == null) {
+ } else if (outputType == ValueType.COMPLEX) {
+ // Can't group on complex columns.
return null;
}
- dimensions.add(dimensionSpec);
- rowOrder.add(dimensionSpec.getOutputName());
+
+ dimensions.add(new DimensionExpression(dimOutputNameCurrent, druidExpression, outputType));
+ rowOrder.add(dimOutputNameCurrent);
}
}
// Translate aggregates.
+ int aggNameCounter = 0;
for (int i = 0; i < aggregate.getAggCallList().size(); i++) {
+ // Aggregation might need to create virtual columns. Avoid giving it a name that would lead to colliding columns.
+ String aggNameCurrent = "a" + aggNameCounter++;
+ while (Calcites.anyStartsWith(reservedSourceRowNames, aggNameCurrent + ":")) {
+ aggNameCurrent = "a" + aggNameCounter++;
+ }
+
+ reservedSourceRowNames.add(aggNameCurrent + ":");
+
final AggregateCall aggCall = aggregate.getAggCallList().get(i);
final Aggregation aggregation = translateAggregateCall(
druidRel.getPlannerContext(),
sourceRowSignature,
+ druidRel.getCluster().getRexBuilder(),
project,
aggCall,
aggregations,
- i
+ aggNameCurrent
);
if (aggregation == null) {
@@ -469,27 +434,20 @@ private static DruidRel applyAggregate(
rowOrder.add(aggregation.getOutputName());
}
+ final Grouping grouping = Grouping.create(dimensions, aggregations);
+
if (isNestedQuery) {
// Nested groupBy.
- return DruidNestedGroupBy.from(
- druidRel,
- filter,
- Grouping.create(dimensions, aggregations),
- aggregate.getRowType(),
- rowOrder
- );
+ return DruidNestedGroupBy.from(druidRel, filter, grouping, aggregate.getRowType(), rowOrder);
} else {
- // groupBy on a base dataSource.
+ // groupBy on a base dataSource or semiJoin.
return druidRel.withQueryBuilder(
druidRel.getQueryBuilder()
.withFilter(filter)
- .withGrouping(
- Grouping.create(dimensions, aggregations),
- aggregate.getRowType(),
- rowOrder
- )
+ .withGrouping(grouping, aggregate.getRowType(), rowOrder)
);
}
+
}
private static boolean canApplyPostAggregation(final DruidRel druidRel)
@@ -502,54 +460,56 @@ private static boolean canApplyPostAggregation(final DruidRel druidRel)
*
* @return new rel, or null if the projection cannot be applied
*/
- private static DruidRel applyPostAggregation(final DruidRel druidRel, final Project postProject)
+ private static DruidRel applyPostAggregation(
+ final DruidRel druidRel,
+ final Project postProject
+ )
{
Preconditions.checkState(canApplyPostAggregation(druidRel), "Cannot applyPostAggregation");
- final List rowOrder = druidRel.getQueryBuilder().getRowOrder();
final Grouping grouping = druidRel.getQueryBuilder().getGrouping();
final List newAggregations = Lists.newArrayList(grouping.getAggregations());
- final List finalizingPostAggregatorFactories = Lists.newArrayList();
final List newRowOrder = Lists.newArrayList();
-
- // Build list of finalizingPostAggregatorFactories.
- final Map aggregationMap = Maps.newHashMap();
- for (final Aggregation aggregation : grouping.getAggregations()) {
- aggregationMap.put(aggregation.getOutputName(), aggregation);
- }
- for (final String field : rowOrder) {
- final Aggregation aggregation = aggregationMap.get(field);
- finalizingPostAggregatorFactories.add(
- aggregation == null
- ? null
- : aggregation.getFinalizingPostAggregatorFactory()
- );
- }
+ final Set allPostAggregatorNames = grouping.getPostAggregators()
+ .stream()
+ .map(PostAggregator::getName)
+ .collect(Collectors.toSet());
// Walk through the postProject expressions.
+ int projectPostAggregatorCount = 0;
for (final RexNode projectExpression : postProject.getChildExps()) {
- if (projectExpression.isA(SqlKind.INPUT_REF)) {
- final RexInputRef ref = (RexInputRef) projectExpression;
- final String fieldName = rowOrder.get(ref.getIndex());
- newRowOrder.add(fieldName);
- finalizingPostAggregatorFactories.add(null);
+ // Attempt to convert to PostAggregator.
+ final DruidExpression postAggregatorExpression = Expressions.toDruidExpression(
+ druidRel.getPlannerContext(),
+ druidRel.getOutputRowSignature(),
+ projectExpression
+ );
+
+ if (postAggregatorExpression == null) {
+ return null;
+ }
+
+ if (postAggregatorExpression.isDirectColumnAccess()
+ && druidRel.getQueryBuilder()
+ .getOutputRowSignature()
+ .getColumnType(postAggregatorExpression.getDirectColumn())
+ .equals(Calcites.getValueTypeForSqlTypeName(projectExpression.getType().getSqlTypeName()))) {
+ // Direct column access, without any type cast as far as Druid's runtime is concerned.
+ // (There might be a SQL-level type cast that we don't care about)
+ newRowOrder.add(postAggregatorExpression.getDirectColumn());
} else {
- // Attempt to convert to PostAggregator.
- final String postAggregatorName = aggOutputName(newAggregations.size());
- final PostAggregator postAggregator = Expressions.toPostAggregator(
- postAggregatorName,
- rowOrder,
- finalizingPostAggregatorFactories,
- projectExpression,
- druidRel.getPlannerContext()
- );
- if (postAggregator != null) {
- newAggregations.add(Aggregation.create(postAggregator));
- newRowOrder.add(postAggregator.getName());
- finalizingPostAggregatorFactories.add(null);
- } else {
- return null;
+ String postAggregatorNameCurrent = "p" + projectPostAggregatorCount++;
+ while (allPostAggregatorNames.contains(postAggregatorNameCurrent)) {
+ postAggregatorNameCurrent = "p" + postAggregatorNameCurrent;
}
+ final PostAggregator postAggregator = new ExpressionPostAggregator(
+ postAggregatorNameCurrent,
+ postAggregatorExpression.getExpression(),
+ null,
+ druidRel.getPlannerContext().getExprMacroTable()
+ );
+ newAggregations.add(Aggregation.create(postAggregator));
+ newRowOrder.add(postAggregator.getName());
}
}
@@ -615,13 +575,14 @@ private static DruidRel applyLimit(final DruidRel druidRel, final Sort sort)
Preconditions.checkState(canApplyLimit(druidRel), "Cannot applyLimit.");
final Grouping grouping = druidRel.getQueryBuilder().getGrouping();
+ final RowSignature outputRowSignature = druidRel.getOutputRowSignature();
final DefaultLimitSpec limitSpec = toLimitSpec(druidRel.getQueryBuilder().getRowOrder(), sort);
if (limitSpec == null) {
return null;
}
final List orderBys = limitSpec.getColumns();
- final List newDimensions = Lists.newArrayList(grouping.getDimensions());
+ final List newDimensions = Lists.newArrayList(grouping.getDimensions());
// Reorder dimensions, maybe, to allow groupBy to consider pushing down sorting (see DefaultLimitSpec).
if (!orderBys.isEmpty()) {
@@ -632,15 +593,23 @@ private static DruidRel applyLimit(final DruidRel druidRel, final Sort sort)
for (int i = 0; i < orderBys.size(); i++) {
final OrderByColumnSpec orderBy = orderBys.get(i);
final Integer dimensionOrder = dimensionOrderByOutputName.get(orderBy.getDimension());
+ final StringComparator comparator = outputRowSignature.naturalStringComparator(
+ SimpleExtraction.of(orderBy.getDimension(), null)
+ );
+
if (dimensionOrder != null
- && dimensionOrder != i
&& orderBy.getDirection() == OrderByColumnSpec.Direction.ASCENDING
- && orderBy.getDimensionComparator().equals(StringComparators.LEXICOGRAPHIC)) {
- final DimensionSpec tmp = newDimensions.get(i);
- newDimensions.set(i, newDimensions.get(dimensionOrder));
- newDimensions.set(dimensionOrder, tmp);
- dimensionOrderByOutputName.put(newDimensions.get(i).getOutputName(), i);
- dimensionOrderByOutputName.put(newDimensions.get(dimensionOrder).getOutputName(), dimensionOrder);
+ && orderBy.getDimensionComparator().equals(comparator)) {
+ if (dimensionOrder != i) {
+ final DimensionExpression tmp = newDimensions.get(i);
+ newDimensions.set(i, newDimensions.get(dimensionOrder));
+ newDimensions.set(dimensionOrder, tmp);
+ dimensionOrderByOutputName.put(newDimensions.get(i).getOutputName(), i);
+ dimensionOrderByOutputName.put(newDimensions.get(dimensionOrder).getOutputName(), dimensionOrder);
+ }
+ } else {
+ // Ordering by something that we can't shift into the grouping key. Bail out.
+ break;
}
}
}
@@ -718,15 +687,14 @@ public static DefaultLimitSpec toLimitSpec(
private static Aggregation translateAggregateCall(
final PlannerContext plannerContext,
final RowSignature sourceRowSignature,
+ final RexBuilder rexBuilder,
final Project project,
final AggregateCall call,
final List existingAggregations,
- final int aggNumber
+ final String name
)
{
- final List filters = Lists.newArrayList();
- final List rowOrder = sourceRowSignature.getRowOrder();
- final String name = aggOutputName(aggNumber);
+ final DimFilter filter;
final SqlKind kind = call.getAggregation().getKind();
final SqlTypeName outputType = call.getType().getSqlTypeName();
@@ -738,33 +706,32 @@ private static Aggregation translateAggregateCall(
}
final RexNode expression = project.getChildExps().get(call.filterArg);
- final DimFilter filter = Expressions.toFilter(plannerContext, sourceRowSignature, expression);
+ filter = Expressions.toFilter(plannerContext, sourceRowSignature, expression);
if (filter == null) {
return null;
}
-
- filters.add(filter);
+ } else {
+ filter = null;
}
if (kind == SqlKind.COUNT && call.getArgList().isEmpty()) {
// COUNT(*)
- return Aggregation.create(new CountAggregatorFactory(name)).filter(makeFilter(filters, sourceRowSignature));
- } else if (kind == SqlKind.COUNT && call.isDistinct()) {
- // COUNT(DISTINCT x)
- if (plannerContext.getPlannerConfig().isUseApproximateCountDistinct()) {
+ return Aggregation.create(new CountAggregatorFactory(name)).filter(makeFilter(filter, sourceRowSignature));
+ } else if (call.isDistinct()) {
+ // AGG(DISTINCT x)
+ if (kind == SqlKind.COUNT && plannerContext.getPlannerConfig().isUseApproximateCountDistinct()) {
+ // Approximate COUNT(DISTINCT x)
return APPROX_COUNT_DISTINCT.toDruidAggregation(
name,
sourceRowSignature,
- plannerContext.getOperatorTable(),
plannerContext,
existingAggregations,
project,
call,
- makeFilter(filters, sourceRowSignature)
+ makeFilter(filter, sourceRowSignature)
);
} else {
- // Can't do exact distinct count as an aggregator. Return null here and give Calcite's rules a chance
- // to rewrite this query as a nested groupBy.
+ // Exact COUNT(DISTINCT x), or some non-COUNT aggregator.
return null;
}
} else if (kind == SqlKind.COUNT
@@ -774,88 +741,58 @@ private static Aggregation translateAggregateCall(
|| kind == SqlKind.MAX
|| kind == SqlKind.AVG) {
// Built-in agg, not distinct, not COUNT(*)
- boolean forceCount = false;
- final FieldOrExpression input;
-
- final int inputField = Iterables.getOnlyElement(call.getArgList());
- final RexNode rexNode = Expressions.fromFieldAccess(sourceRowSignature, project, inputField);
- final FieldOrExpression foe = FieldOrExpression.fromRexNode(plannerContext, rowOrder, rexNode);
-
- if (foe != null) {
- input = foe;
- } else if (rexNode.getKind() == SqlKind.CASE && ((RexCall) rexNode).getOperands().size() == 3) {
- // Possibly a CASE-style filtered aggregation. Styles supported:
- // A1: AGG(CASE WHEN x = 'foo' THEN cnt END) => operands (x = 'foo', cnt, null)
- // A2: SUM(CASE WHEN x = 'foo' THEN cnt ELSE 0 END) => operands (x = 'foo', cnt, 0); must be SUM
- // B: SUM(CASE WHEN x = 'foo' THEN 1 ELSE 0 END) => operands (x = 'foo', 1, 0)
- // C: COUNT(CASE WHEN x = 'foo' THEN 'dummy' END) => operands (x = 'foo', 'dummy', null)
- // If the null and non-null args are switched, "flip" is set, which negates the filter.
-
- final RexCall caseCall = (RexCall) rexNode;
- final boolean flip = RexLiteral.isNullLiteral(caseCall.getOperands().get(1))
- && !RexLiteral.isNullLiteral(caseCall.getOperands().get(2));
- final RexNode arg1 = caseCall.getOperands().get(flip ? 2 : 1);
- final RexNode arg2 = caseCall.getOperands().get(flip ? 1 : 2);
-
- // Operand 1: Filter
- final DimFilter filter = Expressions.toFilter(
- plannerContext,
- sourceRowSignature,
- caseCall.getOperands().get(0)
- );
- if (filter == null) {
- return null;
- } else {
- filters.add(flip ? new NotDimFilter(filter) : filter);
- }
+ final RexNode rexNode = Expressions.fromFieldAccess(
+ sourceRowSignature,
+ project,
+ Iterables.getOnlyElement(call.getArgList())
+ );
- if (call.getAggregation().getKind() == SqlKind.COUNT
- && arg1 instanceof RexLiteral
- && !RexLiteral.isNullLiteral(arg1)
- && RexLiteral.isNullLiteral(arg2)) {
- // Case C
- forceCount = true;
- input = null;
- } else if (call.getAggregation().getKind() == SqlKind.SUM
- && Calcites.isIntLiteral(arg1) && RexLiteral.intValue(arg1) == 1
- && Calcites.isIntLiteral(arg2) && RexLiteral.intValue(arg2) == 0) {
- // Case B
- forceCount = true;
- input = null;
- } else if (RexLiteral.isNullLiteral(arg2) /* Case A1 */
- || (kind == SqlKind.SUM
- && Calcites.isIntLiteral(arg2)
- && RexLiteral.intValue(arg2) == 0) /* Case A2 */) {
- input = FieldOrExpression.fromRexNode(plannerContext, rowOrder, arg1);
- if (input == null) {
- return null;
- }
- } else {
- // Can't translate CASE into a filter.
- return null;
- }
- } else {
- // Can't translate operand.
+ final DruidExpression input = toDruidExpressionForAggregator(plannerContext, sourceRowSignature, rexNode);
+ if (input == null) {
return null;
}
- if (!forceCount) {
- Preconditions.checkNotNull(input, "WTF?! input was null for non-COUNT aggregation");
- }
+ if (kind == SqlKind.COUNT) {
+ // COUNT(x) should count all non-null values of x.
+ if (rexNode.getType().isNullable()) {
+ final DimFilter nonNullFilter = Expressions.toFilter(
+ plannerContext,
+ sourceRowSignature,
+ rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, ImmutableList.of(rexNode))
+ );
+
+ if (nonNullFilter == null) {
+ // Don't expect this to happen.
+ throw new ISE("Could not create not-null filter for rexNode[%s]", rexNode);
+ }
- if (forceCount || kind == SqlKind.COUNT) {
- // COUNT(x)
- return Aggregation.create(new CountAggregatorFactory(name)).filter(makeFilter(filters, sourceRowSignature));
+ return Aggregation.create(new CountAggregatorFactory(name)).filter(
+ makeFilter(
+ filter == null ? nonNullFilter : new AndDimFilter(ImmutableList.of(filter, nonNullFilter)),
+ sourceRowSignature
+ )
+ );
+ } else {
+ return Aggregation.create(new CountAggregatorFactory(name)).filter(makeFilter(filter, sourceRowSignature));
+ }
} else {
// Built-in aggregator that is not COUNT.
final Aggregation retVal;
- final String fieldName = input.getFieldName();
- final String expression = input.getExpression();
- final ExprMacroTable macroTable = plannerContext.getExprMacroTable();
final boolean isLong = SqlTypeName.INT_TYPES.contains(outputType)
|| SqlTypeName.TIMESTAMP == outputType
|| SqlTypeName.DATE == outputType;
+ final String fieldName;
+ final String expression;
+ final ExprMacroTable macroTable = plannerContext.getExprMacroTable();
+
+ if (input.isDirectColumnAccess()) {
+ fieldName = input.getDirectColumn();
+ expression = null;
+ } else {
+ fieldName = null;
+ expression = input.getExpression();
+ }
if (kind == SqlKind.SUM || kind == SqlKind.SUM0) {
retVal = isLong
@@ -870,8 +807,8 @@ private static Aggregation translateAggregateCall(
? Aggregation.create(new LongMaxAggregatorFactory(name, fieldName, expression, macroTable))
: Aggregation.create(new DoubleMaxAggregatorFactory(name, fieldName, expression, macroTable));
} else if (kind == SqlKind.AVG) {
- final String sumName = aggInternalName(aggNumber, "sum");
- final String countName = aggInternalName(aggNumber, "count");
+ final String sumName = String.format("%s:sum", name);
+ final String countName = String.format("%s:count", name);
final AggregatorFactory sum = isLong
? new LongSumAggregatorFactory(sumName, fieldName, expression, macroTable)
: new DoubleSumAggregatorFactory(sumName, fieldName, expression, macroTable);
@@ -881,7 +818,7 @@ private static Aggregation translateAggregateCall(
new ArithmeticPostAggregator(
name,
"quotient",
- ImmutableList.of(
+ ImmutableList.of(
new FieldAccessPostAggregator(null, sumName),
new FieldAccessPostAggregator(null, countName)
)
@@ -892,46 +829,55 @@ private static Aggregation translateAggregateCall(
throw new ISE("WTF?! Kind[%s] got into the built-in aggregator path somehow?!", kind);
}
- return retVal.filter(makeFilter(filters, sourceRowSignature));
+ return retVal.filter(makeFilter(filter, sourceRowSignature));
}
} else {
// Not a built-in aggregator, check operator table.
final SqlAggregator sqlAggregator = plannerContext.getOperatorTable()
- .lookupAggregator(call.getAggregation().getName());
+ .lookupAggregator(call.getAggregation());
- return sqlAggregator != null ? sqlAggregator.toDruidAggregation(
- name,
- sourceRowSignature,
- plannerContext.getOperatorTable(),
- plannerContext,
- existingAggregations,
- project,
- call,
- makeFilter(filters, sourceRowSignature)
- ) : null;
+ if (sqlAggregator != null) {
+ return sqlAggregator.toDruidAggregation(
+ name,
+ sourceRowSignature,
+ plannerContext,
+ existingAggregations,
+ project,
+ call,
+ makeFilter(filter, sourceRowSignature)
+ );
+ } else {
+ return null;
+ }
}
}
- public static String dimOutputName(final int dimNumber)
- {
- return "d" + dimNumber;
- }
-
- private static String aggOutputName(final int aggNumber)
+ private static DruidExpression toDruidExpressionForAggregator(
+ final PlannerContext plannerContext,
+ final RowSignature rowSignature,
+ final RexNode rexNode
+ )
{
- return "a" + aggNumber;
- }
+ final DruidExpression druidExpression = Expressions.toDruidExpression(plannerContext, rowSignature, rexNode);
+ if (druidExpression == null) {
+ return null;
+ }
- private static String aggInternalName(final int aggNumber, final String key)
- {
- return "A" + aggNumber + ":" + key;
+ if (druidExpression.isSimpleExtraction() &&
+ (!druidExpression.isDirectColumnAccess()
+ || rowSignature.getColumnType(druidExpression.getDirectColumn()) == ValueType.STRING)) {
+ // Aggregators are unable to implicitly cast strings to numbers. So remove the simple extraction in this case.
+ return druidExpression.map(simpleExtraction -> null, Function.identity());
+ } else {
+ return druidExpression;
+ }
}
- private static DimFilter makeFilter(final List filters, final RowSignature sourceRowSignature)
+ private static DimFilter makeFilter(final DimFilter filter, final RowSignature sourceRowSignature)
{
- return filters.isEmpty()
+ return filter == null
? null
- : Filtration.create(new AndDimFilter(filters))
+ : Filtration.create(filter)
.optimizeFilterOnly(sourceRowSignature)
.getDimFilter();
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/rule/SelectRules.java b/sql/src/main/java/io/druid/sql/calcite/rule/SelectRules.java
index 802b585b2bf8..83a880a9ef0a 100644
--- a/sql/src/main/java/io/druid/sql/calcite/rule/SelectRules.java
+++ b/sql/src/main/java/io/druid/sql/calcite/rule/SelectRules.java
@@ -20,16 +20,12 @@
package io.druid.sql.calcite.rule;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
-import io.druid.java.util.common.ISE;
-import io.druid.query.dimension.DimensionSpec;
-import io.druid.query.extraction.ExtractionFn;
import io.druid.query.groupby.orderby.DefaultLimitSpec;
import io.druid.query.groupby.orderby.OrderByColumnSpec;
+import io.druid.segment.VirtualColumn;
import io.druid.segment.column.Column;
-import io.druid.segment.column.ValueType;
+import io.druid.sql.calcite.expression.DruidExpression;
import io.druid.sql.calcite.expression.Expressions;
-import io.druid.sql.calcite.expression.RowExtraction;
import io.druid.sql.calcite.planner.Calcites;
import io.druid.sql.calcite.rel.DruidRel;
import io.druid.sql.calcite.rel.SelectProjection;
@@ -38,9 +34,8 @@
import org.apache.calcite.plan.RelOptRuleCall;
import org.apache.calcite.rel.core.Project;
import org.apache.calcite.rel.core.Sort;
-import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.sql.type.SqlTypeName;
+import java.util.ArrayList;
import java.util.List;
public class SelectRules
@@ -85,69 +80,46 @@ public void onMatch(RelOptRuleCall call)
// Leave anything more complicated to DruidAggregateProjectRule for possible handling in a GroupBy query.
final RowSignature sourceRowSignature = druidRel.getSourceRowSignature();
- final List dimensions = Lists.newArrayList();
- final List metrics = Lists.newArrayList();
- final List rowOrder = Lists.newArrayList();
-
- int dimOutputNameCounter = 0;
- for (int i = 0; i < project.getRowType().getFieldCount(); i++) {
- final RexNode rexNode = project.getChildExps().get(i);
- final RowExtraction rex = Expressions.toRowExtraction(
- druidRel.getPlannerContext(),
- sourceRowSignature.getRowOrder(),
- rexNode
- );
-
- if (rex == null) {
- return;
- }
+ final List expressions = Expressions.toDruidExpressions(
+ druidRel.getPlannerContext(),
+ sourceRowSignature,
+ project.getChildExps()
+ );
- final String column = rex.getColumn();
- final ExtractionFn extractionFn = rex.getExtractionFn();
-
- // Check if this field should be a dimension, a metric, or a reference to __time.
- final ValueType columnType = sourceRowSignature.getColumnType(column);
-
- if (columnType == ValueType.STRING || (column.equals(Column.TIME_COLUMN_NAME) && extractionFn != null)) {
- // Add to dimensions.
- do {
- dimOutputNameCounter++;
- } while (sourceRowSignature.getColumnType(GroupByRules.dimOutputName(dimOutputNameCounter)) != null);
- final String outputName = GroupByRules.dimOutputName(dimOutputNameCounter);
- final SqlTypeName sqlTypeName = rexNode.getType().getSqlTypeName();
- final ValueType outputType = Calcites.getValueTypeForSqlTypeName(sqlTypeName);
- if (outputType == null) {
- throw new ISE("Cannot translate sqlTypeName[%s] to Druid type for field[%s]", sqlTypeName, outputName);
- }
- final DimensionSpec dimensionSpec = rex.toDimensionSpec(sourceRowSignature, outputName, columnType);
+ if (expressions == null) {
+ return;
+ }
- if (dimensionSpec == null) {
- // Really should have been possible due to the checks above.
- throw new ISE("WTF?! Could not create DimensionSpec for rowExtraction[%s].", rex);
- }
+ final List directColumns = new ArrayList<>();
+ final List virtualColumns = new ArrayList<>();
+ final List rowOrder = new ArrayList<>();
- dimensions.add(dimensionSpec);
- rowOrder.add(outputName);
- } else if (extractionFn == null && !column.equals(Column.TIME_COLUMN_NAME)) {
- // Add to metrics.
- metrics.add(column);
- rowOrder.add(column);
- } else if (extractionFn == null && column.equals(Column.TIME_COLUMN_NAME)) {
- // This is __time.
- rowOrder.add(Column.TIME_COLUMN_NAME);
+ int virtualColumnNameCounter = 0;
+ for (int i = 0; i < expressions.size(); i++) {
+ final DruidExpression expression = expressions.get(i);
+ if (expression.isDirectColumnAccess()) {
+ directColumns.add(expression.getDirectColumn());
+ rowOrder.add(expression.getDirectColumn());
} else {
- // Don't know what to do!
- return;
+ String candidate = "v" + virtualColumnNameCounter++;
+ while (sourceRowSignature.getColumnType(candidate) != null) {
+ candidate = "v" + virtualColumnNameCounter++;
+ }
+ virtualColumns.add(
+ expression.toVirtualColumn(
+ candidate,
+ Calcites.getValueTypeForSqlTypeName(project.getChildExps().get(i).getType().getSqlTypeName()),
+ druidRel.getPlannerContext().getExprMacroTable()
+ )
+ );
+ rowOrder.add(candidate);
}
}
call.transformTo(
druidRel.withQueryBuilder(
druidRel.getQueryBuilder()
- .withSelectProjection(
- new SelectProjection(project, dimensions, metrics),
- rowOrder
- )
+ .withSelectProjection(new SelectProjection(project, directColumns, virtualColumns), rowOrder)
)
);
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/rule/SortCollapseRule.java b/sql/src/main/java/io/druid/sql/calcite/rule/SortCollapseRule.java
index 7549cceb40e6..168908792f86 100644
--- a/sql/src/main/java/io/druid/sql/calcite/rule/SortCollapseRule.java
+++ b/sql/src/main/java/io/druid/sql/calcite/rule/SortCollapseRule.java
@@ -85,6 +85,7 @@ public void onMatch(final RelOptRuleCall call)
);
call.transformTo(combined);
+ call.getPlanner().setImportance(second, 0.0);
}
}
}
diff --git a/sql/src/main/java/io/druid/sql/calcite/table/RowSignature.java b/sql/src/main/java/io/druid/sql/calcite/table/RowSignature.java
index aada90bbae39..c02ebd94d51c 100644
--- a/sql/src/main/java/io/druid/sql/calcite/table/RowSignature.java
+++ b/sql/src/main/java/io/druid/sql/calcite/table/RowSignature.java
@@ -31,13 +31,14 @@
import io.druid.query.ordering.StringComparators;
import io.druid.segment.column.Column;
import io.druid.segment.column.ValueType;
-import io.druid.sql.calcite.expression.RowExtraction;
+import io.druid.sql.calcite.expression.SimpleExtraction;
import io.druid.sql.calcite.planner.Calcites;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.sql.SqlCollation;
import org.apache.calcite.sql.type.SqlTypeName;
+import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
@@ -95,15 +96,16 @@ public List getRowOrder()
* Return the "natural" {@link StringComparator} for an extraction from this row signature. This will be a
* lexicographic comparator for String types and a numeric comparator for Number types.
*
- * @param rowExtraction extraction from this kind of row
+ * @param simpleExtraction extraction from this kind of row
*
* @return natural comparator
*/
- public StringComparator naturalStringComparator(final RowExtraction rowExtraction)
+ @Nonnull
+ public StringComparator naturalStringComparator(final SimpleExtraction simpleExtraction)
{
- Preconditions.checkNotNull(rowExtraction, "rowExtraction");
- if (rowExtraction.getExtractionFn() != null
- || getColumnType(rowExtraction.getColumn()) == ValueType.STRING) {
+ Preconditions.checkNotNull(simpleExtraction, "simpleExtraction");
+ if (simpleExtraction.getExtractionFn() != null
+ || getColumnType(simpleExtraction.getColumn()) == ValueType.STRING) {
return StringComparators.LEXICOGRAPHIC;
} else {
return StringComparators.NUMERIC;
@@ -130,10 +132,13 @@ public RelDataType getRelDataType(final RelDataTypeFactory typeFactory)
switch (columnType) {
case STRING:
// Note that there is no attempt here to handle multi-value in any special way. Maybe one day...
- type = typeFactory.createTypeWithCharsetAndCollation(
- typeFactory.createSqlType(SqlTypeName.VARCHAR),
- Calcites.defaultCharset(),
- SqlCollation.IMPLICIT
+ type = typeFactory.createTypeWithNullability(
+ typeFactory.createTypeWithCharsetAndCollation(
+ typeFactory.createSqlType(SqlTypeName.VARCHAR),
+ Calcites.defaultCharset(),
+ SqlCollation.IMPLICIT
+ ),
+ true
);
break;
case LONG:
@@ -208,6 +213,9 @@ private Builder()
public Builder add(String columnName, ValueType columnType)
{
+ Preconditions.checkNotNull(columnName, "columnName");
+ Preconditions.checkNotNull(columnType, "columnType");
+
columnTypeList.add(Pair.of(columnName, columnType));
return this;
}
diff --git a/sql/src/main/java/io/druid/sql/guice/SqlBindings.java b/sql/src/main/java/io/druid/sql/guice/SqlBindings.java
index c0bbc4fc74dd..da879d05dbe8 100644
--- a/sql/src/main/java/io/druid/sql/guice/SqlBindings.java
+++ b/sql/src/main/java/io/druid/sql/guice/SqlBindings.java
@@ -22,7 +22,7 @@
import com.google.inject.Binder;
import com.google.inject.multibindings.Multibinder;
import io.druid.sql.calcite.aggregation.SqlAggregator;
-import io.druid.sql.calcite.expression.SqlExtractionOperator;
+import io.druid.sql.calcite.expression.SqlOperatorConversion;
public class SqlBindings
{
@@ -31,16 +31,14 @@ public static void addAggregator(
final Class extends SqlAggregator> aggregatorClass
)
{
- final Multibinder setBinder = Multibinder.newSetBinder(binder, SqlAggregator.class);
- setBinder.addBinding().to(aggregatorClass);
+ Multibinder.newSetBinder(binder, SqlAggregator.class).addBinding().to(aggregatorClass);
}
- public static void addExtractionOperator(
+ public static void addOperatorConversion(
final Binder binder,
- final Class extends SqlExtractionOperator> clazz
+ final Class extends SqlOperatorConversion> clazz
)
{
- final Multibinder setBinder = Multibinder.newSetBinder(binder, SqlExtractionOperator.class);
- setBinder.addBinding().to(clazz);
+ Multibinder.newSetBinder(binder, SqlOperatorConversion.class).addBinding().to(clazz);
}
}
diff --git a/sql/src/main/java/io/druid/sql/guice/SqlModule.java b/sql/src/main/java/io/druid/sql/guice/SqlModule.java
index 6be648b6dd54..3e266cd3a939 100644
--- a/sql/src/main/java/io/druid/sql/guice/SqlModule.java
+++ b/sql/src/main/java/io/druid/sql/guice/SqlModule.java
@@ -36,13 +36,21 @@
import io.druid.sql.avatica.DruidAvaticaHandler;
import io.druid.sql.calcite.aggregation.ApproxCountDistinctSqlAggregator;
import io.druid.sql.calcite.aggregation.SqlAggregator;
-import io.druid.sql.calcite.expression.CharacterLengthExtractionOperator;
-import io.druid.sql.calcite.expression.ExtractExtractionOperator;
-import io.druid.sql.calcite.expression.FloorExtractionOperator;
-import io.druid.sql.calcite.expression.LookupExtractionOperator;
-import io.druid.sql.calcite.expression.RegexpExtractExtractionOperator;
-import io.druid.sql.calcite.expression.SqlExtractionOperator;
-import io.druid.sql.calcite.expression.SubstringExtractionOperator;
+import io.druid.sql.calcite.expression.CeilOperatorConversion;
+import io.druid.sql.calcite.expression.ExtractOperatorConversion;
+import io.druid.sql.calcite.expression.FloorOperatorConversion;
+import io.druid.sql.calcite.expression.LookupOperatorConversion;
+import io.druid.sql.calcite.expression.MillisToTimestampOperatorConversion;
+import io.druid.sql.calcite.expression.RegexpExtractOperatorConversion;
+import io.druid.sql.calcite.expression.SqlOperatorConversion;
+import io.druid.sql.calcite.expression.SubstringOperatorConversion;
+import io.druid.sql.calcite.expression.TimeArithmeticOperatorConversion;
+import io.druid.sql.calcite.expression.TimeExtractOperatorConversion;
+import io.druid.sql.calcite.expression.TimeFloorOperatorConversion;
+import io.druid.sql.calcite.expression.TimeFormatOperatorConversion;
+import io.druid.sql.calcite.expression.TimeParseOperatorConversion;
+import io.druid.sql.calcite.expression.TimeShiftOperatorConversion;
+import io.druid.sql.calcite.expression.TimestampToMillisOperatorConversion;
import io.druid.sql.calcite.planner.Calcites;
import io.druid.sql.calcite.planner.PlannerConfig;
import io.druid.sql.calcite.schema.DruidSchema;
@@ -60,14 +68,23 @@ public class SqlModule implements Module
ApproxCountDistinctSqlAggregator.class
);
- public static final List> DEFAULT_EXTRACTION_OPERATOR_CLASSES = ImmutableList.>of(
- CharacterLengthExtractionOperator.class,
- ExtractExtractionOperator.class,
- FloorExtractionOperator.class,
- LookupExtractionOperator.class,
- SubstringExtractionOperator.class,
- RegexpExtractExtractionOperator.class
- );
+ public static final List> DEFAULT_OPERATOR_CONVERSION_CLASSES = ImmutableList.>builder()
+ .add(CeilOperatorConversion.class)
+ .add(ExtractOperatorConversion.class)
+ .add(FloorOperatorConversion.class)
+ .add(LookupOperatorConversion.class)
+ .add(MillisToTimestampOperatorConversion.class)
+ .add(RegexpExtractOperatorConversion.class)
+ .add(SubstringOperatorConversion.class)
+ .add(TimeArithmeticOperatorConversion.TimeMinusIntervalOperatorConversion.class)
+ .add(TimeArithmeticOperatorConversion.TimePlusIntervalOperatorConversion.class)
+ .add(TimeExtractOperatorConversion.class)
+ .add(TimeFloorOperatorConversion.class)
+ .add(TimeFormatOperatorConversion.class)
+ .add(TimeParseOperatorConversion.class)
+ .add(TimeShiftOperatorConversion.class)
+ .add(TimestampToMillisOperatorConversion.class)
+ .build();
private static final String PROPERTY_SQL_ENABLE = "druid.sql.enable";
private static final String PROPERTY_SQL_ENABLE_JSON_OVER_HTTP = "druid.sql.http.enable";
@@ -96,8 +113,8 @@ public void configure(Binder binder)
SqlBindings.addAggregator(binder, clazz);
}
- for (Class extends SqlExtractionOperator> clazz : DEFAULT_EXTRACTION_OPERATOR_CLASSES) {
- SqlBindings.addExtractionOperator(binder, clazz);
+ for (Class extends SqlOperatorConversion> clazz : DEFAULT_OPERATOR_CONVERSION_CLASSES) {
+ SqlBindings.addOperatorConversion(binder, clazz);
}
if (isJsonOverHttpEnabled()) {
diff --git a/sql/src/test/java/io/druid/sql/avatica/DruidAvaticaHandlerTest.java b/sql/src/test/java/io/druid/sql/avatica/DruidAvaticaHandlerTest.java
index e3103a5416c6..3de4c0c364aa 100644
--- a/sql/src/test/java/io/druid/sql/avatica/DruidAvaticaHandlerTest.java
+++ b/sql/src/test/java/io/druid/sql/avatica/DruidAvaticaHandlerTest.java
@@ -255,7 +255,7 @@ public void testExplainSelectCount() throws Exception
ImmutableList.of(
ImmutableMap.of(
"PLAN",
- "DruidQueryRel(dataSource=[foo], dimensions=[[]], aggregations=[[Aggregation{aggregatorFactories=[CountAggregatorFactory{name='a0'}], postAggregator=null, finalizingPostAggregatorFactory=null}]])\n"
+ "DruidQueryRel(dataSource=[foo], dimensions=[[]], aggregations=[[Aggregation{virtualColumns=[], aggregatorFactories=[CountAggregatorFactory{name='a0'}], postAggregator=null}]])\n"
)
),
getRows(resultSet)
@@ -340,7 +340,7 @@ public void testDatabaseMetaDataColumns() throws Exception
Pair.of("COLUMN_NAME", "dim1"),
Pair.of("DATA_TYPE", Types.VARCHAR),
Pair.of("TYPE_NAME", "VARCHAR"),
- Pair.of("IS_NULLABLE", "NO")
+ Pair.of("IS_NULLABLE", "YES")
),
ROW(
Pair.of("TABLE_SCHEM", "druid"),
@@ -348,7 +348,7 @@ public void testDatabaseMetaDataColumns() throws Exception
Pair.of("COLUMN_NAME", "dim2"),
Pair.of("DATA_TYPE", Types.VARCHAR),
Pair.of("TYPE_NAME", "VARCHAR"),
- Pair.of("IS_NULLABLE", "NO")
+ Pair.of("IS_NULLABLE", "YES")
),
ROW(
Pair.of("TABLE_SCHEM", "druid"),
diff --git a/sql/src/test/java/io/druid/sql/calcite/CalciteQueryTest.java b/sql/src/test/java/io/druid/sql/calcite/CalciteQueryTest.java
index d06284f2ccbf..1a4a746c2feb 100644
--- a/sql/src/test/java/io/druid/sql/calcite/CalciteQueryTest.java
+++ b/sql/src/test/java/io/druid/sql/calcite/CalciteQueryTest.java
@@ -42,27 +42,23 @@
import io.druid.query.aggregation.LongMaxAggregatorFactory;
import io.druid.query.aggregation.LongMinAggregatorFactory;
import io.druid.query.aggregation.LongSumAggregatorFactory;
-import io.druid.query.aggregation.PostAggregator;
import io.druid.query.aggregation.cardinality.CardinalityAggregatorFactory;
-import io.druid.query.aggregation.hyperloglog.HyperUniqueFinalizingPostAggregator;
import io.druid.query.aggregation.hyperloglog.HyperUniquesAggregatorFactory;
import io.druid.query.aggregation.post.ArithmeticPostAggregator;
-import io.druid.query.aggregation.post.ConstantPostAggregator;
import io.druid.query.aggregation.post.ExpressionPostAggregator;
import io.druid.query.aggregation.post.FieldAccessPostAggregator;
import io.druid.query.dimension.DefaultDimensionSpec;
import io.druid.query.dimension.DimensionSpec;
import io.druid.query.dimension.ExtractionDimensionSpec;
-import io.druid.query.extraction.BucketExtractionFn;
import io.druid.query.extraction.CascadeExtractionFn;
import io.druid.query.extraction.ExtractionFn;
import io.druid.query.extraction.RegexDimExtractionFn;
-import io.druid.query.extraction.StrlenExtractionFn;
import io.druid.query.extraction.SubstringDimExtractionFn;
import io.druid.query.extraction.TimeFormatExtractionFn;
import io.druid.query.filter.AndDimFilter;
import io.druid.query.filter.BoundDimFilter;
import io.druid.query.filter.DimFilter;
+import io.druid.query.filter.ExpressionDimFilter;
import io.druid.query.filter.InDimFilter;
import io.druid.query.filter.LikeDimFilter;
import io.druid.query.filter.NotDimFilter;
@@ -84,6 +80,7 @@
import io.druid.query.topn.TopNQueryBuilder;
import io.druid.segment.column.Column;
import io.druid.segment.column.ValueType;
+import io.druid.segment.virtual.ExpressionVirtualColumn;
import io.druid.server.initialization.ServerConfig;
import io.druid.sql.calcite.filtration.Filtration;
import io.druid.sql.calcite.planner.Calcites;
@@ -244,7 +241,7 @@ public void testSelectConstantExpression() throws Exception
{
testQuery(
"SELECT 1 + 1",
- ImmutableList.of(),
+ ImmutableList.of(),
ImmutableList.of(
new Object[]{2}
)
@@ -256,7 +253,7 @@ public void testExplainSelectConstantExpression() throws Exception
{
testQuery(
"EXPLAIN PLAN FOR SELECT 1 + 1",
- ImmutableList.of(),
+ ImmutableList.of(),
ImmutableList.of(
new Object[]{"BindableValues(tuples=[[{ 2 }]])\n"}
)
@@ -268,7 +265,7 @@ public void testInformationSchemaSchemata() throws Exception
{
testQuery(
"SELECT DISTINCT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA",
- ImmutableList.of(),
+ ImmutableList.of(),
ImmutableList.of(
new Object[]{"druid"},
new Object[]{"INFORMATION_SCHEMA"}
@@ -283,7 +280,7 @@ public void testInformationSchemaTables() throws Exception
"SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE\n"
+ "FROM INFORMATION_SCHEMA.TABLES\n"
+ "WHERE TABLE_TYPE IN ('SYSTEM_TABLE', 'TABLE', 'VIEW')",
- ImmutableList.of(),
+ ImmutableList.of(),
ImmutableList.of(
new Object[]{"druid", "foo", "TABLE"},
new Object[]{"druid", "foo2", "TABLE"},
@@ -303,12 +300,12 @@ public void testInformationSchemaColumnsOnTable() throws Exception
"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE\n"
+ "FROM INFORMATION_SCHEMA.COLUMNS\n"
+ "WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = 'foo'",
- ImmutableList.of(),
+ ImmutableList.of(),
ImmutableList.of(
new Object[]{"__time", "TIMESTAMP", "NO"},
new Object[]{"cnt", "BIGINT", "NO"},
- new Object[]{"dim1", "VARCHAR", "NO"},
- new Object[]{"dim2", "VARCHAR", "NO"},
+ new Object[]{"dim1", "VARCHAR", "YES"},
+ new Object[]{"dim2", "VARCHAR", "YES"},
new Object[]{"m1", "FLOAT", "NO"},
new Object[]{"unique_dim1", "OTHER", "NO"}
)
@@ -322,9 +319,9 @@ public void testInformationSchemaColumnsOnView() throws Exception
"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE\n"
+ "FROM INFORMATION_SCHEMA.COLUMNS\n"
+ "WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = 'aview'",
- ImmutableList.of(),
+ ImmutableList.of(),
ImmutableList.of(
- new Object[]{"dim1_firstchar", "VARCHAR", "NO"}
+ new Object[]{"dim1_firstchar", "VARCHAR", "YES"}
)
);
}
@@ -337,11 +334,11 @@ public void testExplainInformationSchemaColumns() throws Exception
+ "SELECT COLUMN_NAME, DATA_TYPE\n"
+ "FROM INFORMATION_SCHEMA.COLUMNS\n"
+ "WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = 'foo'",
- ImmutableList.of(),
+ ImmutableList.of(),
ImmutableList.of(
new Object[]{
"BindableProject(COLUMN_NAME=[$3], DATA_TYPE=[$7])\n"
- + " BindableFilter(condition=[AND(=(CAST($1):VARCHAR(5) CHARACTER SET \"UTF-16LE\" COLLATE \"UTF-16LE$en_US$primary\" NOT NULL, 'druid'), =(CAST($2):VARCHAR(3) CHARACTER SET \"UTF-16LE\" COLLATE \"UTF-16LE$en_US$primary\" NOT NULL, 'foo'))])\n"
+ + " BindableFilter(condition=[AND(=($1, 'druid'), =($2, 'foo'))])\n"
+ " BindableTableScan(table=[[INFORMATION_SCHEMA, COLUMNS]])\n"
}
)
@@ -353,11 +350,13 @@ public void testSelectStar() throws Exception
{
testQuery(
"SELECT * FROM druid.foo",
- ImmutableList.of(
+ ImmutableList.of(
Druids.newSelectQueryBuilder()
.dataSource(CalciteTests.DATASOURCE1)
.intervals(QSS(Filtration.eternity()))
.granularity(Granularities.ALL)
+ .dimensions(ImmutableList.of("dummy"))
+ .metrics(ImmutableList.of("__time", "cnt", "dim1", "dim2", "m1", "unique_dim1"))
.pagingSpec(FIRST_PAGING_SPEC)
.context(QUERY_CONTEXT_DEFAULT)
.build(),
@@ -365,6 +364,8 @@ public void testSelectStar() throws Exception
.dataSource(CalciteTests.DATASOURCE1)
.intervals(QSS(Filtration.eternity()))
.granularity(Granularities.ALL)
+ .dimensions(ImmutableList.of("dummy"))
+ .metrics(ImmutableList.of("__time", "cnt", "dim1", "dim2", "m1", "unique_dim1"))
.pagingSpec(
new PagingSpec(
ImmutableMap.of("foo_1970-01-01T00:00:00.000Z_2001-01-03T00:00:00.001Z_1", 5),
@@ -391,7 +392,7 @@ public void testUnqualifiedTableName() throws Exception
{
testQuery(
"SELECT COUNT(*) FROM foo",
- ImmutableList.of(
+ ImmutableList.of(
Druids.newTimeseriesQueryBuilder()
.dataSource(CalciteTests.DATASOURCE1)
.intervals(QSS(Filtration.eternity()))
@@ -411,7 +412,7 @@ public void testExplainSelectStar() throws Exception
{
testQuery(
"EXPLAIN PLAN FOR SELECT * FROM druid.foo",
- ImmutableList.of(),
+ ImmutableList.of(),
ImmutableList.of(
new Object[]{
"DruidQueryRel(dataSource=[foo])\n"
@@ -425,11 +426,13 @@ public void testSelectStarWithLimit() throws Exception
{
testQuery(
"SELECT * FROM druid.foo LIMIT 2",
- ImmutableList.of(
+ ImmutableList.of(
Druids.newSelectQueryBuilder()
.dataSource(CalciteTests.DATASOURCE1)
.intervals(QSS(Filtration.eternity()))
.granularity(Granularities.ALL)
+ .dimensions(ImmutableList.of("dummy"))
+ .metrics(ImmutableList.of("__time", "cnt", "dim1", "dim2", "m1", "unique_dim1"))
.pagingSpec(FIRST_PAGING_SPEC)
.context(QUERY_CONTEXT_DEFAULT)
.build()
@@ -446,11 +449,13 @@ public void testSelectStarWithLimitDescending() throws Exception
{
testQuery(
"SELECT * FROM druid.foo ORDER BY __time DESC LIMIT 2",
- ImmutableList.of(
+ ImmutableList.of(
Druids.newSelectQueryBuilder()
.dataSource(CalciteTests.DATASOURCE1)
.intervals(QSS(Filtration.eternity()))
.granularity(Granularities.ALL)
+ .dimensions(ImmutableList.of("dummy"))
+ .metrics(ImmutableList.of("__time", "cnt", "dim1", "dim2", "m1", "unique_dim1"))
.descending(true)
.pagingSpec(FIRST_PAGING_SPEC)
.context(QUERY_CONTEXT_DEFAULT)
@@ -468,16 +473,14 @@ public void testSelectSingleColumnTwice() throws Exception
{
testQuery(
"SELECT dim2 x, dim2 y FROM druid.foo LIMIT 2",
- ImmutableList.of(
+ ImmutableList.of(
Druids.newSelectQueryBuilder()
.dataSource(CalciteTests.DATASOURCE1)
.intervals(QSS(Filtration.eternity()))
- .dimensionSpecs(DIMS(
- new DefaultDimensionSpec("dim2", "d1"),
- new DefaultDimensionSpec("dim2", "d2")
- ))
.granularity(Granularities.ALL)
.descending(false)
+ .dimensions(ImmutableList.of("dummy"))
+ .metrics(ImmutableList.of("dim2"))
.pagingSpec(FIRST_PAGING_SPEC)
.context(QUERY_CONTEXT_DEFAULT)
.build()
@@ -494,13 +497,15 @@ public void testSelectSingleColumnWithLimitDescending() throws Exception
{
testQuery(
"SELECT dim1 FROM druid.foo ORDER BY __time DESC LIMIT 2",
- ImmutableList.of(
+ ImmutableList.of(
Druids.newSelectQueryBuilder()
.dataSource(CalciteTests.DATASOURCE1)
.intervals(QSS(Filtration.eternity()))
.dimensionSpecs(DIMS(new DefaultDimensionSpec("dim1", "d1")))
.granularity(Granularities.ALL)
.descending(true)
+ .dimensions(ImmutableList.of("dummy"))
+ .metrics(ImmutableList.of("__time", "dim1"))
.pagingSpec(FIRST_PAGING_SPEC)
.context(QUERY_CONTEXT_DEFAULT)
.build()
@@ -518,7 +523,7 @@ public void testGroupBySingleColumnDescendingNoTopN() throws Exception
testQuery(
PLANNER_CONFIG_DEFAULT,
"SELECT dim1 FROM druid.foo GROUP BY dim1 ORDER BY dim1 DESC",
- ImmutableList.of(
+ ImmutableList.of(
new GroupByQuery.Builder()
.setDataSource(CalciteTests.DATASOURCE1)
.setInterval(QSS(Filtration.eternity()))
@@ -560,11 +565,13 @@ public void testSelfJoinWithFallback() throws Exception
+ " druid.foo x INNER JOIN druid.foo y ON x.dim1 = y.dim2\n"
+ "WHERE\n"
+ " x.dim1 <> ''",
- ImmutableList.of(
+ ImmutableList.of(
Druids.newSelectQueryBuilder()
.dataSource(CalciteTests.DATASOURCE1)
.intervals(QSS(Filtration.eternity()))
.granularity(Granularities.ALL)
+ .dimensions(ImmutableList.of("dummy"))
+ .metrics(ImmutableList.of("__time", "cnt", "dim1", "dim2", "m1", "unique_dim1"))
.pagingSpec(FIRST_PAGING_SPEC)
.context(QUERY_CONTEXT_DEFAULT)
.build(),
@@ -572,6 +579,8 @@ public void testSelfJoinWithFallback() throws Exception
.dataSource(CalciteTests.DATASOURCE1)
.intervals(QSS(Filtration.eternity()))
.granularity(Granularities.ALL)
+ .dimensions(ImmutableList.of("dummy"))
+ .metrics(ImmutableList.of("__time", "cnt", "dim1", "dim2", "m1", "unique_dim1"))
.pagingSpec(
new PagingSpec(
ImmutableMap.of("foo_1970-01-01T00:00:00.000Z_2001-01-03T00:00:00.001Z_1", 5),
@@ -586,6 +595,8 @@ public void testSelfJoinWithFallback() throws Exception
.intervals(QSS(Filtration.eternity()))
.granularity(Granularities.ALL)
.filters(NOT(SELECTOR("dim1", "", null)))
+ .dimensions(ImmutableList.of("dummy"))
+ .metrics(ImmutableList.of("__time", "cnt", "dim1", "dim2", "m1", "unique_dim1"))
.pagingSpec(FIRST_PAGING_SPEC)
.context(QUERY_CONTEXT_DEFAULT)
.build(),
@@ -594,6 +605,8 @@ public void testSelfJoinWithFallback() throws Exception
.intervals(QSS(Filtration.eternity()))
.granularity(Granularities.ALL)
.filters(NOT(SELECTOR("dim1", "", null)))
+ .dimensions(ImmutableList.of("dummy"))
+ .metrics(ImmutableList.of("__time", "cnt", "dim1", "dim2", "m1", "unique_dim1"))
.pagingSpec(
new PagingSpec(
ImmutableMap.of("foo_1970-01-01T00:00:00.000Z_2001-01-03T00:00:00.001Z_1", 4),
@@ -621,7 +634,7 @@ public void testExplainSelfJoinWithFallback() throws Exception
+ " druid.foo x INNER JOIN druid.foo y ON x.dim1 = y.dim2\n"
+ "WHERE\n"
+ " x.dim1 <> ''",
- ImmutableList.of(),
+ ImmutableList.of(),
ImmutableList.of(
new Object[]{
"BindableProject(dim1=[$8], dim10=[$2], dim2=[$3])\n"
@@ -638,7 +651,7 @@ public void testGroupByLong() throws Exception
{
testQuery(
"SELECT cnt, COUNT(*) FROM druid.foo GROUP BY cnt",
- ImmutableList.of(
+ ImmutableList.of(
GroupByQuery.builder()
.setDataSource(CalciteTests.DATASOURCE1)
.setInterval(QSS(Filtration.eternity()))
@@ -659,7 +672,7 @@ public void testGroupByFloat() throws Exception
{
testQuery(
"SELECT m1, COUNT(*) FROM druid.foo GROUP BY m1",
- ImmutableList.of(
+ ImmutableList.of(
GroupByQuery.builder()
.setDataSource(CalciteTests.DATASOURCE1)
.setInterval(QSS(Filtration.eternity()))
@@ -685,7 +698,7 @@ public void testFilterOnFloat() throws Exception
{
testQuery(
"SELECT COUNT(*) FROM druid.foo WHERE m1 = 1.0",
- ImmutableList.of(
+ ImmutableList.of(
Druids.newTimeseriesQueryBuilder()
.dataSource(CalciteTests.DATASOURCE1)
.intervals(QSS(Filtration.eternity()))
@@ -706,7 +719,7 @@ public void testHavingOnFloat() throws Exception
{
testQuery(
"SELECT dim1, SUM(m1) AS m1_sum FROM druid.foo GROUP BY dim1 HAVING SUM(m1) > 1",
- ImmutableList.of(
+ ImmutableList.of(
GroupByQuery.builder()
.setDataSource(CalciteTests.DATASOURCE1)
.setInterval(QSS(Filtration.eternity()))
@@ -740,6 +753,368 @@ public void testHavingOnFloat() throws Exception
);
}
+ @Test
+ public void testColumnComparison() throws Exception
+ {
+ testQuery(
+ "SELECT dim1, m1, COUNT(*) FROM druid.foo WHERE m1 - 1 = dim1 GROUP BY dim1, m1",
+ ImmutableList.of(
+ GroupByQuery.builder()
+ .setDataSource(CalciteTests.DATASOURCE1)
+ .setInterval(QSS(Filtration.eternity()))
+ .setGranularity(Granularities.ALL)
+ .setDimFilter(EXPRESSION_FILTER("((\"m1\" - 1) == \"dim1\")"))
+ .setDimensions(DIMS(
+ new DefaultDimensionSpec("dim1", "d0"),
+ new DefaultDimensionSpec("m1", "d1", ValueType.FLOAT)
+ ))
+ .setAggregatorSpecs(AGGS(new CountAggregatorFactory("a0")))
+ .setContext(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"", 1.0d, 1L},
+ new Object[]{"2", 3.0d, 1L}
+ )
+ );
+ }
+
+ @Test
+ public void testHavingOnRatio() throws Exception
+ {
+ // Test for https://github.com/druid-io/druid/issues/4264
+
+ testQuery(
+ "SELECT\n"
+ + " dim1,\n"
+ + " COUNT(*) FILTER(WHERE dim2 <> 'a')/COUNT(*) as ratio\n"
+ + "FROM druid.foo\n"
+ + "GROUP BY dim1\n"
+ + "HAVING COUNT(*) FILTER(WHERE dim2 <> 'a')/COUNT(*) = 1",
+ ImmutableList.of(
+ GroupByQuery.builder()
+ .setDataSource(CalciteTests.DATASOURCE1)
+ .setInterval(QSS(Filtration.eternity()))
+ .setGranularity(Granularities.ALL)
+ .setDimensions(DIMS(new DefaultDimensionSpec("dim1", "d0")))
+ .setAggregatorSpecs(AGGS(
+ new FilteredAggregatorFactory(
+ new CountAggregatorFactory("a0"),
+ NOT(SELECTOR("dim2", "a", null))
+ ),
+ new CountAggregatorFactory("a1")
+ ))
+ .setPostAggregatorSpecs(ImmutableList.of(
+ EXPRESSION_POST_AGG("p0", "(\"a0\" / \"a1\")")
+ ))
+ .setHavingSpec(new DimFilterHavingSpec(EXPRESSION_FILTER("((\"a0\" / \"a1\") == 1)")))
+ .setContext(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"10.1", 1L},
+ new Object[]{"2", 1L},
+ new Object[]{"abc", 1L},
+ new Object[]{"def", 1L}
+ )
+ );
+ }
+
+ @Test
+ public void testGroupByWithSelectProjections() throws Exception
+ {
+ testQuery(
+ "SELECT\n"
+ + " dim1,"
+ + " SUBSTRING(dim1, 2)\n"
+ + "FROM druid.foo\n"
+ + "GROUP BY dim1\n",
+ ImmutableList.of(
+ GroupByQuery.builder()
+ .setDataSource(CalciteTests.DATASOURCE1)
+ .setInterval(QSS(Filtration.eternity()))
+ .setGranularity(Granularities.ALL)
+ .setDimensions(DIMS(new DefaultDimensionSpec("dim1", "d0")))
+ .setPostAggregatorSpecs(ImmutableList.of(
+ EXPRESSION_POST_AGG("p0", "substring(\"d0\", 1, -1)")
+ ))
+ .setContext(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"", ""},
+ new Object[]{"1", ""},
+ new Object[]{"10.1", "0.1"},
+ new Object[]{"2", ""},
+ new Object[]{"abc", "bc"},
+ new Object[]{"def", "ef"}
+ )
+ );
+ }
+
+ @Test
+ public void testGroupByWithSelectAndOrderByProjections() throws Exception
+ {
+ testQuery(
+ "SELECT\n"
+ + " dim1,"
+ + " SUBSTRING(dim1, 2)\n"
+ + "FROM druid.foo\n"
+ + "GROUP BY dim1\n"
+ + "ORDER BY CHARACTER_LENGTH(dim1) DESC, dim1",
+ ImmutableList.of(
+ GroupByQuery.builder()
+ .setDataSource(CalciteTests.DATASOURCE1)
+ .setInterval(QSS(Filtration.eternity()))
+ .setGranularity(Granularities.ALL)
+ .setDimensions(DIMS(new DefaultDimensionSpec("dim1", "d0")))
+ .setPostAggregatorSpecs(ImmutableList.of(
+ EXPRESSION_POST_AGG("p0", "substring(\"d0\", 1, -1)"),
+ EXPRESSION_POST_AGG("p1", "strlen(\"d0\")")
+ ))
+ .setLimitSpec(new DefaultLimitSpec(
+ ImmutableList.of(
+ new OrderByColumnSpec(
+ "p1",
+ OrderByColumnSpec.Direction.DESCENDING,
+ StringComparators.NUMERIC
+ ),
+ new OrderByColumnSpec(
+ "d0",
+ OrderByColumnSpec.Direction.ASCENDING,
+ StringComparators.LEXICOGRAPHIC
+ )
+ ),
+ Integer.MAX_VALUE
+ ))
+ .setContext(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"10.1", "0.1"},
+ new Object[]{"abc", "bc"},
+ new Object[]{"def", "ef"},
+ new Object[]{"1", ""},
+ new Object[]{"2", ""},
+ new Object[]{"", ""}
+ )
+ );
+ }
+
+ @Test
+ public void testTopNWithSelectProjections() throws Exception
+ {
+ testQuery(
+ "SELECT\n"
+ + " dim1,"
+ + " SUBSTRING(dim1, 2)\n"
+ + "FROM druid.foo\n"
+ + "GROUP BY dim1\n"
+ + "LIMIT 10",
+ ImmutableList.of(
+ new TopNQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(QSS(Filtration.eternity()))
+ .granularity(Granularities.ALL)
+ .dimension(new DefaultDimensionSpec("dim1", "d0"))
+ .postAggregators(ImmutableList.of(
+ EXPRESSION_POST_AGG("p0", "substring(\"d0\", 1, -1)")
+ ))
+ .metric(new DimensionTopNMetricSpec(null, StringComparators.LEXICOGRAPHIC))
+ .threshold(10)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"", ""},
+ new Object[]{"1", ""},
+ new Object[]{"10.1", "0.1"},
+ new Object[]{"2", ""},
+ new Object[]{"abc", "bc"},
+ new Object[]{"def", "ef"}
+ )
+ );
+ }
+
+ @Test
+ public void testTopNWithSelectAndOrderByProjections() throws Exception
+ {
+ testQuery(
+ "SELECT\n"
+ + " dim1,"
+ + " SUBSTRING(dim1, 2)\n"
+ + "FROM druid.foo\n"
+ + "GROUP BY dim1\n"
+ + "ORDER BY CHARACTER_LENGTH(dim1) DESC\n"
+ + "LIMIT 10",
+ ImmutableList.of(
+ new TopNQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(QSS(Filtration.eternity()))
+ .granularity(Granularities.ALL)
+ .dimension(new DefaultDimensionSpec("dim1", "d0"))
+ .postAggregators(ImmutableList.of(
+ EXPRESSION_POST_AGG("p0", "substring(\"d0\", 1, -1)"),
+ EXPRESSION_POST_AGG("p1", "strlen(\"d0\")")
+ ))
+ .metric(new NumericTopNMetricSpec("p1"))
+ .threshold(10)
+ .context(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"10.1", "0.1"},
+ new Object[]{"abc", "bc"},
+ new Object[]{"def", "ef"},
+ new Object[]{"1", ""},
+ new Object[]{"2", ""},
+ new Object[]{"", ""}
+ )
+ );
+ }
+
+ @Test
+ public void testGroupByCaseWhen() throws Exception
+ {
+ testQuery(
+ "SELECT\n"
+ + " CASE EXTRACT(DAY FROM __time)\n"
+ + " WHEN m1 THEN 'match-m1'\n"
+ + " WHEN cnt THEN 'match-cnt'\n"
+ + " WHEN 0 THEN 'zero'"
+ + " END,"
+ + " COUNT(*)\n"
+ + "FROM druid.foo\n"
+ + "GROUP BY"
+ + " CASE EXTRACT(DAY FROM __time)\n"
+ + " WHEN m1 THEN 'match-m1'\n"
+ + " WHEN cnt THEN 'match-cnt'\n"
+ + " WHEN 0 THEN 'zero'"
+ + " END",
+ ImmutableList.of(
+ GroupByQuery.builder()
+ .setDataSource(CalciteTests.DATASOURCE1)
+ .setInterval(QSS(Filtration.eternity()))
+ .setGranularity(Granularities.ALL)
+ .setVirtualColumns(
+ EXPRESSION_VIRTUAL_COLUMN(
+ "d0:v",
+ "case_searched("
+ + "(CAST(timestamp_extract(\"__time\",'DAY','UTC'), 'DOUBLE') == \"m1\"),"
+ + "'match-m1 ',"
+ + "(timestamp_extract(\"__time\",'DAY','UTC') == \"cnt\"),"
+ + "'match-cnt',"
+ + "(timestamp_extract(\"__time\",'DAY','UTC') == 0),"
+ + "'zero ',"
+ + "'')",
+ ValueType.STRING
+ )
+ )
+ .setDimensions(DIMS(new DefaultDimensionSpec("d0:v", "d0")))
+ .setAggregatorSpecs(AGGS(new CountAggregatorFactory("a0")))
+ .setContext(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"", 2L},
+ new Object[]{"match-cnt", 1L},
+ new Object[]{"match-m1 ", 3L}
+ )
+ );
+ }
+
+ @Test
+ public void testNullEmptyStringEquality() throws Exception
+ {
+ // Doesn't conform to the SQL standard, but it's how we do it.
+ // This example is used in the sql.md doc.
+
+ final ImmutableList wheres = ImmutableList.of(
+ "NULLIF(dim2, 'a') = ''",
+ "NULLIF(dim2, 'a') IS NULL"
+ );
+
+ for (String where : wheres) {
+ testQuery(
+ "SELECT COUNT(*)\n"
+ + "FROM druid.foo\n"
+ + "WHERE " + where,
+ ImmutableList.of(
+ Druids.newTimeseriesQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(QSS(Filtration.eternity()))
+ .granularity(Granularities.ALL)
+ .filters(EXPRESSION_FILTER("case_searched((\"dim2\" == 'a'),1,(\"dim2\" == ''))"))
+ .aggregators(AGGS(new CountAggregatorFactory("a0")))
+ .context(TIMESERIES_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ // Matches everything but "abc"
+ new Object[]{5L}
+ )
+ );
+ }
+ }
+
+ @Test
+ public void testCoalesceColumns() throws Exception
+ {
+ // Doesn't conform to the SQL standard, but it's how we do it.
+ // This example is used in the sql.md doc.
+
+ testQuery(
+ "SELECT COALESCE(dim2, dim1), COUNT(*) FROM druid.foo GROUP BY COALESCE(dim2, dim1)\n",
+ ImmutableList.of(
+ GroupByQuery.builder()
+ .setDataSource(CalciteTests.DATASOURCE1)
+ .setInterval(QSS(Filtration.eternity()))
+ .setGranularity(Granularities.ALL)
+ .setVirtualColumns(
+ EXPRESSION_VIRTUAL_COLUMN(
+ "d0:v",
+ "case_searched((\"dim2\" != ''),\"dim2\",\"dim1\")",
+ ValueType.STRING
+ )
+ )
+ .setDimensions(DIMS(new DefaultDimensionSpec("d0:v", "d0", ValueType.STRING)))
+ .setAggregatorSpecs(AGGS(new CountAggregatorFactory("a0")))
+ .setContext(QUERY_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{"10.1", 1L},
+ new Object[]{"2", 1L},
+ new Object[]{"a", 2L},
+ new Object[]{"abc", 2L}
+ )
+ );
+ }
+
+ @Test
+ public void testColumnIsNull() throws Exception
+ {
+ // Doesn't conform to the SQL standard, but it's how we do it.
+ // This example is used in the sql.md doc.
+
+ testQuery(
+ "SELECT COUNT(*) FROM druid.foo WHERE dim2 IS NULL\n",
+ ImmutableList.of(
+ Druids.newTimeseriesQueryBuilder()
+ .dataSource(CalciteTests.DATASOURCE1)
+ .intervals(QSS(Filtration.eternity()))
+ .granularity(Granularities.ALL)
+ .filters(SELECTOR("dim2", null, null))
+ .aggregators(AGGS(new CountAggregatorFactory("a0")))
+ .context(TIMESERIES_CONTEXT_DEFAULT)
+ .build()
+ ),
+ ImmutableList.of(
+ new Object[]{3L}
+ )
+ );
+ }
+
@Test
public void testUnplannableQueries() throws Exception
{
@@ -748,15 +1123,9 @@ public void testUnplannableQueries() throws Exception
// It's also here so when we do support these features, we can have "real" tests for these queries.
final List queries = ImmutableList.of(
- "SELECT (dim1 || ' ' || dim2) AS cc, COUNT(*) FROM druid.foo GROUP BY dim1 || ' ' || dim2", // Concat two dims
"SELECT dim1 FROM druid.foo ORDER BY dim1", // SELECT query with order by
"SELECT TRIM(dim1) FROM druid.foo", // TRIM function
- "SELECT COUNT(*) FROM druid.foo WHERE dim1 = dim2", // Filter on two columns equaling each other
- "SELECT COUNT(*) FROM druid.foo WHERE CHARACTER_LENGTH(dim1) = CHARACTER_LENGTH(dim2)", // Similar to above
- "SELECT CHARACTER_LENGTH(dim1) + 1 FROM druid.foo GROUP BY CHARACTER_LENGTH(dim1) + 1", // Group by math
"SELECT COUNT(*) FROM druid.foo x, druid.foo y", // Self-join
- "SELECT SUBSTRING(dim1, 2) FROM druid.foo GROUP BY dim1", // Project a dimension from GROUP BY
- "SELECT dim1 FROM druid.foo GROUP BY dim1 ORDER BY SUBSTRING(dim1, 2)", // ORDER BY projection
"SELECT DISTINCT dim2 FROM druid.foo ORDER BY dim2 LIMIT 2 OFFSET 5" // DISTINCT with OFFSET
);
@@ -790,7 +1159,7 @@ private void assertQueryIsUnplannable(final PlannerConfig plannerConfig, final S
{
Exception e = null;
try {
- testQuery(plannerConfig, sql, ImmutableList.of(), ImmutableList.