diff --git a/docs/source/user-guide/latest/datatypes.md b/docs/source/user-guide/latest/datatypes.md index 80465121da..0d90b80f29 100644 --- a/docs/source/user-guide/latest/datatypes.md +++ b/docs/source/user-guide/latest/datatypes.md @@ -85,8 +85,8 @@ Interval types fall back to Spark today. Native acceleration is tracked by | Type | Status | Notes | | ----------------------- | ------ | ----------------- | -| `YearMonthIntervalType` | 🔜 | Tracked by #4540. | -| `DayTimeIntervalType` | 🔜 | Tracked by #4540. | +| `YearMonthIntervalType` | ✅ | | +| `DayTimeIntervalType` | ✅ | | | `CalendarIntervalType` | 🔜 | Tracked by #4540. | ## Complex diff --git a/docs/source/user-guide/latest/expressions.md b/docs/source/user-guide/latest/expressions.md index 4ad4d9afe3..4a2b9bdd42 100644 --- a/docs/source/user-guide/latest/expressions.md +++ b/docs/source/user-guide/latest/expressions.md @@ -398,7 +398,7 @@ expression-level). The `outer` variants are wired but marked `Incompatible`; the | Function | Status | Notes | | --- | --- | --- | | `%` | ✅ | | -| `*` | ✅ | Interval multiplication falls back | +| `*` | ✅ | YearMonth interval multiplication routes through the JVM codegen dispatcher; DayTime/Calendar interval forms fall back | | `+` | ✅ | | | `-` | ✅ | | | `/` | ✅ | | diff --git a/native/core/src/execution/planner.rs b/native/core/src/execution/planner.rs index 7ed2b3331c..c77c1a29fb 100644 --- a/native/core/src/execution/planner.rs +++ b/native/core/src/execution/planner.rs @@ -35,7 +35,9 @@ use crate::execution::{ }; use crate::jvm_bridge::{jni_call, JVMClasses}; use arrow::compute::CastOptions; -use arrow::datatypes::{DataType, Field, FieldRef, Schema, TimeUnit, DECIMAL128_MAX_PRECISION}; +use arrow::datatypes::{ + DataType, Field, FieldRef, IntervalUnit, Schema, TimeUnit, DECIMAL128_MAX_PRECISION, +}; use arrow::ffi_stream::FFI_ArrowArrayStream; use datafusion::functions_aggregate::bit_and_or_xor::{bit_and_udaf, bit_or_udaf, bit_xor_udaf}; use datafusion::functions_aggregate::count::count_udaf; @@ -101,8 +103,8 @@ use datafusion::physical_expr::LexOrdering; use crate::parquet::parquet_exec::init_datasource_exec; use arrow::array::{ new_empty_array, Array, ArrayRef, BinaryBuilder, BooleanArray, Date32Array, Decimal128Array, - Float32Array, Float64Array, Int16Array, Int32Array, Int64Array, Int8Array, ListArray, - NullArray, StringBuilder, TimestampMicrosecondArray, + Float32Array, Float64Array, Int16Array, Int32Array, Int64Array, Int8Array, + IntervalYearMonthArray, ListArray, NullArray, StringBuilder, TimestampMicrosecondArray, }; use arrow::buffer::{BooleanBuffer, NullBuffer, OffsetBuffer}; use arrow::row::{OwnedRow, RowConverter, SortField}; @@ -362,6 +364,9 @@ impl PhysicalPlanner { DataType::Time64(TimeUnit::Nanosecond) => { ScalarValue::Time64Nanosecond(None) } + DataType::Interval(IntervalUnit::YearMonth) => { + ScalarValue::IntervalYearMonth(None) + } dt => { return Err(GeneralError(format!("{dt:?} is not supported in Comet"))) } @@ -374,9 +379,12 @@ impl PhysicalPlanner { Value::IntVal(value) => match data_type { DataType::Int32 => ScalarValue::Int32(Some(*value)), DataType::Date32 => ScalarValue::Date32(Some(*value)), + DataType::Interval(IntervalUnit::YearMonth) => { + ScalarValue::IntervalYearMonth(Some(*value)) + } dt => { return Err(GeneralError(format!( - "Expected either 'Int32' or 'Date32' for IntVal, but found {dt:?}" + "Expected either 'Int32', 'Date32', or 'Interval(YearMonth)' for IntVal, but found {dt:?}" ))) } }, @@ -3949,6 +3957,10 @@ fn literal_to_array_ref( list_literal.int_values.into(), Some(nulls.clone().into()), ))), + DataType::Interval(IntervalUnit::YearMonth) => Ok(Arc::new(IntervalYearMonthArray::new( + list_literal.int_values.into(), + Some(nulls.clone().into()), + ))), DataType::Timestamp(TimeUnit::Microsecond, None) => { Ok(Arc::new(TimestampMicrosecondArray::new( list_literal.long_values.into(), diff --git a/spark/src/main/scala/org/apache/comet/codegen/CometBatchKernelCodegenInput.scala b/spark/src/main/scala/org/apache/comet/codegen/CometBatchKernelCodegenInput.scala index 09bfc52bd4..d9adf67d51 100644 --- a/spark/src/main/scala/org/apache/comet/codegen/CometBatchKernelCodegenInput.scala +++ b/spark/src/main/scala/org/apache/comet/codegen/CometBatchKernelCodegenInput.scala @@ -57,6 +57,7 @@ private[codegen] object CometBatchKernelCodegenInput { classOf[TinyIntVector], classOf[SmallIntVector], classOf[IntVector], + classOf[IntervalYearVector], classOf[BigIntVector], classOf[Float4Vector], classOf[Float8Vector], @@ -127,7 +128,9 @@ private[codegen] object CometBatchKernelCodegenInput { } val intCases = withOrd.collect { case (ArrowColumnSpec(cls, _), ord) - if cls == classOf[IntVector] || cls == classOf[DateDayVector] => + if cls == classOf[IntVector] || + cls == classOf[DateDayVector] || + cls == classOf[IntervalYearVector] => s" case $ord: return this.col$ord.getInt(this.rowIdx);" } val longCases = withOrd.collect { @@ -590,7 +593,7 @@ private[codegen] object CometBatchKernelCodegenInput { case BooleanType => s"getBoolean($idx)" case ByteType => s"getByte($idx)" case ShortType => s"getShort($idx)" - case IntegerType | DateType => s"getInt($idx)" + case IntegerType | DateType | _: YearMonthIntervalType => s"getInt($idx)" case LongType | TimestampType | TimestampNTZType => s"getLong($idx)" case FloatType => s"getFloat($idx)" case DoubleType => s"getDouble($idx)" @@ -687,7 +690,7 @@ private[codegen] object CometBatchKernelCodegenInput { | public short getShort(int i) { | return $childField.getShort(startIndex + i); | }""".stripMargin - case IntegerType | DateType => + case IntegerType | DateType | _: YearMonthIntervalType => s""" @Override | public int getInt(int i) { | return $childField.getInt(startIndex + i); @@ -843,7 +846,7 @@ private[codegen] object CometBatchKernelCodegenInput { s" case $fi: return ${path}_f$fi.getByte(this.rowIdx);" case ShortType => s" case $fi: return ${path}_f$fi.getShort(this.rowIdx);" - case IntegerType | DateType => + case IntegerType | DateType | _: YearMonthIntervalType => s" case $fi: return ${path}_f$fi.getInt(this.rowIdx);" case LongType | TimestampType | TimestampNTZType => s" case $fi: return ${path}_f$fi.getLong(this.rowIdx);" @@ -891,8 +894,11 @@ private[codegen] object CometBatchKernelCodegenInput { fieldReadScalar(fi, ShortType, f.nullable) } val intCases = scalarOrd.collect { - case (f, fi) if f.sparkType == IntegerType || f.sparkType == DateType => - fieldReadScalar(fi, IntegerType, f.nullable) + case (f, fi) + if f.sparkType == IntegerType || + f.sparkType == DateType || + f.sparkType.isInstanceOf[YearMonthIntervalType] => + fieldReadScalar(fi, f.sparkType, f.nullable) } val longCases = scalarOrd.collect { case (f, fi) diff --git a/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala b/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala index 5eee0c5cad..185acc6de5 100644 --- a/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala +++ b/spark/src/main/scala/org/apache/comet/serde/QueryPlanSerde.scala @@ -303,6 +303,7 @@ object QueryPlanSerde extends Logging with CometExprShim with CometTypeShim { classOf[MicrosToTimestamp] -> CometMicrosToTimestamp, classOf[MillisToTimestamp] -> CometMillisToTimestamp, classOf[MonthsBetween] -> CometMonthsBetween, + classOf[MultiplyYMInterval] -> CometMultiplyYMInterval, classOf[Minute] -> CometMinute, classOf[NextDay] -> CometNextDay, classOf[PreciseTimestampConversion] -> CometPreciseTimestampConversion, diff --git a/spark/src/main/scala/org/apache/comet/serde/datetime.scala b/spark/src/main/scala/org/apache/comet/serde/datetime.scala index 9f2533d85f..3dc992a21a 100644 --- a/spark/src/main/scala/org/apache/comet/serde/datetime.scala +++ b/spark/src/main/scala/org/apache/comet/serde/datetime.scala @@ -21,7 +21,7 @@ package org.apache.comet.serde import java.util.Locale -import org.apache.spark.sql.catalyst.expressions.{AddMonths, Attribute, ConvertTimezone, DateAdd, DateDiff, DateFormatClass, DateFromUnixDate, DateSub, DayOfMonth, DayOfWeek, DayOfYear, Days, Expression, FromUTCTimestamp, GetDateField, GetTimestamp, Hour, Hours, LastDay, Literal, MakeDate, MakeDTInterval, MakeTimestamp, MakeYMInterval, MicrosToTimestamp, MillisToTimestamp, Minute, Month, MonthsBetween, NextDay, PreciseTimestampConversion, Quarter, Second, SecondsToTimestamp, ToUnixTimestamp, ToUTCTimestamp, TruncDate, TruncTimestamp, UnixDate, UnixMicros, UnixMillis, UnixSeconds, UnixTimestamp, WeekDay, WeekOfYear, Year} +import org.apache.spark.sql.catalyst.expressions.{AddMonths, Attribute, ConvertTimezone, DateAdd, DateDiff, DateFormatClass, DateFromUnixDate, DateSub, DayOfMonth, DayOfWeek, DayOfYear, Days, Expression, FromUTCTimestamp, GetDateField, GetTimestamp, Hour, Hours, LastDay, Literal, MakeDate, MakeDTInterval, MakeTimestamp, MakeYMInterval, MicrosToTimestamp, MillisToTimestamp, Minute, Month, MonthsBetween, MultiplyYMInterval, NextDay, PreciseTimestampConversion, Quarter, Second, SecondsToTimestamp, ToUnixTimestamp, ToUTCTimestamp, TruncDate, TruncTimestamp, UnixDate, UnixMicros, UnixMillis, UnixSeconds, UnixTimestamp, WeekDay, WeekOfYear, Year} import org.apache.spark.sql.internal.SQLConf import org.apache.spark.sql.types.{DataType, DateType, DoubleType, FloatType, IntegerType, LongType, StringType, TimestampNTZType, TimestampType} import org.apache.spark.unsafe.types.UTF8String @@ -952,6 +952,8 @@ object CometGetTimestamp extends CometCodegenDispatch[GetTimestamp] object CometMakeYMInterval extends CometCodegenDispatch[MakeYMInterval] +object CometMultiplyYMInterval extends CometCodegenDispatch[MultiplyYMInterval] + object CometMakeDTInterval extends CometCodegenDispatch[MakeDTInterval] /** diff --git a/spark/src/main/scala/org/apache/comet/serde/literals.scala b/spark/src/main/scala/org/apache/comet/serde/literals.scala index 4f2a5dfa5e..8f9cbf04f3 100644 --- a/spark/src/main/scala/org/apache/comet/serde/literals.scala +++ b/spark/src/main/scala/org/apache/comet/serde/literals.scala @@ -24,7 +24,7 @@ import java.lang import org.apache.spark.internal.Logging import org.apache.spark.sql.catalyst.expressions.{Attribute, Literal} import org.apache.spark.sql.catalyst.util.ArrayData -import org.apache.spark.sql.types.{ArrayType, BinaryType, BooleanType, ByteType, DateType, Decimal, DecimalType, DoubleType, FloatType, IntegerType, LongType, NullType, ShortType, StringType, TimestampNTZType, TimestampType} +import org.apache.spark.sql.types.{ArrayType, BinaryType, BooleanType, ByteType, DateType, Decimal, DecimalType, DoubleType, FloatType, IntegerType, LongType, NullType, ShortType, StringType, TimestampNTZType, TimestampType, YearMonthIntervalType} import org.apache.spark.unsafe.types.UTF8String import com.google.protobuf.ByteString @@ -41,22 +41,25 @@ object CometLiteral extends CometExpressionSerde[Literal] with Logging { "Not all data types are supported for literal values") override def getSupportLevel(expr: Literal): SupportLevel = { - + val dataType = expr.dataType if (supportedDataType( - expr.dataType, + dataType, allowComplex = expr.value == null || // Nested literal support for native reader // can be tracked https://github.com/apache/datafusion-comet/issues/1937 - (expr.dataType + (dataType .isInstanceOf[ArrayType] && (!isComplexType( - expr.dataType.asInstanceOf[ArrayType].elementType) || expr.dataType + dataType.asInstanceOf[ArrayType].elementType) || dataType .asInstanceOf[ArrayType] .elementType .isInstanceOf[ArrayType])))) { Compatible(None) } else { - Unsupported(Some(s"Unsupported data type ${expr.dataType}")) + dataType match { + case _: YearMonthIntervalType => Compatible(None) + case _ => Unsupported(Some(s"Unsupported data type $dataType")) + } } } @@ -77,7 +80,8 @@ object CometLiteral extends CometExpressionSerde[Literal] with Logging { case _: BooleanType => exprBuilder.setBoolVal(value.asInstanceOf[Boolean]) case _: ByteType => exprBuilder.setByteVal(value.asInstanceOf[Byte]) case _: ShortType => exprBuilder.setShortVal(value.asInstanceOf[Short]) - case _: IntegerType | _: DateType => exprBuilder.setIntVal(value.asInstanceOf[Int]) + case _: IntegerType | _: DateType | _: YearMonthIntervalType => + exprBuilder.setIntVal(value.asInstanceOf[Int]) case _: LongType | _: TimestampType | _: TimestampNTZType => exprBuilder.setLongVal(value.asInstanceOf[Long]) case dt if isTimeType(dt) => @@ -150,7 +154,7 @@ object CometLiteral extends CometExpressionSerde[Literal] with Logging { else null.asInstanceOf[Integer]) listLiteralBuilder.addNullMask(casted != null) }) - case IntegerType | DateType => + case IntegerType | DateType | _: YearMonthIntervalType => array.foreach(v => { val casted = v.asInstanceOf[Integer] listLiteralBuilder.addIntValues(casted) diff --git a/spark/src/main/scala/org/apache/comet/udf/codegen/CometScalaUDFCodegen.scala b/spark/src/main/scala/org/apache/comet/udf/codegen/CometScalaUDFCodegen.scala index f575dd5b53..ad7d3037ee 100644 --- a/spark/src/main/scala/org/apache/comet/udf/codegen/CometScalaUDFCodegen.scala +++ b/spark/src/main/scala/org/apache/comet/udf/codegen/CometScalaUDFCodegen.scala @@ -217,10 +217,10 @@ class CometScalaUDFCodegen extends CometUDF with Logging { child = specFor(childVec)) } StructColumnSpec(nullable = true, fieldSpecs) - case _: BitVector | _: TinyIntVector | _: SmallIntVector | _: IntVector | _: BigIntVector | - _: Float4Vector | _: Float8Vector | _: DecimalVector | _: VarCharVector | - _: VarBinaryVector | _: DateDayVector | _: TimeStampMicroVector | - _: TimeStampMicroTZVector => + case _: BitVector | _: TinyIntVector | _: SmallIntVector | _: IntVector | + _: IntervalYearVector | _: BigIntVector | _: Float4Vector | _: Float8Vector | + _: DecimalVector | _: VarCharVector | _: VarBinaryVector | _: DateDayVector | + _: TimeStampMicroVector | _: TimeStampMicroTZVector => ScalarColumnSpec(v.getClass.asInstanceOf[Class[_ <: ValueVector]], nullable = true) case other => throw new UnsupportedOperationException( diff --git a/spark/src/test/resources/sql-tests/expressions/datetime/multiply_ym_interval.sql b/spark/src/test/resources/sql-tests/expressions/datetime/multiply_ym_interval.sql new file mode 100644 index 0000000000..c64a1a786b --- /dev/null +++ b/spark/src/test/resources/sql-tests/expressions/datetime/multiply_ym_interval.sql @@ -0,0 +1,64 @@ +-- 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. + +-- Routes multiply_ym_interval through the codegen dispatcher; produces YearMonthIntervalType. +-- Config: spark.comet.exec.scalaUDF.codegen.enabled=true + +statement +CREATE TABLE test_multiply_ym_interval(y int, m int, i int, l long, f float, d double, dec decimal(10,2)) USING parquet + +statement +INSERT INTO test_multiply_ym_interval VALUES + (1, 2, 2, CAST(3 AS BIGINT), CAST(1.5 AS FLOAT), CAST(2.5 AS DOUBLE), CAST(2.50 AS DECIMAL(10, 2))), + (-1, 1, -2, CAST(-3 AS BIGINT), CAST(-1.5 AS FLOAT), CAST(-2.5 AS DOUBLE), CAST(-2.50 AS DECIMAL(10, 2))), + (0, 6, 0, CAST(0 AS BIGINT), CAST(0.5 AS FLOAT), CAST(0.5 AS DOUBLE), CAST(0.50 AS DECIMAL(10, 2))), + (2, -6, NULL, NULL, NULL, NULL, NULL) + +query +SELECT + make_ym_interval(y, m) * i, + make_ym_interval(y, m) * l, + make_ym_interval(y, m) * f, + make_ym_interval(y, m) * d, + make_ym_interval(y, m) * dec +FROM test_multiply_ym_interval + +-- literal interval input +query +SELECT INTERVAL '1-2' YEAR TO MONTH * i FROM test_multiply_ym_interval + +-- numeric on the left is normalized by Spark to multiply_ym_interval. +query +SELECT i * make_ym_interval(y, m), 2 * INTERVAL '1-2' YEAR TO MONTH +FROM test_multiply_ym_interval + +-- literal multipliers, including half-up rounding for fractional months. +query +SELECT + make_ym_interval(1, 2) * 2, + make_ym_interval(1, 2) * 1.5D, + make_ym_interval(1, 2) * CAST(1.50 AS DECIMAL(10, 2)), + make_ym_interval(-1, 1) * 1.5D + +-- null interval input +query +SELECT make_ym_interval(NULL, m) * 2 FROM test_multiply_ym_interval + +-- 178956970 years and 7 months is Int.MaxValue months. Multiplication overflows regardless of +-- ANSI mode; the lowercase pattern matches Spark 3.x and 4.x error messages. +query expect_error(overflow) +SELECT make_ym_interval(178956970, 7) * 2