All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.druid.sql.calcite.planner.Calcites Maven / Gradle / Ivy

There is a newer version: 31.0.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.druid.sql.calcite.planner;

import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Chars;
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.rex.RexUtil;
import org.apache.calcite.sql.SqlCollation;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlOperatorBinding;
import org.apache.calcite.sql.type.SqlReturnTypeInference;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.util.ConversionUtil;
import org.apache.calcite.util.DateString;
import org.apache.calcite.util.TimeString;
import org.apache.calcite.util.TimestampString;
import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.StringUtils;
import org.apache.druid.math.expr.ExpressionProcessing;
import org.apache.druid.math.expr.ExpressionProcessingConfig;
import org.apache.druid.query.ordering.StringComparator;
import org.apache.druid.query.ordering.StringComparators;
import org.apache.druid.segment.column.ColumnType;
import org.apache.druid.segment.column.ValueType;
import org.apache.druid.sql.calcite.table.RowSignatures;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Days;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.DateTimeFormatterBuilder;
import org.joda.time.format.ISODateTimeFormat;

import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.sql.Date;
import java.sql.JDBCType;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.NavigableSet;
import java.util.TreeSet;
import java.util.regex.Pattern;

/**
 * Utility functions for Calcite.
 * 

* See also the file {@code saffron.properties} which holds the * character set system properties formerly set in this file. */ public class Calcites { private static final DateTimes.UtcFormatter CALCITE_DATE_PARSER = DateTimes.wrapFormatter(ISODateTimeFormat.dateParser()); private static final DateTimes.UtcFormatter CALCITE_TIMESTAMP_PARSER = DateTimes.wrapFormatter( new DateTimeFormatterBuilder() .append(ISODateTimeFormat.dateParser()) .appendLiteral(' ') .append(ISODateTimeFormat.timeParser()) .toFormatter() ); private static final DateTimeFormatter CALCITE_TIME_PRINTER = DateTimeFormat.forPattern("HH:mm:ss.S"); private static final DateTimeFormatter CALCITE_DATE_PRINTER = DateTimeFormat.forPattern("yyyy-MM-dd"); private static final DateTimeFormatter CALCITE_TIMESTAMP_PRINTER = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS"); private static final Charset DEFAULT_CHARSET = Charset.forName(ConversionUtil.NATIVE_UTF16_CHARSET_NAME); private static final Pattern TRAILING_ZEROS = Pattern.compile("\\.?0+$"); public static final SqlReturnTypeInference ARG0_NULLABLE_ARRAY_RETURN_TYPE_INFERENCE = new Arg0NullableArrayTypeInference(); public static final SqlReturnTypeInference ARG1_NULLABLE_ARRAY_RETURN_TYPE_INFERENCE = new Arg1NullableArrayTypeInference(); public static SqlReturnTypeInference complexReturnTypeWithNullability(ColumnType columnType, boolean nullable) { return opBinding -> RowSignatures.makeComplexType( opBinding.getTypeFactory(), columnType, nullable ); } private Calcites() { // No instantiation. } public static Charset defaultCharset() { return DEFAULT_CHARSET; } public static String escapeStringLiteral(final String s) { Preconditions.checkNotNull(s); boolean isPlainAscii = true; final StringBuilder builder = new StringBuilder("'"); for (int i = 0; i < s.length(); i++) { final char c = s.charAt(i); if (Character.isLetterOrDigit(c) || c == ' ') { builder.append(c); if (c > 127) { isPlainAscii = false; } } else { builder.append("\\").append(BaseEncoding.base16().encode(Chars.toByteArray(c))); isPlainAscii = false; } } builder.append("'"); return isPlainAscii ? builder.toString() : "U&" + builder; } /** * Returns whether an expression is a literal. Like {@link RexUtil#isLiteral(RexNode, boolean)} but can accept * arrays too. * * @param rexNode expression * @param allowCast allow calls to CAST if its argument is literal * @param allowArray allow calls to ARRAY if its arguments are literal */ public static boolean isLiteral(RexNode rexNode, boolean allowCast, boolean allowArray) { if (RexUtil.isLiteral(rexNode, allowCast)) { return true; } else if (allowArray && rexNode.isA(SqlKind.ARRAY_VALUE_CONSTRUCTOR)) { for (final RexNode element : ((RexCall) rexNode).getOperands()) { if (!RexUtil.isLiteral(element, allowCast)) { return false; } } return true; } else { return false; } } /** * Convert {@link RelDataType} to the most appropriate {@link ColumnType}, or null if there is no {@link ColumnType} * that is appropriate for this {@link RelDataType}. * * Equivalent to {@link #getValueTypeForRelDataTypeFull(RelDataType)}, except this method returns * {@link ColumnType#STRING} when {@link ColumnType#isArray()} if * {@link ExpressionProcessingConfig#processArraysAsMultiValueStrings()} is set via the server property * {@code druid.expressions.allowArrayToStringCast}. * * @return type, or null if there is no matching type */ @Nullable public static ColumnType getColumnTypeForRelDataType(final RelDataType type) { ColumnType valueType = getValueTypeForRelDataTypeFull(type); // coerce array to multi value string if (ExpressionProcessing.processArraysAsMultiValueStrings() && valueType != null && valueType.isArray()) { return ColumnType.STRING; } return valueType; } /** * Convert {@link RelDataType} to the most appropriate {@link ColumnType}, or null if there is no {@link ColumnType} * that is appropriate for this {@link RelDataType}. * * Equivalent to {@link #getColumnTypeForRelDataType(RelDataType)}, but ignores * {@link ExpressionProcessingConfig#processArraysAsMultiValueStrings()} (and acts as if it is false). * * @return type, or null if there is no matching type */ @Nullable public static ColumnType getValueTypeForRelDataTypeFull(final RelDataType type) { final SqlTypeName sqlTypeName = type.getSqlTypeName(); if (SqlTypeName.FLOAT == sqlTypeName) { return ColumnType.FLOAT; } else if (isDoubleType(sqlTypeName)) { return ColumnType.DOUBLE; } else if (isLongType(sqlTypeName)) { return ColumnType.LONG; } else if (isStringType(sqlTypeName)) { return ColumnType.STRING; } else if (SqlTypeName.OTHER == sqlTypeName) { if (type instanceof RowSignatures.ComplexSqlType) { return ColumnType.ofComplex(((RowSignatures.ComplexSqlType) type).getComplexTypeName()); } return ColumnType.UNKNOWN_COMPLEX; } else if (sqlTypeName == SqlTypeName.ARRAY) { ColumnType elementType = getValueTypeForRelDataTypeFull(type.getComponentType()); if (elementType != null) { return ColumnType.ofArray(elementType); } return null; } else { return null; } } public static boolean isStringType(SqlTypeName sqlTypeName) { return SqlTypeName.CHAR_TYPES.contains(sqlTypeName) || SqlTypeName.INTERVAL_TYPES.contains(sqlTypeName); } public static boolean isDoubleType(SqlTypeName sqlTypeName) { return SqlTypeName.FRACTIONAL_TYPES.contains(sqlTypeName) || SqlTypeName.APPROX_TYPES.contains(sqlTypeName); } public static boolean isLongType(SqlTypeName sqlTypeName) { return SqlTypeName.TIMESTAMP == sqlTypeName || SqlTypeName.DATE == sqlTypeName || SqlTypeName.BOOLEAN == sqlTypeName || SqlTypeName.INT_TYPES.contains(sqlTypeName); } /** * Returns the natural StringComparator associated with the RelDataType */ public static StringComparator getStringComparatorForRelDataType(RelDataType dataType) { final ColumnType valueType = getColumnTypeForRelDataType(dataType); return getStringComparatorForValueType(valueType); } /** * Returns the natural StringComparator associated with the given ColumnType */ public static StringComparator getStringComparatorForValueType(ColumnType valueType) { if (valueType.isNumeric()) { return StringComparators.NUMERIC; } else if (valueType.is(ValueType.STRING)) { return StringComparators.LEXICOGRAPHIC; } else { return StringComparators.NATURAL; } } /** * Like RelDataTypeFactory.createSqlType, but creates types that align best with how Druid represents them. */ public static RelDataType createSqlType(final RelDataTypeFactory typeFactory, final SqlTypeName typeName) { return createSqlTypeWithNullability(typeFactory, typeName, false); } /** * Like RelDataTypeFactory.createSqlTypeWithNullability, but creates types that align best with how Druid * represents them. */ public static RelDataType createSqlTypeWithNullability( final RelDataTypeFactory typeFactory, final SqlTypeName typeName, final boolean nullable ) { final RelDataType dataType; switch (typeName) { case TIMESTAMP: case TIMESTAMP_WITH_LOCAL_TIME_ZONE: // Our timestamps are down to the millisecond (precision = 3). dataType = typeFactory.createSqlType(typeName, DruidTypeSystem.DEFAULT_TIMESTAMP_PRECISION); break; case CHAR: case VARCHAR: dataType = typeFactory.createTypeWithCharsetAndCollation( typeFactory.createSqlType(typeName), Calcites.defaultCharset(), SqlCollation.IMPLICIT ); break; default: dataType = typeFactory.createSqlType(typeName); } return typeFactory.createTypeWithNullability(dataType, nullable); } /** * Like RelDataTypeFactory.createSqlTypeWithNullability, but creates types that align best with how Druid * represents them. */ public static RelDataType createSqlArrayTypeWithNullability( final RelDataTypeFactory typeFactory, final SqlTypeName elementTypeName, final boolean nullable ) { final RelDataType dataType = typeFactory.createTypeWithNullability( typeFactory.createArrayType( createSqlTypeWithNullability(typeFactory, elementTypeName, nullable), -1 ), true ); return dataType; } /** * Calcite expects "TIMESTAMP" types to be an instant that has the expected local time fields if printed as UTC. * * @param dateTime joda timestamp * @param timeZone session time zone * * @return Calcite style millis */ public static long jodaToCalciteTimestamp(final DateTime dateTime, final DateTimeZone timeZone) { return dateTime.withZone(timeZone).withZoneRetainFields(DateTimeZone.UTC).getMillis(); } /** * Calcite expects "DATE" types to be number of days from the epoch to the UTC date matching the local time fields. * * @param dateTime joda timestamp * @param timeZone session time zone * * @return Calcite style date */ public static int jodaToCalciteDate(final DateTime dateTime, final DateTimeZone timeZone) { final DateTime date = dateTime.withZone(timeZone).dayOfMonth().roundFloorCopy(); return Days.daysBetween(DateTimes.EPOCH, date.withZoneRetainFields(DateTimeZone.UTC)).getDays(); } /** * Creates a Calcite TIMESTAMP literal from a Joda DateTime. * * @param dateTime joda timestamp * @param sessionTimeZone session time zone * * @return Calcite style Calendar, appropriate for literals */ public static RexLiteral jodaToCalciteTimestampLiteral( final RexBuilder rexBuilder, final DateTime dateTime, final DateTimeZone sessionTimeZone, final int precision ) { return rexBuilder.makeTimestampLiteral(jodaToCalciteTimestampString(dateTime, sessionTimeZone), precision); } /** * Calcite expects TIMESTAMP literals to be represented by TimestampStrings in the local time zone. * * @param dateTime joda timestamp * @param sessionTimeZone session time zone * * @return Calcite style Calendar, appropriate for literals */ public static TimestampString jodaToCalciteTimestampString( final DateTime dateTime, final DateTimeZone sessionTimeZone ) { // Calcite expects TIMESTAMP literals to be represented by TimestampStrings in the session time zone. // The TRAILING_ZEROS ... replaceAll is because Calcite doesn't like trailing zeroes in its fractional seconds part. final String timestampString = TRAILING_ZEROS .matcher(CALCITE_TIMESTAMP_PRINTER.print(dateTime.withZone(sessionTimeZone))) .replaceAll(""); return new TimestampString(timestampString); } /** * Calcite expects TIME literals to be represented by TimeStrings in the local time zone. * * @param dateTime joda timestamp * @param timeZone session time zone * * @return Calcite style Calendar, appropriate for literals */ public static TimeString jodaToCalciteTimeString(final DateTime dateTime, final DateTimeZone timeZone) { // The replaceAll is because Calcite doesn't like trailing zeroes in its fractional seconds part. String timeString = TRAILING_ZEROS .matcher(CALCITE_TIME_PRINTER.print(dateTime.withZone(timeZone))) .replaceAll(""); return new TimeString(timeString); } /** * Calcite expects DATE literals to be represented by DateStrings in the local time zone. * * @param dateTime joda timestamp * @param timeZone session time zone * * @return Calcite style Calendar, appropriate for literals */ public static DateString jodaToCalciteDateString(final DateTime dateTime, final DateTimeZone timeZone) { return new DateString(CALCITE_DATE_PRINTER.print(dateTime.withZone(timeZone))); } /** * 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 literal but got[%s]", literal.getKind()); } if (typeName == SqlTypeName.TIMESTAMP) { final TimestampString timestampString = (TimestampString) RexLiteral.value(literal); return CALCITE_TIMESTAMP_PARSER.parse(timestampString.toString()).withZoneRetainFields(timeZone); } else if (typeName == SqlTypeName.DATE) { final DateString dateString = (DateString) RexLiteral.value(literal); return CALCITE_DATE_PARSER.parse(dateString.toString()).withZoneRetainFields(timeZone); } else { throw new IAE("Expected TIMESTAMP or DATE but got[%s]", typeName); } } /** * The inverse of {@link #jodaToCalciteTimestamp(DateTime, DateTimeZone)}. * * @param timestamp Calcite style timestamp * @param timeZone session time zone * * @return joda timestamp, with time zone set to the session time zone */ public static DateTime calciteTimestampToJoda(final long timestamp, final DateTimeZone timeZone) { return new DateTime(timestamp, DateTimeZone.UTC).withZoneRetainFields(timeZone); } /** * The inverse of {@link #jodaToCalciteDate(DateTime, DateTimeZone)}. * * @param date Calcite style date * @param timeZone session time zone * * @return joda timestamp, with time zone set to the session time zone */ public static DateTime calciteDateToJoda(final int date, final DateTimeZone timeZone) { return DateTimes.EPOCH.plusDays(date).withZoneRetainFields(timeZone); } /** * Find a string that is either equal to "basePrefix", or basePrefix prepended by underscores, and where nothing in * "strings" starts with prefix plus a digit. */ public static String findUnusedPrefixForDigits(final String basePrefix, final Iterable strings) { final NavigableSet navigableStrings; if (strings instanceof NavigableSet) { navigableStrings = (NavigableSet) strings; } else { navigableStrings = new TreeSet<>(); Iterables.addAll(navigableStrings, strings); } String prefix = basePrefix; while (!isUnusedPrefix(prefix, navigableStrings)) { prefix = "_" + prefix; } return prefix; } private static boolean isUnusedPrefix(final String prefix, final NavigableSet strings) { // ":" is one character after "9" final NavigableSet subSet = strings.subSet(prefix + "0", true, prefix + ":", false); return subSet.isEmpty(); } public static String makePrefixedName(final String prefix, final String suffix) { return StringUtils.format("%s:%s", prefix, suffix); } public static Class sqlTypeNameJdbcToJavaClass(SqlTypeName typeName) { // reference: https://docs.oracle.com/javase/1.5.0/docs/guide/jdbc/getstart/mapping.html JDBCType jdbcType = JDBCType.valueOf(typeName.getJdbcOrdinal()); switch (jdbcType) { case CHAR: case VARCHAR: case LONGVARCHAR: return String.class; case NUMERIC: case DECIMAL: return BigDecimal.class; case BIT: return Boolean.class; case TINYINT: return Byte.class; case SMALLINT: return Short.class; case INTEGER: return Integer.class; case BIGINT: return Long.class; case REAL: return Float.class; case FLOAT: case DOUBLE: return Double.class; case BINARY: case VARBINARY: return Byte[].class; case DATE: return Date.class; case TIME: return Time.class; case TIMESTAMP: return Timestamp.class; default: return Object.class; } } public static class Arg0NullableArrayTypeInference implements SqlReturnTypeInference { @Override public RelDataType inferReturnType(SqlOperatorBinding opBinding) { RelDataType type = opBinding.getOperandType(0); if (SqlTypeUtil.isArray(type)) { return type; } return Calcites.createSqlArrayTypeWithNullability( opBinding.getTypeFactory(), type.getSqlTypeName(), true ); } } public static class Arg1NullableArrayTypeInference implements SqlReturnTypeInference { @Override public RelDataType inferReturnType(SqlOperatorBinding opBinding) { RelDataType type = opBinding.getOperandType(1); if (SqlTypeUtil.isArray(type)) { return type; } return Calcites.createSqlArrayTypeWithNullability( opBinding.getTypeFactory(), type.getSqlTypeName(), true ); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy