Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.apache.gluten.expression.VeloxDummyExpression
import org.apache.spark.SparkConf
import org.apache.spark.shuffle.GlutenShuffleUtils
import org.apache.spark.sql.{DataFrame, Row}
import org.apache.spark.sql.catalyst.expressions.Cast
import org.apache.spark.sql.execution._
import org.apache.spark.sql.execution.adaptive.{AdaptiveSparkPlanHelper, AQEShuffleReadExec, ColumnarAQEShuffleReadExec, ShuffleQueryStageExec}
import org.apache.spark.sql.execution.joins.BaseJoinExec
Expand Down Expand Up @@ -1957,6 +1958,35 @@ class MiscOperatorSuite extends VeloxWholeStageTransformerSuite with AdaptiveSpa
}
}

test("cast null type to complex type") {
// An outer join whose right side turns out to be empty is replaced with a projection of a
// null cast to the type of each of that side's output attributes. Here the right side is only
// known to be empty once its stage has run, so AQE adds the casts after constant folding and
// they reach the backend as casts from the null type rather than as typed null literals.
val query =
"""
|select l.l_orderkey, r.arr, r.m, r.s
|from lineitem l left outer join (
| select l_orderkey, array(l_partkey) as arr, map('k', l_partkey) as m,
| struct(l_partkey as a) as s
| from lineitem where l_orderkey < 0
|) r on l.l_orderkey = r.l_orderkey
|""".stripMargin
runQueryAndCompare(query) {
df =>
val plan = df.queryExecution.executedPlan
val castsToComplexTypes = collect(plan) { case p: ProjectExecTransformer => p }
.flatMap(_.projectList)
.flatMap(_.collect { case c: Cast if c.child.dataType == NullType => c.dataType })
assert(
castsToComplexTypes.exists(_.isInstanceOf[ArrayType]),
s"Expect the null casts to be offloaded in:\n$plan")
// The casts must run natively rather than being split out to the JVM.
assert(collect(plan) { case p: ColumnarPartialProjectExec => p }.isEmpty)
assert(collect(plan) { case p: ProjectExec => p }.isEmpty)
}
}

test("timestamp broadcast join") {
spark.range(0, 5).createOrReplaceTempView("right")
spark.sql("SELECT id, timestamp_micros(id) as ts from right").createOrReplaceTempView("left")
Expand Down
8 changes: 8 additions & 0 deletions cpp/velox/substrait/SubstraitToVeloxPlanValidator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,14 @@ bool SubstraitToVeloxPlanValidator::isAllowedCast(const TypePtr& fromType, const
return false;
}

// Casting from UNKNOWN, e.g. a null constant, is allowed for any target type,
// including complex ones. The input is all nulls, so Velox short-circuits the
// cast to a null constant of the target type without ever looking at the
// input values.
if (fromType->kind() == TypeKind::UNKNOWN) {
return true;
}

// Limited support for DATE to X.
if (fromType->isDate() && !toType->isTimestamp() && !toType->isVarchar()) {
return false;
Expand Down
53 changes: 53 additions & 0 deletions cpp/velox/tests/Substrait2VeloxPlanValidatorTest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,57 @@ TEST_F(Substrait2VeloxPlanValidatorTest, aggregateMaskMustBeTopLevelField) {
EXPECT_FALSE(validatePlan(nestedPlan));
}

TEST_F(Substrait2VeloxPlanValidatorTest, castFromUnknown) {
const auto validateCast = [&](const RowTypePtr& inputType,
const std::function<void(::substrait::Expression*)>& setInput,
const std::function<void(::substrait::Type*)>& setToType) {
::substrait::Expression expression;
auto* cast = expression.mutable_cast();
setInput(cast->mutable_input());
setToType(cast->mutable_type());

auto planValidator = std::make_shared<SubstraitToVeloxPlanValidator>(pool_.get());
return planValidator->validate(expression, inputType, {});
};

// A null constant, e.g. Spark's NullType, is expressed as the Nothing type.
const auto setNullInput = [](::substrait::Expression* input) {
input->mutable_literal()->mutable_null()->mutable_nothing();
};
const auto setNullableI32 = [](::substrait::Type* type) {
type->mutable_i32()->set_nullability(::substrait::Type_Nullability_NULLABILITY_NULLABLE);
};

// Casting from Nothing is allowed for both scalar and complex target types.
EXPECT_TRUE(validateCast(ROW({}, {}), setNullInput, setNullableI32));

EXPECT_TRUE(validateCast(ROW({}, {}), setNullInput, [&](::substrait::Type* type) {
auto* list = type->mutable_list();
list->set_nullability(::substrait::Type_Nullability_NULLABILITY_NULLABLE);
setNullableI32(list->mutable_type());
}));

EXPECT_TRUE(validateCast(ROW({}, {}), setNullInput, [&](::substrait::Type* type) {
auto* map = type->mutable_map();
map->set_nullability(::substrait::Type_Nullability_NULLABILITY_NULLABLE);
setNullableI32(map->mutable_key());
setNullableI32(map->mutable_value());
}));

EXPECT_TRUE(validateCast(ROW({}, {}), setNullInput, [&](::substrait::Type* type) {
auto* structType = type->mutable_struct_();
structType->set_nullability(::substrait::Type_Nullability_NULLABILITY_NULLABLE);
structType->add_names("");
setNullableI32(structType->add_types());
}));

// Casting a complex type to an unrelated type is still not allowed.
EXPECT_FALSE(validateCast(
ROW({"a"}, {ARRAY(INTEGER())}),
[](::substrait::Expression* input) {
input->mutable_selection()->mutable_direct_reference()->mutable_struct_field()->set_field(0);
},
setNullableI32));
}

} // namespace gluten
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,8 @@ class VeloxTestSettings extends BackendTestSettings {
.exclude("NOT NULL checks for atomic top-level fields (byPosition)")
.exclude("NOT NULL checks for nested struct fields (byName)")
.exclude("NOT NULL checks for nested struct fields (byPosition)")
.exclude("NOT NULL checks for nested structs, arrays, maps (byName)")
.exclude("NOT NULL checks for nested structs, arrays, maps (byPosition)")
.exclude("NOT NULL checks for nullable array with required element (byPosition)")
.exclude("not null checks for fields inside nullable array (byPosition)")
enableSuite[GlutenTableOptionsConstantFoldingSuite]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,8 @@ class VeloxTestSettings extends BackendTestSettings {
.exclude("NOT NULL checks for atomic top-level fields (byPosition)")
.exclude("NOT NULL checks for nested struct fields (byName)")
.exclude("NOT NULL checks for nested struct fields (byPosition)")
.exclude("NOT NULL checks for nested structs, arrays, maps (byName)")
.exclude("NOT NULL checks for nested structs, arrays, maps (byPosition)")
.exclude("NOT NULL checks for nullable array with required element (byPosition)")
.exclude("not null checks for fields inside nullable array (byPosition)")
enableSuite[GlutenTableOptionsConstantFoldingSuite]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,7 @@ class VeloxTestSettings extends BackendTestSettings {
.exclude("NOT NULL checks for nested struct fields (byName)")
.exclude("NOT NULL checks for nested struct fields (byPosition)")
.exclude("NOT NULL checks for nested structs, arrays, maps (byName)")
.exclude("NOT NULL checks for nested structs, arrays, maps (byPosition)")
.exclude("NOT NULL checks for nullable array with required element (byPosition)")
.exclude("not null checks for fields inside nullable array (byPosition)")
enableSuite[GlutenTableOptionsConstantFoldingSuite]
Expand Down