This is an automated email from the ASF dual-hosted git repository.
rui-mo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git
The following commit(s) were added to refs/heads/main by this push:
new e63d43ef4f [GLUTEN-12538][CORE] Preserve TIMESTAMP_NTZ literals
(#13041)
e63d43ef4f is described below
commit e63d43ef4f677c860b2a3c27f768572d46725a8c
Author: Felipe Pessoto <[email protected]>
AuthorDate: Thu Nov 5 14:22:30 2026 -0800
[GLUTEN-12538][CORE] Preserve TIMESTAMP_NTZ literals (#13041)
---
cpp/velox/substrait/SubstraitParser.cc | 2 +-
cpp/velox/substrait/VeloxToSubstraitExpr.cc | 119 ++++++------
cpp/velox/substrait/VeloxToSubstraitExpr.h | 5 +-
cpp/velox/tests/SubstraitVeloxExprConverterTest.cc | 95 ++++++++++
cpp/velox/tests/VeloxSubstraitRoundTripTest.cc | 106 +++++++++++
.../substrait/expression/ExpressionBuilder.java | 3 +
.../substrait/expression/StructLiteralNode.java | 2 +-
.../apache/gluten/expression/ConverterUtils.scala | 2 +
.../extension/columnar/validator/Validators.scala | 27 ++-
.../gluten/utils/velox/VeloxTestSettings.scala | 1 +
.../spark/sql/GlutenTimestampNtzLiteralSuite.scala | 137 ++++++++++++++
.../gluten/utils/velox/VeloxTestSettings.scala | 1 +
.../spark/sql/GlutenTimestampNtzLiteralSuite.scala | 1 +
.../GlutenTimestampNtzValidatorSuite.scala | 201 +++++++++++++++++++++
14 files changed, 638 insertions(+), 64 deletions(-)
diff --git a/cpp/velox/substrait/SubstraitParser.cc
b/cpp/velox/substrait/SubstraitParser.cc
index 3333810e01..031840fbf1 100644
--- a/cpp/velox/substrait/SubstraitParser.cc
+++ b/cpp/velox/substrait/SubstraitParser.cc
@@ -426,7 +426,7 @@ bool SubstraitParser::getLiteralValue(const
::substrait::Expression::Literal& li
template <>
Timestamp SubstraitParser::getLiteralValue(const
::substrait::Expression::Literal& literal) {
- return Timestamp::fromMicros(literal.timestamp_tz());
+ return Timestamp::fromMicros(literal.has_timestamp() ? literal.timestamp() :
literal.timestamp_tz());
}
template <>
diff --git a/cpp/velox/substrait/VeloxToSubstraitExpr.cc
b/cpp/velox/substrait/VeloxToSubstraitExpr.cc
index f9cd1d2880..d335316331 100644
--- a/cpp/velox/substrait/VeloxToSubstraitExpr.cc
+++ b/cpp/velox/substrait/VeloxToSubstraitExpr.cc
@@ -23,10 +23,21 @@ namespace gluten {
namespace {
const ::substrait::Expression_Literal& toSubstraitNullLiteral(
google::protobuf::Arena& arena,
- const velox::TypeKind& typeKind) {
+ const velox::TypePtr& type) {
::substrait::Expression_Literal* substraitField =
google::protobuf::Arena::CreateMessage<::substrait::Expression_Literal>(&arena);
- switch (typeKind) {
+ substraitField->set_nullable(true);
+ auto nestedType = type;
+ while (nestedType->isArray()) {
+ nestedType = nestedType->asArray().elementType();
+ }
+ if (nestedType->kind() == TypeKind::TIMESTAMP) {
+ VeloxToSubstraitTypeConvertor typeConvertor;
+
substraitField->mutable_null()->MergeFrom(typeConvertor.toSubstraitType(arena,
type));
+ return *substraitField;
+ }
+ // Preserve physical-kind encoding for non-timestamp nulls.
+ switch (type->kind()) {
case velox::TypeKind::BOOLEAN: {
::substrait::Type_Boolean* nullValue =
google::protobuf::Arena::CreateMessage<::substrait::Type_Boolean>(&arena);
nullValue->set_nullability(::substrait::Type_Nullability_NULLABILITY_NULLABLE);
@@ -92,16 +103,32 @@ const ::substrait::Expression_Literal&
toSubstraitNullLiteral(
break;
}
default: {
- VELOX_UNSUPPORTED("Unsupported type '{}'",
TypeKindName::toName(typeKind));
+ VELOX_UNSUPPORTED("Unsupported type '{}'",
TypeKindName::toName(type->kind()));
}
}
- substraitField->set_nullable(true);
return *substraitField;
}
+const ::substrait::Expression_Literal&
+toSubstraitTimestampLiteral(google::protobuf::Arena& arena, const Timestamp&
value, const TypePtr& type) {
+ auto* literal =
google::protobuf::Arena::CreateMessage<::substrait::Expression_Literal>(&arena);
+ const auto micros = value.toMicros();
+ if (type->equivalent(*TIMESTAMP_UTC())) {
+ literal->set_timestamp(micros);
+ } else {
+ literal->set_timestamp_tz(micros);
+ }
+ literal->set_nullable(false);
+ return *literal;
+}
+
const ::substrait::Expression_Literal& toSubstraitNotNullLiteral(
google::protobuf::Arena& arena,
- const velox::variant& variantValue) {
+ const velox::variant& variantValue,
+ const TypePtr& logicalType) {
+ if (variantValue.kind() == TypeKind::TIMESTAMP) {
+ return toSubstraitTimestampLiteral(arena,
variantValue.value<TypeKind::TIMESTAMP>(), logicalType);
+ }
::substrait::Expression_Literal* literalExpr =
google::protobuf::Arena::CreateMessage<::substrait::Expression_Literal>(&arena);
switch (variantValue.kind()) {
@@ -133,12 +160,6 @@ const ::substrait::Expression_Literal&
toSubstraitNotNullLiteral(
literalExpr->set_fp64(variantValue.value<TypeKind::DOUBLE>());
break;
}
- case velox::TypeKind::TIMESTAMP: {
- auto vTimeStamp = variantValue.value<TypeKind::TIMESTAMP>();
- auto micros = vTimeStamp.getSeconds() * 1000000 + vTimeStamp.getNanos()
/ 1000;
- literalExpr->set_timestamp_tz(micros);
- break;
- }
case velox::TypeKind::VARCHAR: {
auto vCharValue = variantValue.value<StringView>();
auto* sVarChar =
google::protobuf::Arena::CreateMessage<::substrait::Expression_Literal::VarChar>(&arena);
@@ -243,18 +264,6 @@ const ::substrait::Expression_Literal&
toSubstraitNotNullLiteral<TypeKind::DOUBL
return *literalExpr;
}
-template <>
-const ::substrait::Expression_Literal&
toSubstraitNotNullLiteral<TypeKind::TIMESTAMP>(
- google::protobuf::Arena& arena,
- const Timestamp& value) {
- ::substrait::Expression_Literal* literalExpr =
-
google::protobuf::Arena::CreateMessage<::substrait::Expression_Literal>(&arena);
- auto micros = value.getSeconds() * 1000000 + value.getNanos() / 1000;
- literalExpr->set_timestamp_tz(micros);
- literalExpr->set_nullable(false);
- return *literalExpr;
-}
-
template <>
const ::substrait::Expression_Literal&
toSubstraitNotNullLiteral<TypeKind::VARCHAR>(
google::protobuf::Arena& arena,
@@ -269,6 +278,21 @@ const ::substrait::Expression_Literal&
toSubstraitNotNullLiteral<TypeKind::VARCH
return *literalExpr;
}
+template <TypeKind kind>
+const ::substrait::Expression_Literal&
+scalarVectorToLiteral(google::protobuf::Arena& arena, const VectorPtr& vector,
vector_size_t row) {
+ if (vector->isNullAt(row)) {
+ return toSubstraitNullLiteral(arena, vector->type());
+ }
+ using T = typename TypeTraits<kind>::NativeType;
+ const auto value = vector->as<SimpleVector<T>>()->valueAt(row);
+ if constexpr (kind == TypeKind::TIMESTAMP) {
+ return toSubstraitTimestampLiteral(arena, value, vector->type());
+ } else {
+ return toSubstraitNotNullLiteral<kind>(arena, value);
+ }
+}
+
template <TypeKind Kind>
void arrayVectorToLiteral(
google::protobuf::Arena& arena,
@@ -276,15 +300,9 @@ void arrayVectorToLiteral(
::substrait::Expression_Literal_List* listLiteral,
vector_size_t offset,
vector_size_t size) {
- using T = typename TypeTraits<Kind>::NativeType;
- auto elements = arrayVector->elements()->as<SimpleVector<T>>();
for (auto i = offset; i < offset + size; ++i) {
::substrait::Expression_Literal* childLiteral = listLiteral->add_values();
- if (elements->isNullAt(i)) {
- childLiteral->MergeFrom(toSubstraitNullLiteral(arena, Kind));
- } else {
- childLiteral->MergeFrom(toSubstraitNotNullLiteral<Kind>(arena,
elements->valueAt(i)));
- }
+ childLiteral->MergeFrom(scalarVectorToLiteral<Kind>(arena,
arrayVector->elements(), i));
}
}
@@ -292,24 +310,9 @@ template <TypeKind kind>
void convertVectorValue(
google::protobuf::Arena& arena,
const velox::VectorPtr& vectorValue,
- ::substrait::Expression_Nested_Struct* litValue,
- ::substrait::Expression_Literal* substraitField) {
- const TypePtr& childType = vectorValue->type();
-
- using T = typename TypeTraits<kind>::NativeType;
-
- auto childToFlatVec = vectorValue->as<SimpleVector<T>>();
-
- // Get the batchSize and convert each value in it.
- vector_size_t flatVecSize = childToFlatVec->size();
- for (int64_t i = 0; i < flatVecSize; i++) {
- substraitField = litValue->add_fields()->mutable_literal();
- if (childToFlatVec->isNullAt(i)) {
- // Process the null value.
- substraitField->MergeFrom(toSubstraitNullLiteral(arena,
childType->kind()));
- } else {
- substraitField->MergeFrom(toSubstraitNotNullLiteral<kind>(arena,
childToFlatVec->valueAt(i)));
- }
+ ::substrait::Expression_Nested_Struct* litValue) {
+ for (vector_size_t i = 0; i < vectorValue->size(); ++i) {
+
litValue->add_fields()->mutable_literal()->MergeFrom(scalarVectorToLiteral<kind>(arena,
vectorValue, i));
}
}
@@ -516,20 +519,21 @@ const ::substrait::Expression_Literal&
VeloxToSubstraitExprConvertor::toSubstrai
if (constExpr->hasValueVector()) {
return toSubstraitLiteral(arena, constExpr->valueVector(), litValue);
} else {
- return toSubstraitLiteral(arena, constExpr->value());
+ return toSubstraitLiteral(arena, constExpr->value(), constExpr->type());
}
}
const ::substrait::Expression_Literal&
VeloxToSubstraitExprConvertor::toSubstraitLiteral(
google::protobuf::Arena& arena,
- const velox::variant& variantValue) {
+ const velox::variant& variantValue,
+ const TypePtr& logicalType) {
::substrait::Expression_Literal* literalExpr =
google::protobuf::Arena::CreateMessage<::substrait::Expression_Literal>(&arena);
if (variantValue.isNull()) {
- literalExpr->MergeFrom(toSubstraitNullLiteral(arena, variantValue.kind()));
+ literalExpr->MergeFrom(toSubstraitNullLiteral(arena, logicalType));
} else {
- literalExpr->MergeFrom(toSubstraitNotNullLiteral(arena, variantValue));
+ literalExpr->MergeFrom(toSubstraitNotNullLiteral(arena, variantValue,
logicalType));
}
return *literalExpr;
}
@@ -572,7 +576,7 @@ const ::substrait::Expression_Literal&
VeloxToSubstraitExprConvertor::toSubstrai
if (constantVector->typeKind() == TypeKind::ARRAY) {
if (constantVector->isNullAt(0)) {
// Process the null value.
- substraitField->MergeFrom(toSubstraitNullLiteral(arena,
constantVector->typeKind()));
+ substraitField->MergeFrom(toSubstraitNullLiteral(arena,
constantVector->type()));
return *substraitField;
}
auto encoding = constantVector->valueVector()->encoding();
@@ -599,8 +603,13 @@ const ::substrait::Expression_Literal&
VeloxToSubstraitExprConvertor::toSubstrai
::substrait::Expression_Literal* substraitField =
google::protobuf::Arena::CreateMessage<::substrait::Expression_Literal>(&arena);
if (vectorValue->isScalar()) {
- VELOX_DYNAMIC_SCALAR_TYPE_DISPATCH(
- convertVectorValue, vectorValue->type()->kind(), arena, vectorValue,
litValue, substraitField);
+ if (litValue == nullptr) {
+ VELOX_CHECK_GT(vectorValue->size(), 0, "A scalar constant must contain a
value");
+ substraitField->MergeFrom(
+ VELOX_DYNAMIC_SCALAR_TYPE_DISPATCH(scalarVectorToLiteral,
vectorValue->typeKind(), arena, vectorValue, 0));
+ return *substraitField;
+ }
+ VELOX_DYNAMIC_SCALAR_TYPE_DISPATCH(convertVectorValue,
vectorValue->type()->kind(), arena, vectorValue, litValue);
return *substraitField;
}
diff --git a/cpp/velox/substrait/VeloxToSubstraitExpr.h
b/cpp/velox/substrait/VeloxToSubstraitExpr.h
index 83b9a5260f..dafbbc2e8f 100644
--- a/cpp/velox/substrait/VeloxToSubstraitExpr.h
+++ b/cpp/velox/substrait/VeloxToSubstraitExpr.h
@@ -47,6 +47,7 @@ class VeloxToSubstraitExprConvertor {
/// @param constExpr Velox Constant expression needed to be converted.
/// @param litValue The Nested.Struct the converted literals are appended
to, each wrapped in an
/// Expression. Substrait models struct-valued expressions as
Expression.Nested.Struct.
+ /// If null, converts only the constant value at index zero.
/// @return A pointer to Substrait Literal expression object allocated on
/// the arena and representing the input Velox Constant expression.
const ::substrait::Expression_Literal& toSubstraitExpr(
@@ -67,6 +68,7 @@ class VeloxToSubstraitExprConvertor {
/// Convert Velox vector to Substrait literal. One literal per row is
appended to litValue,
/// each wrapped in an Expression so it fits Substrait's
Expression.Nested.Struct container.
+ /// If litValue is null, converts only the scalar value at index zero.
const ::substrait::Expression_Literal& toSubstraitLiteral(
google::protobuf::Arena& arena,
const velox::VectorPtr& vectorValue,
@@ -88,7 +90,8 @@ class VeloxToSubstraitExprConvertor {
/// Convert Velox variant to Substrait Literal Expression.
const ::substrait::Expression_Literal& toSubstraitLiteral(
google::protobuf::Arena& arena,
- const velox::variant& variantValue);
+ const velox::variant& variantValue,
+ const velox::TypePtr& logicalType);
/// Convert values in Velox array vector to Substrait Literal List.
const ::substrait::Expression_Literal_List&
diff --git a/cpp/velox/tests/SubstraitVeloxExprConverterTest.cc
b/cpp/velox/tests/SubstraitVeloxExprConverterTest.cc
index 32b94da2b5..c3e02b24cb 100644
--- a/cpp/velox/tests/SubstraitVeloxExprConverterTest.cc
+++ b/cpp/velox/tests/SubstraitVeloxExprConverterTest.cc
@@ -16,6 +16,7 @@
*/
#include "substrait/SubstraitToVeloxExpr.h"
+#include "substrait/VeloxToSubstraitExpr.h"
#include "velox/common/base/tests/GTestUtils.h"
#include "velox/core/QueryConfig.h"
@@ -30,6 +31,100 @@ namespace gluten {
class SubstraitVeloxExprConverterExecutionTest : public
exec::test::OperatorTestBase {};
+TEST_F(SubstraitVeloxExprConverterExecutionTest, timestampLiterals) {
+ const std::unordered_map<uint64_t, std::string> functionMap;
+ SubstraitVeloxExprConverter converter(pool(), functionMap);
+ for (const auto& type : std::vector<TypePtr>{TIMESTAMP_UTC(), TIMESTAMP()}) {
+ SCOPED_TRACE(type->toString());
+ for (const int64_t micros : {-1LL, 0LL, 1LL, 1'704'067'200'123'456LL}) {
+ SCOPED_TRACE(micros);
+ ::substrait::Expression expression;
+ auto* literal = expression.mutable_literal();
+ if (type->equivalent(*TIMESTAMP_UTC())) {
+ literal->set_timestamp(micros);
+ } else {
+ literal->set_timestamp_tz(micros);
+ }
+ auto result =
+ std::dynamic_pointer_cast<const
core::ConstantTypedExpr>(converter.toVeloxExpr(expression, ROW({}, {})));
+ ASSERT_NE(result, nullptr);
+ EXPECT_TRUE(type->equivalent(*result->type()));
+ EXPECT_EQ(result->value().value<Timestamp>().toMicros(), micros);
+ }
+ }
+}
+
+TEST_F(SubstraitVeloxExprConverterExecutionTest, timestampLiteralRoundTrip) {
+ const std::unordered_map<uint64_t, std::string> functionMap;
+ SubstraitVeloxExprConverter reader(pool(), functionMap);
+ auto extensions = std::make_shared<SubstraitExtensionCollector>();
+ VeloxToSubstraitExprConvertor writer(extensions);
+ const std::vector<std::optional<Timestamp>> values = {
+ Timestamp(-1, 999'999'000), Timestamp(1'704'067'200, 123'456'000),
std::nullopt};
+
+ for (const auto& type : std::vector<TypePtr>{TIMESTAMP_UTC(), TIMESTAMP()}) {
+ SCOPED_TRACE(type->toString());
+ for (const auto& value : values) {
+ SCOPED_TRACE(value.has_value() ? value->toString() : "null");
+ for (const bool vectorBacked : {false, true}) {
+ SCOPED_TRACE(vectorBacked);
+ auto constant = vectorBacked
+ ? std::make_shared<const
core::ConstantTypedExpr>(makeConstant<Timestamp>(value, 1, type))
+ : std::make_shared<const core::ConstantTypedExpr>(
+ type, value.has_value() ? variant(value.value()) :
variant::null(TypeKind::TIMESTAMP));
+ google::protobuf::Arena arena;
+ ::substrait::Expression expression;
+ expression.mutable_literal()->MergeFrom(writer.toSubstraitExpr(arena,
constant));
+ const auto& literal = expression.literal();
+ if (value.has_value()) {
+ if (type->equivalent(*TIMESTAMP_UTC())) {
+ ASSERT_TRUE(literal.has_timestamp());
+ EXPECT_EQ(literal.timestamp(), value->toMicros());
+ } else {
+ ASSERT_TRUE(literal.has_timestamp_tz());
+ EXPECT_EQ(literal.timestamp_tz(), value->toMicros());
+ }
+ } else {
+ ASSERT_TRUE(literal.has_null());
+
EXPECT_TRUE(type->equivalent(*SubstraitParser::parseType(literal.null())));
+ }
+ auto result =
+ std::dynamic_pointer_cast<const
core::ConstantTypedExpr>(reader.toVeloxExpr(expression, ROW({}, {})));
+ ASSERT_NE(result, nullptr);
+ EXPECT_TRUE(type->equivalent(*result->type()));
+ ASSERT_EQ(result->isNull(), !value.has_value());
+ if (value.has_value()) {
+ EXPECT_EQ(result->value().value<Timestamp>(), value.value());
+ }
+ }
+ }
+ }
+}
+
+TEST_F(SubstraitVeloxExprConverterExecutionTest, dateLiteralEncoding) {
+ auto extensions = std::make_shared<SubstraitExtensionCollector>();
+ VeloxToSubstraitExprConvertor writer(extensions);
+ for (const auto& value : std::vector<std::optional<int32_t>>{std::nullopt,
19'723}) {
+ SCOPED_TRACE(value.has_value() ? std::to_string(value.value()) : "null");
+ for (const bool vectorBacked : {false, true}) {
+ SCOPED_TRACE(vectorBacked);
+ auto constant = vectorBacked
+ ? std::make_shared<const
core::ConstantTypedExpr>(makeConstant<int32_t>(value, 1, DATE()))
+ : std::make_shared<const core::ConstantTypedExpr>(
+ DATE(), value.has_value() ? variant(value.value()) :
variant::null(TypeKind::INTEGER));
+ google::protobuf::Arena arena;
+ const auto& literal = writer.toSubstraitExpr(arena, constant);
+ if (value.has_value()) {
+ ASSERT_TRUE(literal.has_i32());
+ EXPECT_EQ(literal.i32(), value.value());
+ } else {
+ ASSERT_TRUE(literal.has_null());
+ EXPECT_TRUE(literal.null().has_i32());
+ }
+ }
+ }
+}
+
// Regression test for a SIGSEGV in
// SubstraitVeloxExprConverter::toVeloxExpr(Expression::FieldReference, ...).
// The direct-reference loop descends one nested struct_field at a time with
diff --git a/cpp/velox/tests/VeloxSubstraitRoundTripTest.cc
b/cpp/velox/tests/VeloxSubstraitRoundTripTest.cc
index 6a9b2447d0..ae1922c329 100644
--- a/cpp/velox/tests/VeloxSubstraitRoundTripTest.cc
+++ b/cpp/velox/tests/VeloxSubstraitRoundTripTest.cc
@@ -122,6 +122,21 @@ class VeloxSubstraitRoundTripTest : public
OperatorTestBase {
}
}
+ void assertTypedPlanConversion(const ::substrait::Plan& plan, const
RowVectorPtr& expected) {
+ auto config =
std::make_shared<facebook::velox::config::ConfigBase>(std::unordered_map<std::string,
std::string>());
+ SubstraitToVeloxPlanConverter converter(
+ pool_.get(),
+ config.get(),
+ std::vector<std::shared_ptr<ResultIterator>>{},
+ VeloxConnectorIds{},
+ std::nullopt,
+ std::nullopt,
+ true);
+ auto result = converter.toVeloxPlan(plan);
+ ASSERT_TRUE(expected->type()->equivalent(*result->outputType())) <<
result->outputType()->toString();
+ assertQuery(result, expected);
+ }
+
std::shared_ptr<VeloxToSubstraitPlanConvertor> veloxConvertor_ =
std::make_shared<VeloxToSubstraitPlanConvertor>();
};
@@ -431,6 +446,78 @@ TEST_F(VeloxSubstraitRoundTripTest, notNullLiteral) {
assertPlanConversion(plan, "SELECT true, 23, 45, 678, 910, 1.23, 4.56,
'789'");
}
+TEST_F(VeloxSubstraitRoundTripTest, timestampLiteralProjection) {
+ const auto beforeEpoch = Timestamp(-1, 999'999'000);
+ const auto afterEpoch = Timestamp(1'704'067'200, 123'456'000);
+ auto input = makeRowVector(ROW({}, {}), 1);
+ for (const auto& type : std::vector<TypePtr>{TIMESTAMP_UTC(), TIMESTAMP()}) {
+ SCOPED_TRACE(type->toString());
+ auto expected = makeRowVector(
+ {makeFlatVector<Timestamp>({beforeEpoch}, type),
+ makeFlatVector<Timestamp>({afterEpoch}, type),
+ makeNullableFlatVector<Timestamp>({std::nullopt}, type)});
+ auto plan = PlanBuilder(pool_.get())
+ .values({input})
+ .projectExpressions(
+ {makeConstantExpr(type, variant(beforeEpoch)),
+ makeConstantExpr(type, variant(afterEpoch)),
+ makeConstantExpr(type,
variant::null(TypeKind::TIMESTAMP))})
+ .planNode();
+ assertQuery(plan, expected);
+ google::protobuf::Arena arena;
+ const auto& substraitPlan = veloxConvertor_->toSubstrait(arena, plan);
+ ASSERT_NO_FATAL_FAILURE(assertTypedPlanConversion(substraitPlan,
expected));
+ }
+}
+
+TEST_F(VeloxSubstraitRoundTripTest, timestampValues) {
+ const auto beforeEpoch = Timestamp(-1, 999'999'000);
+ const auto afterEpoch = Timestamp(1'704'067'200, 123'456'000);
+ auto input = makeRowVector(
+ {makeNullableFlatVector<Timestamp>({beforeEpoch, afterEpoch,
std::nullopt}, TIMESTAMP_UTC()),
+ makeNullableFlatVector<Timestamp>({afterEpoch, std::nullopt,
beforeEpoch}, TIMESTAMP())});
+ auto plan = PlanBuilder(pool_.get()).values({input}).planNode();
+ google::protobuf::Arena arena;
+ const auto& substraitPlan = veloxConvertor_->toSubstrait(arena, plan);
+ const auto& fields =
substraitPlan.relations(0).root().input().read().virtual_table().expressions(0).fields();
+ ASSERT_EQ(fields.size(), 6);
+ ASSERT_TRUE(fields.Get(0).literal().has_timestamp());
+ EXPECT_EQ(fields.Get(0).literal().timestamp(), beforeEpoch.toMicros());
+ ASSERT_TRUE(fields.Get(2).literal().has_null());
+ EXPECT_TRUE(fields.Get(2).literal().null().has_precision_timestamp());
+ ASSERT_TRUE(fields.Get(3).literal().has_timestamp_tz());
+ EXPECT_EQ(fields.Get(3).literal().timestamp_tz(), afterEpoch.toMicros());
+ ASSERT_TRUE(fields.Get(4).literal().has_null());
+ EXPECT_TRUE(fields.Get(4).literal().null().has_precision_timestamp_tz());
+ assertTypedPlanConversion(substraitPlan, input);
+}
+
+TEST_F(VeloxSubstraitRoundTripTest, timestampArrayLiteral) {
+ const auto beforeEpoch = Timestamp(-1, 999'999'000);
+ const auto afterEpoch = Timestamp(1'704'067'200, 123'456'000);
+ auto input = makeRowVector(ROW({}, {}), 1);
+ for (const auto& type : std::vector<TypePtr>{TIMESTAMP_UTC(), TIMESTAMP()}) {
+ SCOPED_TRACE(type->toString());
+ auto values = makeNullableArrayVector<Timestamp>({{beforeEpoch,
std::nullopt, afterEpoch}}, ARRAY(type));
+ auto empty = makeArrayVector<Timestamp>({{}}, type);
+ auto nullArray = BaseVector::createNullConstant(ARRAY(type), 1,
pool_.get());
+ auto nullNestedArray = BaseVector::createNullConstant(ARRAY(ARRAY(type)),
1, pool_.get());
+ auto expected = makeRowVector({values, empty, nullArray, nullNestedArray});
+ auto plan = PlanBuilder(pool_.get())
+ .values({input})
+ .projectExpressions(
+ {makeConstantExpr(values),
+ makeConstantExpr(empty),
+ makeConstantExpr(nullArray),
+ makeConstantExpr(nullNestedArray)})
+ .planNode();
+ assertQuery(plan, expected);
+ google::protobuf::Arena arena;
+ const auto& substraitPlan = veloxConvertor_->toSubstrait(arena, plan);
+ ASSERT_NO_FATAL_FAILURE(assertTypedPlanConversion(substraitPlan,
expected));
+ }
+}
+
TEST_F(VeloxSubstraitRoundTripTest, arrayLiteral) {
auto vectors = makeRowVector(ROW({}), 1);
auto plan = PlanBuilder(pool_.get())
@@ -470,6 +557,25 @@ TEST_F(VeloxSubstraitRoundTripTest, arrayLiteral) {
"array[], array[array[1,2,3], array[4,5]]");
}
+TEST_F(VeloxSubstraitRoundTripTest, dateArrayNullPositions) {
+ auto leadingNull = makeNullableArrayVector<int32_t>({{std::nullopt,
19'723}}, ARRAY(DATE()));
+ auto trailingNull = makeNullableArrayVector<int32_t>({{19'723,
std::nullopt}}, ARRAY(DATE()));
+ auto comparison = std::make_shared<const core::CallTypedExpr>(
+ BOOLEAN(),
+ std::vector<core::TypedExprPtr>{makeConstantExpr(leadingNull),
makeConstantExpr(trailingNull)},
+ "equalto");
+ auto input = makeRowVector(ROW({}, {}), 1);
+ auto expected = makeRowVector({makeFlatVector<bool>({false})});
+ auto plan = PlanBuilder(pool_.get())
+ .values({input})
+
.projectExpressions(std::vector<core::TypedExprPtr>{comparison})
+ .planNode();
+ assertQuery(plan, expected);
+ google::protobuf::Arena arena;
+ const auto& substraitPlan = veloxConvertor_->toSubstrait(arena, plan);
+ assertTypedPlanConversion(substraitPlan, expected);
+}
+
// Disabled due to https://github.com/facebookincubator/velox/pull/17318.
TEST_F(VeloxSubstraitRoundTripTest, DISABLED_dateType) {
auto a = makeFlatVector<int32_t>({0, 1});
diff --git
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/ExpressionBuilder.java
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/ExpressionBuilder.java
index 9d5b5c9a10..9f41fce803 100644
---
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/ExpressionBuilder.java
+++
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/ExpressionBuilder.java
@@ -177,6 +177,9 @@ public class ExpressionBuilder {
if (typeNode instanceof TimestampTypeNode) {
return makeTimestampLiteral((Long) obj, typeNode);
}
+ if (typeNode instanceof TimestampNTZTypeNode) {
+ return new TimestampNTZLiteralNode((Long) obj, typeNode);
+ }
if (typeNode instanceof StringTypeNode) {
return makeStringLiteral(obj.toString(), typeNode);
}
diff --git
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/StructLiteralNode.java
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/StructLiteralNode.java
index 981fa36f4c..aadeb0f035 100644
---
a/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/StructLiteralNode.java
+++
b/gluten-substrait/src/main/java/org/apache/gluten/substrait/expression/StructLiteralNode.java
@@ -58,7 +58,7 @@ public class StructLiteralNode extends
LiteralNodeWithValue<InternalRow> {
if (type instanceof DateTypeNode) {
return ExpressionBuilder.makeLiteral(value.getInt(index), type);
}
- if (type instanceof TimestampTypeNode) {
+ if (type instanceof TimestampTypeNode || type instanceof
TimestampNTZTypeNode) {
return ExpressionBuilder.makeLiteral(value.getLong(index), type);
}
if (type instanceof StringTypeNode) {
diff --git
a/gluten-substrait/src/main/scala/org/apache/gluten/expression/ConverterUtils.scala
b/gluten-substrait/src/main/scala/org/apache/gluten/expression/ConverterUtils.scala
index 2cd0fa9c06..284f21dbb5 100644
---
a/gluten-substrait/src/main/scala/org/apache/gluten/expression/ConverterUtils.scala
+++
b/gluten-substrait/src/main/scala/org/apache/gluten/expression/ConverterUtils.scala
@@ -328,6 +328,8 @@ object ConverterUtils extends Logging {
DecimalType(d.precision, d.scale)
case _: TimestampTypeNode =>
TimestampType
+ case _: TimestampNTZTypeNode =>
+ TimestampNTZType
case m: MapNode =>
val keyType = parseFromTypeNode(m.getKeyType)
val valueType = parseFromTypeNode(m.getValueType)
diff --git
a/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/validator/Validators.scala
b/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/validator/Validators.scala
index 07f847f70a..792e1c0edd 100644
---
a/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/validator/Validators.scala
+++
b/gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/validator/Validators.scala
@@ -25,7 +25,8 @@ import
org.apache.gluten.extension.columnar.offload.OffloadSingleNode
import org.apache.gluten.sql.shims.SparkShimLoader
import org.apache.spark.internal.Logging
-import org.apache.spark.sql.catalyst.expressions.{Alias, Attribute, Cast,
ConvertTimezone, Expression, GetStructField, Hour, IsNotNull, IsNull,
MakeTimestamp, Minute, Second, TimestampAdd}
+import org.apache.spark.sql.catalyst.analysis.PartitionSpec
+import org.apache.spark.sql.catalyst.expressions.{Alias, Attribute, Cast,
ConvertTimezone, Expression, GetStructField, Hour, IsNotNull, IsNull,
MakeTimestamp, Minute, Second, TimestampAdd, WindowFrame, WindowSpecDefinition}
import org.apache.spark.sql.execution._
import org.apache.spark.sql.execution.aggregate.{HashAggregateExec,
ObjectHashAggregateExec, SortAggregateExec}
import org.apache.spark.sql.execution.datasources.WriteFilesExec
@@ -93,9 +94,12 @@ object Validators {
builder.add(new FallbackByTestInjects())
}
- /** Fails validation if a plan node's input or output schema contains
TimestampNTZType. */
+ /**
+ * Fails validation for unsupported TimestampNTZType schemas, and for NTZ
expressions on
+ * backends without TimestampNTZ support.
+ */
def fallbackByTimestampNTZ(): Validator.Builder = {
- builder.add(new FallbackByTimestampNTZ(veloxConf))
+ builder.add(new FallbackByTimestampNTZ(veloxConf,
settings.supportTimestampNtz))
}
/**
@@ -232,7 +236,10 @@ object Validators {
}
}
- private class FallbackByTimestampNTZ(veloxConf: Option[Any]) extends
Validator {
+ private[validator] class FallbackByTimestampNTZ(
+ veloxConf: Option[Any],
+ backendSupportsTimestampNtz: Boolean)
+ extends Validator {
// Check if TimestampNTZ validation is enabled via VeloxConfig
// Default to true (enabled) if VeloxConfig is not available or method
call fails
private val enableValidation: Boolean = veloxConf
@@ -247,8 +254,6 @@ object Validators {
}
.getOrElse(true)
- private val backendSupportsTimestampNtz =
BackendsApiManager.getSettings.supportTimestampNtz
-
override def validate(plan: SparkPlan): Validator.OutCome = {
def containsNTZ(dataType: DataType): Boolean = dataType match {
case TimestampNTZType => true
@@ -267,6 +272,16 @@ object Validators {
val hasNTZ = plan.output.exists(a => containsNTZ(a.dataType)) ||
plan.children.exists(_.output.exists(a => containsNTZ(a.dataType)))
if (!hasNTZ) {
+ if (
+ !backendSupportsTimestampNtz &&
+ plan.expressions.exists(_.exists {
+ // These metadata nodes have no dataType; exists still visits
their children.
+ case _: WindowSpecDefinition | _: WindowFrame | _: PartitionSpec
=> false
+ case expr => containsNTZ(expr.dataType)
+ })
+ ) {
+ return fail(s"${plan.nodeName} has TimestampNTZType in expressions")
+ }
return pass()
}
diff --git
a/gluten-ut/spark41/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
b/gluten-ut/spark41/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
index d96a2b0bd8..1e29452c78 100644
---
a/gluten-ut/spark41/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
+++
b/gluten-ut/spark41/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
@@ -205,6 +205,7 @@ class VeloxTestSettings extends BackendTestSettings {
.exclude("default")
// FIXME(yma11): ObjectType is not covered in RowEncoder/Serializer in
vanilla spark
.exclude("SPARK-37967: Literal.create support ObjectType")
+ enableSuite[GlutenTimestampNtzLiteralSuite]
enableSuite[GlutenMathExpressionsSuite]
// Spark round UT for round(3.1415,3) is not correct.
.exclude("round/bround/floor/ceil")
diff --git
a/gluten-ut/spark41/src/test/scala/org/apache/spark/sql/GlutenTimestampNtzLiteralSuite.scala
b/gluten-ut/spark41/src/test/scala/org/apache/spark/sql/GlutenTimestampNtzLiteralSuite.scala
new file mode 100644
index 0000000000..28c1168b15
--- /dev/null
+++
b/gluten-ut/spark41/src/test/scala/org/apache/spark/sql/GlutenTimestampNtzLiteralSuite.scala
@@ -0,0 +1,137 @@
+/*
+ * 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.spark.sql
+
+import org.apache.gluten.config.GlutenConfig
+import org.apache.gluten.execution.ProjectExecTransformer
+import org.apache.gluten.substrait.expression.ExpressionBuilder
+
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.util.GenericArrayData
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.{ArrayType, StructField, StructType,
TimestampNTZType, TimestampType}
+
+import java.time.LocalDateTime
+
+class GlutenTimestampNtzLiteralSuite extends GlutenSQLTestsTrait {
+
+ testGluten("timestamp literal serialization preserves logical types") {
+ Seq(TimestampNTZType, TimestampType).foreach {
+ dataType =>
+ Seq(-1L, 1704067200123456L).foreach {
+ micros =>
+ val scalar =
+ ExpressionBuilder.makeLiteral(Long.box(micros), dataType,
true).toProtobuf.getLiteral
+ val array = ExpressionBuilder
+ .makeLiteral(
+ new GenericArrayData(Array[Any](micros, null)),
+ ArrayType(dataType),
+ true)
+ .toProtobuf.getLiteral
+ val struct = ExpressionBuilder
+ .makeLiteral(
+ InternalRow(micros, null),
+ StructType(Seq(StructField("value", dataType),
StructField("missing", dataType))),
+ true)
+ .toProtobuf.getLiteral
+ Seq(scalar, array.getList.getValues(0),
struct.getStruct.getFields(0)).foreach {
+ literal =>
+ if (dataType == TimestampNTZType) {
+ assert(literal.hasTimestamp)
+ assert(literal.getTimestamp == micros)
+ } else {
+ assert(literal.hasTimestampTz)
+ assert(literal.getTimestampTz == micros)
+ }
+ }
+ Seq(
+ ExpressionBuilder.makeLiteral(null, dataType,
true).toProtobuf.getLiteral,
+ array.getList.getValues(1),
+ struct.getStruct.getFields(1)).foreach {
+ literal =>
+ assert(literal.hasNull)
+ if (dataType == TimestampNTZType) {
+ assert(literal.getNull.hasPrecisionTimestamp)
+ } else {
+ assert(literal.getNull.hasPrecisionTimestampTz)
+ }
+ }
+ }
+ }
+ }
+
+ testGluten("timestamp_ntz literals in native expressions") {
+ withSQLConf(
+ SQLConf.ANSI_ENABLED.key -> "false",
+ GlutenConfig.GLUTEN_ANSI_FALLBACK_ENABLED.key -> "false",
+ "spark.gluten.sql.columnar.backend.velox.enableTimestampNtzValidation"
-> "false"
+ ) {
+ val result = spark.range(2).selectExpr(
+ "timestampadd(MICROSECOND, id, cast('1969-12-31 23:59:59.999999' as
timestamp_ntz))",
+ "timestampadd(MICROSECOND, id, cast('2024-01-01 00:00:00.123456' as
timestamp_ntz))"
+ )
+ checkAnswer(
+ result,
+ Seq(
+ Row(
+ LocalDateTime.parse("1969-12-31T23:59:59.999999"),
+ LocalDateTime.parse("2024-01-01T00:00:00.123456")),
+ Row(
+ LocalDateTime.parse("1970-01-01T00:00:00"),
+ LocalDateTime.parse("2024-01-01T00:00:00.123457"))
+ )
+ )
+ val resultOutput = result.queryExecution.executedPlan.outputSet
+ assert(
+ getExecutedPlan(result).exists {
+ case project: ProjectExecTransformer => project.outputSet ==
resultOutput
+ case _ => false
+ },
+ result.queryExecution.executedPlan.treeString
+ )
+ }
+ }
+
+ testGluten("timestamp_ntz literals nested in native expressions ignore the
session timezone") {
+ Seq("UTC", "America/Los_Angeles").foreach {
+ timeZone =>
+ withSQLConf(
+ SQLConf.ANSI_ENABLED.key -> "false",
+ SQLConf.SESSION_LOCAL_TIMEZONE.key -> timeZone,
+ GlutenConfig.GLUTEN_ANSI_FALLBACK_ENABLED.key -> "false",
+
"spark.gluten.sql.columnar.backend.velox.enableTimestampNtzValidation" ->
"false"
+ ) {
+ val result = spark.range(2).selectExpr(
+ """hour(element_at(
+ | array(
+ | TIMESTAMP_NTZ '2024-01-01 00:00:00',
+ | TIMESTAMP_NTZ '2024-01-01 01:00:00'),
+ | CAST(id + 1 AS INT))) AS hour
+ |""".stripMargin)
+ checkAnswer(result, Seq(Row(0), Row(1)))
+ val resultOutput = result.queryExecution.executedPlan.outputSet
+ assert(
+ getExecutedPlan(result).exists {
+ case project: ProjectExecTransformer => project.outputSet ==
resultOutput
+ case _ => false
+ },
+ result.queryExecution.executedPlan.treeString
+ )
+ }
+ }
+ }
+}
diff --git
a/gluten-ut/spark42/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
b/gluten-ut/spark42/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
index f39b80e634..36c9d7c327 100644
---
a/gluten-ut/spark42/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
+++
b/gluten-ut/spark42/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala
@@ -230,6 +230,7 @@ class VeloxTestSettings extends BackendTestSettings {
.exclude("default")
// FIXME(yma11): ObjectType is not covered in RowEncoder/Serializer in
vanilla spark
.exclude("SPARK-37967: Literal.create support ObjectType")
+ enableSuite[GlutenTimestampNtzLiteralSuite]
enableSuite[GlutenMathExpressionsSuite]
// Spark round UT for round(3.1415,3) is not correct.
.exclude("round/bround/floor/ceil")
diff --git
a/gluten-ut/spark42/src/test/scala/org/apache/spark/sql/GlutenTimestampNtzLiteralSuite.scala
b/gluten-ut/spark42/src/test/scala/org/apache/spark/sql/GlutenTimestampNtzLiteralSuite.scala
new file mode 120000
index 0000000000..59e14dd497
--- /dev/null
+++
b/gluten-ut/spark42/src/test/scala/org/apache/spark/sql/GlutenTimestampNtzLiteralSuite.scala
@@ -0,0 +1 @@
+../../../../../../../../spark41/src/test/scala/org/apache/spark/sql/GlutenTimestampNtzLiteralSuite.scala
\ No newline at end of file
diff --git
a/gluten-ut/test/src/test/scala/org/apache/gluten/extension/columnar/validator/GlutenTimestampNtzValidatorSuite.scala
b/gluten-ut/test/src/test/scala/org/apache/gluten/extension/columnar/validator/GlutenTimestampNtzValidatorSuite.scala
new file mode 100644
index 0000000000..2927a45d31
--- /dev/null
+++
b/gluten-ut/test/src/test/scala/org/apache/gluten/extension/columnar/validator/GlutenTimestampNtzValidatorSuite.scala
@@ -0,0 +1,201 @@
+/*
+ * 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.gluten.extension.columnar.validator
+
+import org.apache.spark.SparkFunSuite
+import org.apache.spark.rdd.RDD
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.analysis.ResolvedPartitionSpec
+import org.apache.spark.sql.catalyst.expressions._
+import org.apache.spark.sql.catalyst.expressions.aggregate.Sum
+import org.apache.spark.sql.catalyst.util.GenericArrayData
+import org.apache.spark.sql.connector.catalog.SupportsPartitionManagement
+import org.apache.spark.sql.execution.{FilterExec, LeafExecNode, ProjectExec,
SparkPlan}
+import org.apache.spark.sql.execution.datasources.v2.DropPartitionExec
+import org.apache.spark.sql.execution.window.WindowExec
+import org.apache.spark.sql.types._
+
+import org.mockito.Mockito.mock
+
+class GlutenTimestampNtzValidatorSuite extends SparkFunSuite {
+ import GlutenTimestampNtzValidatorSuite.InputPlan
+
+ private val id = AttributeReference("id", IntegerType, nullable = false)()
+ private val input = InputPlan(Seq(id))
+
+ private def validator(supportsNtz: Boolean, enableValidation: Boolean):
Validator = {
+ new Validators.FallbackByTimestampNTZ(
+ Some(new GlutenTimestampNtzValidatorSuite.TestConfig(enableValidation)),
+ supportsNtz)
+ }
+
+ private def windowPlan(spec: WindowSpecDefinition): WindowExec = {
+ val sum = Alias(WindowExpression(Sum(id).toAggregateExpression(), spec),
"sum")()
+ WindowExec(Seq(sum), spec.partitionSpec, spec.orderSpec, input)
+ }
+
+ test("timestamp literals inside projections and filters respect backend
support") {
+ for {
+ dataType <- Seq(TimestampNTZType, TimestampType)
+ supportsNtz <- Seq(false, true)
+ enableValidation <- Seq(false, true)
+ } {
+ val timestamps = Literal(
+ new GenericArrayData(Array(1704067200000000L, 1704070800000000L)),
+ ArrayType(dataType, containsNull = false))
+ val hour = Hour(
+ ElementAt(timestamps, Add(id, Literal(1)), failOnError = false),
+ Some("America/Los_Angeles"))
+ val plans = Seq[SparkPlan](
+ ProjectExec(Seq(Alias(hour, "hour")()), input),
+ FilterExec(EqualTo(hour, Literal(0)), input))
+ plans.foreach {
+ plan =>
+ withClue(s"$dataType, supportsNtz=$supportsNtz,
validation=$enableValidation, $plan") {
+ assert(plan.output.forall(_.dataType == IntegerType))
+ assert(plan.children.forall(_.output.forall(_.dataType ==
IntegerType)))
+ val expected = if (dataType == TimestampNTZType && !supportsNtz) {
+ Validator.Failed(s"${plan.nodeName} has TimestampNTZType in
expressions")
+ } else {
+ Validator.Passed
+ }
+ assert(validator(supportsNtz, enableValidation).validate(plan) ==
expected)
+ }
+ }
+ }
+ }
+
+ test("unsupported backends reject NTZ nested in literal types") {
+ val structType = StructType(Seq(StructField("timestamps",
ArrayType(TimestampNTZType))))
+ val dataTypes = Seq(
+ TimestampNTZType,
+ ArrayType(TimestampNTZType),
+ structType,
+ MapType(TimestampNTZType, StringType),
+ MapType(StringType, ArrayType(structType)))
+ for {
+ dataType <- dataTypes
+ literal <- Seq(Literal.default(dataType), Literal.create(null, dataType))
+ enableValidation <- Seq(false, true)
+ } {
+ val plan = ProjectExec(Seq(Alias(IsNull(literal), "missing")()), input)
+ withClue(s"$literal, validation=$enableValidation") {
+ assert(plan.output.map(_.dataType) == Seq(BooleanType))
+ assert(
+ validator(supportsNtz = false, enableValidation =
enableValidation).validate(plan) ==
+ Validator.Failed("Project has TimestampNTZType in expressions"))
+ }
+ }
+ }
+
+ test("window metadata without a data type does not fail NTZ validation") {
+ for {
+ frameType <- Seq(RowFrame, RangeFrame)
+ supportsNtz <- Seq(false, true)
+ enableValidation <- Seq(false, true)
+ } {
+ val spec = WindowSpecDefinition(
+ Seq(id),
+ Seq(SortOrder(id, Ascending)),
+ SpecifiedWindowFrame(frameType, UnboundedPreceding, CurrentRow))
+ withClue(s"$frameType, supportsNtz=$supportsNtz,
validation=$enableValidation") {
+ assert(validator(
+ supportsNtz,
+ enableValidation).validate(windowPlan(spec)) == Validator.Passed)
+ }
+ }
+ }
+
+ test("NTZ expressions in window specifications and frame bounds still
trigger fallback") {
+ for {
+ dataType <- Seq(TimestampNTZType, TimestampType)
+ supportsNtz <- Seq(false, true)
+ enableValidation <- Seq(false, true)
+ } {
+ val hour = Hour(Literal(1704070800000000L, dataType), Some("UTC"))
+ val spec = WindowSpecDefinition(
+ Seq(id),
+ Seq(SortOrder(id, Ascending)),
+ SpecifiedWindowFrame(RowFrame, UnboundedPreceding, CurrentRow))
+ val specs = Seq(
+ spec.copy(partitionSpec = Seq(hour)),
+ spec.copy(orderSpec = Seq(SortOrder(hour, Ascending))),
+ spec.copy(frameSpecification = SpecifiedWindowFrame(RowFrame,
CurrentRow, hour))
+ )
+ specs.foreach {
+ windowSpec =>
+ val plan = windowPlan(windowSpec)
+ withClue(
+ s"$dataType, supportsNtz=$supportsNtz,
validation=$enableValidation, $windowSpec") {
+ assert(plan.output.map(_.dataType) == Seq(IntegerType, LongType))
+ val expected = if (dataType == TimestampNTZType && !supportsNtz) {
+ Validator.Failed("Window has TimestampNTZType in expressions")
+ } else {
+ Validator.Passed
+ }
+ assert(validator(supportsNtz, enableValidation).validate(plan) ==
expected)
+ }
+ }
+ }
+ }
+
+ test("partition command metadata without a data type does not fail NTZ
validation") {
+ val plan = DropPartitionExec(
+ mock(classOf[SupportsPartitionManagement]),
+ Seq(ResolvedPartitionSpec(Seq("id"), InternalRow(1))),
+ ignoreIfNotExists = false,
+ purge = false,
+ refreshCache = () => ()
+ )
+ for {
+ supportsNtz <- Seq(false, true)
+ enableValidation <- Seq(false, true)
+ } {
+ withClue(s"supportsNtz=$supportsNtz, validation=$enableValidation") {
+ assert(plan.output.isEmpty)
+ assert(validator(supportsNtz, enableValidation).validate(plan) ==
Validator.Passed)
+ }
+ }
+ }
+
+ test("existing NTZ input schema validation is unchanged") {
+ val timestamp = AttributeReference("ts", TimestampNTZType)()
+ val plan = ProjectExec(
+ Seq(Alias(Hour(timestamp, Some("UTC")), "hour")()),
+ InputPlan(Seq(timestamp)))
+ for {
+ supportsNtz <- Seq(false, true)
+ enableValidation <- Seq(false, true)
+ } {
+ val expected = if (supportsNtz && !enableValidation) {
+ Validator.Passed
+ } else {
+ Validator.Failed("Project has TimestampNTZType in input/output schema")
+ }
+ assert(validator(supportsNtz, enableValidation).validate(plan) ==
expected)
+ }
+ }
+}
+
+object GlutenTimestampNtzValidatorSuite {
+ private case class InputPlan(override val output: Seq[Attribute]) extends
LeafExecNode {
+ override protected def doExecute(): RDD[InternalRow] =
+ throw new UnsupportedOperationException("This plan is only used for
schema validation.")
+ }
+
+ class TestConfig(val enableTimestampNtzValidation: Boolean)
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]