Skip to content
Closed
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
2 changes: 2 additions & 0 deletions cpp/src/arrow/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,8 @@ set(ARROW_COMPUTE_SRCS
compute/row/compare_internal.cc
compute/row/grouper.cc
compute/row/row_internal.cc
compute/special_form.cc
compute/special_forms/if_else_special_form.cc
compute/util.cc
compute/util_internal.cc)

Expand Down
33 changes: 27 additions & 6 deletions cpp/src/arrow/compute/exec.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1345,20 +1345,41 @@ const CpuInfo* ExecContext::cpu_info() const { return CpuInfo::GetInstance(); }

SelectionVector::SelectionVector(std::shared_ptr<ArrayData> data)
: data_(std::move(data)) {
DCHECK_EQ(Type::INT32, data_->type->id());
DCHECK_EQ(Type::BOOL, data_->type->id());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bitmaps are alternatives to selection-vectors, yes, but they have a disadvantage: they don't reduce in size as you process them — you start with length bits and end up with length bits and have to scan them over and over. Ideally we should have both forms and use bitmaps when the selection is small, but if we are going to simplify things and have a single form it should be selection vectors.

DCHECK_EQ(0, data_->GetNullCount());
indices_ = data_->GetValues<int32_t>(1);
}

SelectionVector::SelectionVector(const Array& arr) : SelectionVector(arr.data()) {}

int32_t SelectionVector::length() const { return static_cast<int32_t>(data_->length); }
Result<std::unique_ptr<SelectionVector>> SelectionVector::Copy(
const std::shared_ptr<MemoryManager>& mm) const {
ARROW_ASSIGN_OR_RAISE(auto copy, data_->CopyTo(mm));
return std::make_unique<SelectionVector>(std::move(copy));
}

Status SelectionVector::Intersect(const ArrayData& other) {
DCHECK_EQ(Type::BOOL, other.type->id());
DCHECK_EQ(data_->length, other.length);
::arrow::internal::BitmapAnd(data_->buffers[1]->data(), data_->offset,
other.buffers[1]->data(), other.offset, data_->length,
data_->offset, data_->buffers[1]->mutable_data());
return Status::OK();
}

Result<std::shared_ptr<SelectionVector>> SelectionVector::FromMask(
const BooleanArray& arr) {
return Status::NotImplemented("FromMask");
Status SelectionVector::Intersect(const SelectionVector& other) {
return Intersect(*other.data());
}

Status SelectionVector::Invert() {
::arrow::internal::InvertBitmap(data_->buffers[1]->data(), data_->offset, data_->length,
data_->buffers[1]->mutable_data(), data_->offset);
return Status::OK();
}

std::shared_ptr<ArrayData> SelectionVector::data() const { return data_; }

int32_t SelectionVector::length() const { return static_cast<int32_t>(data_->length); }

Result<Datum> CallFunction(const std::string& func_name, const std::vector<Datum>& args,
const FunctionOptions* options, ExecContext* ctx) {
if (ctx == nullptr) {
Expand Down
15 changes: 11 additions & 4 deletions cpp/src/arrow/compute/exec.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,20 @@ class ARROW_EXPORT SelectionVector {

explicit SelectionVector(const Array& arr);

/// \brief Create SelectionVector from boolean mask
static Result<std::shared_ptr<SelectionVector>> FromMask(const BooleanArray& arr);
Result<std::unique_ptr<SelectionVector>> Copy(
const std::shared_ptr<MemoryManager>& mm) const;

const int32_t* indices() const { return indices_; }
Status Intersect(const ArrayData& other);

Status Intersect(const SelectionVector& other);

Status Invert();

std::shared_ptr<ArrayData> data() const;
int32_t length() const;

private:
std::shared_ptr<ArrayData> data_;
const int32_t* indices_;
};

/// An index to represent that a batch does not belong to an ordered stream
Expand Down Expand Up @@ -422,6 +427,8 @@ struct ARROW_EXPORT ExecSpan {

int64_t length = 0;
std::vector<ExecValue> values;
// A non-owning selection vector to be used by selection-vector-aware kernels.
SelectionVector* selection_vector = NULLPTR;
};

/// \defgroup compute-call-function One-shot calls to compute functions
Expand Down
100 changes: 62 additions & 38 deletions cpp/src/arrow/compute/expression.cc
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@

#include "arrow/chunked_array.h"
#include "arrow/compute/api_vector.h"
#include "arrow/compute/exec_internal.h"
#include "arrow/compute/expression_internal.h"
#include "arrow/compute/function_internal.h"
#include "arrow/compute/special_form.h"
#include "arrow/compute/util.h"
#include "arrow/io/memory.h"
#include "arrow/ipc/reader.h"
Expand Down Expand Up @@ -72,11 +72,12 @@ Expression field_ref(FieldRef ref) {
}

Expression call(std::string function, std::vector<Expression> arguments,
std::shared_ptr<compute::FunctionOptions> options) {
std::shared_ptr<compute::FunctionOptions> options, bool is_special_form) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly the kind of added complexity around calls that I'm trying to convince you is not a good idea. This boolean is effectively saying the arguments are passed in call-by-name fashion and not call-by-value [1].

We should keep the Arrow rewrite system a strict-evaluated system with just 1 or 2 special forms. That means calls are always called by value. We keep the if_else and case_when functions as they are and introduce a ConditionalSpecialForm in the context of compute::Expression.

C++ is a strictly evaluated rewrite system. You can't declare a function and say "all the parameters are call-by-name" like you're trying to do with this boolean parameter. So you can implement if_else just like Arrow does here and has to use special syntax to get a conditional that takes two expression in a call by name fashion.

template<typename T>
T if_else(bool cond, T then_case, T else_case) {
  if (cond) { return then_case; } else { return else_case; }
}

// Non-strict evaluation with special form syntax. The branches of the
// if are non-strictly evaluated in a way that can't be achieved with
// the `if_else` Call.
if (den > 0) {
  return num / den;
} else {
  return 0;
}

A general conditional special form can serve for if-else, and, and or. Probably the single special form we need.

https://en.wikipedia.org/wiki/Evaluation_strategy#Non-strict_binding_strategies

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your explanation is convincing and it's hard to not be convinced. I'll need some more time to digest your comments and get back to you.

I really appreciate your kind help and insightful comments, and the pointer to the interesting reading too :)

Thank you @felipecrv !

Expression::Call call;
call.function_name = std::move(function);
call.arguments = std::move(arguments);
call.options = std::move(options);
call.is_special_form = is_special_form;
return Expression(std::move(call));
}

Expand Down Expand Up @@ -119,6 +120,16 @@ const DataType* Expression::type() const {
return CallNotNull(*this)->type.type;
}

bool Expression::selection_vector_aware() const {
DCHECK(IsBound());

if (literal() || field_ref()) {
return true;
}

return CallNotNull(*this)->selection_vector_aware;
}

namespace {

std::string PrintDatum(const Datum& datum) {
Expand Down Expand Up @@ -550,6 +561,11 @@ Result<Expression> BindNonRecursive(Expression::Call call, bool insert_implicit_

ARROW_ASSIGN_OR_RAISE(
call.type, call.kernel->signature->out_type().Resolve(&kernel_context, types));

if (call.is_special_form) {
ARROW_ASSIGN_OR_RAISE(call.special_form, SpecialForm::Make(call.function_name));
}

return Status::OK();
};

Expand All @@ -572,7 +588,11 @@ Result<Expression> BindNonRecursive(Expression::Call call, bool insert_implicit_
types = GetTypesWithSmallestLiteralRepresentation(call.arguments);
ARROW_ASSIGN_OR_RAISE(call.kernel, call.function->DispatchBest(&types));

call.selection_vector_aware = call.kernel->selection_vector_aware;

for (size_t i = 0; i < types.size(); ++i) {
call.selection_vector_aware &= call.arguments[i].selection_vector_aware();

if (types[i] == call.arguments[i].type()) continue;

if (const Datum* lit = call.arguments[i].literal()) {
Expand Down Expand Up @@ -716,6 +736,30 @@ Result<Datum> ExecuteScalarExpression(const Expression& expr, const Schema& full
return ExecuteScalarExpression(expr, input, exec_context);
}

namespace {

// Execute a scalar expression that is not fully selection-vector-aware on a batch
// carrying a valid selection vector using the slow path: gathering the input and
// scattering the output.
Result<Datum> ExecuteScalarExpressionWithSelSlowPath(const Expression& expr,
const ExecBatch& input,
compute::ExecContext* exec_context) {
DCHECK(!expr.selection_vector_aware() && input.selection_vector);
auto values = input.values;
for (auto& value : values) {
if (value.is_scalar()) continue;
ARROW_ASSIGN_OR_RAISE(
value, Filter(value, input.selection_vector->data(), FilterOptions::Defaults()));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, this can be implemented like this, funny

ARROW_ASSIGN_OR_RAISE(ExecBatch selected_batch, ExecBatch::Make(std::move(values)));
ARROW_ASSIGN_OR_RAISE(auto partial_result,
ExecuteScalarExpression(expr, selected_batch, exec_context));
// TODO: Scatter.
return partial_result;
}

} // namespace

Result<Datum> ExecuteScalarExpression(const Expression& expr, const ExecBatch& input,
compute::ExecContext* exec_context) {
if (exec_context == nullptr) {
Expand All @@ -732,6 +776,10 @@ Result<Datum> ExecuteScalarExpression(const Expression& expr, const ExecBatch& i
"ExecuteScalarExpression cannot Execute non-scalar expression ", expr.ToString());
}

if (!expr.selection_vector_aware() && input.selection_vector) {
Copy link
Contributor Author

@zanmato1984 zanmato1984 Jun 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually implies a strategy (1) that is: if any subexpression (including the top-level expression itself) is not selection vector aware, we do the gather/scatter work only once for the top-level expression. This way all the subexpressions can work on a (gathered) continuous input.

An alternative strategy (2) could be: every expression that is not selection vector aware (not counting its subexpressions), does the gather/scatter itself. This way every expression itself maintains the input/output shape the same as the original input.

Given that for a long time (possibly forever), there will be much less selection vector aware kernels than the other, it's probably worth it to use 1 instead of 2, as the gather/scatter only happen once for the whole expression.

return ExecuteScalarExpressionWithSelSlowPath(expr, input, exec_context);
}

if (auto lit = expr.literal()) return *lit;

if (auto param = expr.parameter()) {
Expand All @@ -757,44 +805,20 @@ Result<Datum> ExecuteScalarExpression(const Expression& expr, const ExecBatch& i

auto call = CallNotNull(expr);

std::vector<Datum> arguments(call->arguments.size());

bool all_scalar = true;
for (size_t i = 0; i < arguments.size(); ++i) {
ARROW_ASSIGN_OR_RAISE(
arguments[i], ExecuteScalarExpression(call->arguments[i], input, exec_context));
if (arguments[i].is_array()) {
all_scalar = false;
}
}

int64_t input_length;
if (!arguments.empty() && all_scalar) {
// all inputs are scalar, so use a 1-long batch to avoid
// computing input.length equivalent outputs
input_length = 1;
if (call->is_special_form) {
// Let the special form take over the execution using its own logic of argument
// evaluation.
DCHECK(call->special_form);
return call->special_form->Execute(*call, input, exec_context);
} else {
input_length = input.length;
// Eagerly evaluate all the arguments.
std::vector<Datum> arguments(call->arguments.size());
for (size_t i = 0; i < arguments.size(); ++i) {
ARROW_ASSIGN_OR_RAISE(
arguments[i], ExecuteScalarExpression(call->arguments[i], input, exec_context));
}
return ExecuteCallNonRecursive(*call, input, arguments, exec_context);
}

auto executor = compute::detail::KernelExecutor::MakeScalar();

compute::KernelContext kernel_context(exec_context, call->kernel);
kernel_context.SetState(call->kernel_state.get());

const Kernel* kernel = call->kernel;
std::vector<TypeHolder> types = GetTypes(arguments);
auto options = call->options.get();
RETURN_NOT_OK(executor->Init(&kernel_context, {kernel, types, options}));

compute::detail::DatumAccumulator listener;
RETURN_NOT_OK(
executor->Execute(ExecBatch(std::move(arguments), input_length), &listener));
const auto out = executor->WrapResults(arguments, listener.values());
#ifndef NDEBUG
DCHECK_OK(executor->CheckResultType(out, call->function_name.c_str()));
#endif
return out;
}

namespace {
Expand Down
21 changes: 17 additions & 4 deletions cpp/src/arrow/compute/expression.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class ARROW_EXPORT Expression {
std::string function_name;
std::vector<Expression> arguments;
std::shared_ptr<FunctionOptions> options;
// Whether this call is a special form (e.g. if-else). If true, the `special_form`
// field will be resolved in binding.
bool is_special_form = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A special-form is not a call. A special-form is a special form of expression.

-  using Impl = std::variant<Datum, Parameter, Call>;
+  using Impl = std::variant<Datum, Parameter, Call, Special>;

This is fundamental to the approach. Special forms don't have names, they are virtual sub-classes of Special.

We can start with CondSpecial which can model if-else and case-when based on number of conditions and branches used to construct it.

selection_vector_aware is not a member variable, it's something resolved at evaluation time by traversing the expression.

// Cached hash value
size_t hash;

Expand All @@ -56,6 +59,9 @@ class ARROW_EXPORT Expression {
const Kernel* kernel = NULLPTR;
std::shared_ptr<KernelState> kernel_state;
TypeHolder type;
// Whether the entire call (including all its arguments) is selection-vector-aware
bool selection_vector_aware = false;
std::shared_ptr<SpecialForm> special_form = NULLPTR;
Comment on lines +63 to +64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would selection_vector_aware defined in kernel?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, a kernel will claim itself as selection_vector_aware here: https://github.com/apache/arrow/pull/42106/files#diff-c275e8c64a3935e84eaf970584e61b0d3db88fa5b052965505edf8126526c80eR527-R532
if it has special execution path to deal with the given selection vector directly.


void ComputeHash();
};
Expand Down Expand Up @@ -118,6 +124,12 @@ class ARROW_EXPORT Expression {
// XXX someday
// NullGeneralization::type nullable() const;

/// Whether the entire expression (including all its subexpressions) is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is only something you will discover during evaluation. The same function can have kernels that are aware of selection and some that are not. Another important aspect is the type-checking of the special forms. You need to unify [1] the output type of all the branches, so you can pre-allocate the output and introduce the appropriate casts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is only something you will discover during evaluation.

I don't think it's necessarily during evaluation. This can be done in binding.

The same function can have kernels that are aware of selection and some that are not.

Binding will resolve the function to concrete kernel, so the selection awareness is known once the kernel is resolved. Also note that I made selection aware flag a property of kernel rather than of function.

Another important aspect is the type-checking of the special forms. You need to unify [1] the output type of all the branches, so you can pre-allocate the output and introduce the appropriate casts.

(I don't think this is related to selection-awareness. In this proposal, special forms and expression's selection-awareness are relatively separated - they only correlate on the fact that a special form may use selection vector to mask the evaluation of its subexpressions. So I'm assuming this is a general comment about special form.)

Before responding to your comment about special forms, I want to raise some other discussion which is part of my reasons making this proposal the current way.

In arrow compute, there are "function"s, which users can directly invoke via CallFunction, and there are expressions, one of whose concrete type is a "call", which further leverages the "function" when evaluating. Now take "if_else" as an example. There already exists an "if_else" function, which makes prefect sense because users do need it for two-way branching on a triple of {condition vector, true branch vector, false branch vector}. For expression, "if_else", as a regular function, is represented as a call. This is fine in terms of lexical/grammatical aspects, type checking, kernel resolution, etc., except that "if_else" requires special argument evaluation rules other than what a call does (eagerly evaluating all its arguments) - the reason why we need "if_else" (or "cond") special form.

So my question is, can we assume that for every coming special form in arrow, there exists a function "companion" of it? For some common special forms like "and", "or", "if_else", "case_when", there already are. For other special forms in Lisp, like "quote", "let", "defunc", they don't have corresponding "function"s, but those special forms themselves are more like fundamental parts, like defining variable and assignments, for a Turing-complete language, which I believe arrow won't need to have. So my so-far conclusion is, yes, we can assume it.

Then the next question is, given that we'll have the function companion for every special form, can we leverage the function companion to do type-checking/resolution (the function need to do those anyway)? My feeling is yes, at least I can't think of a case that a special form has different type-checking/resolution rule than its function companion.

So to summarize, if a special form:

  1. Always has a function companion;
  2. Is type-checked/resolved the same as its function companion;
  3. Evaluates the same as its function companion - I mean the evaluation for the last kernel invocation after all its arguments properly evaluated;
  4. Only differs a regular call on the arguments evaluation.

Then we can think of special forms to be a special call with special argument evaluation rules.

Of course, if there are counterexamples for any of above assumptions, then my conclusion would be wrong. And I should take the way around. Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So my question is, can we assume that for every coming special form in arrow, there exists a function "companion" of it? For some common special forms like "and", "or", "if_else", "case_when", there already are. For other special forms in Lisp, like "quote", "let", "defunc", they don't have corresponding "function"s, but those special forms themselves are more like fundamental parts, like defining variable and assignments, for a Turing-complete language, which I believe arrow won't need to have. So my so-far conclusion is, yes, we can assume it.

Special forms in Arrow won't be many and we don't have to store them in a table like we have to do for functions. My use of LISP for inspiration might have led you into a path of over-engineering. We don't need the full generality of LISP or languages where users can define functions with call-by-name parameters. Think more C-like (strict evaluation) and less LISP-like (quote, let, defunc...) because that is overkill. Hence my insistence on making special forms a well-defined set of C++ classes.

/// selection-vector-aware. If true, then the expression can be executed using the "fast
/// path" - all kernels directly working on the selection vector. Otherwise the
/// execution takes the "slow path" - gathering the input and scattering the output.
bool selection_vector_aware() const;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this should return the TypeMatcher or the DataType or be a bool MatchesSelectionVector(TypeHolder/DataType) that is derived from what KernelSignatures contain to let the evaluation system check which kinds of selection vectors they accept (INT32, INT64 or BOOL).

So you can extend KernelSignature to have a new type matcher (matches nothing by default) that can match selection vector types.


struct Parameter {
FieldRef ref;

Expand Down Expand Up @@ -159,14 +171,15 @@ Expression field_ref(FieldRef ref);

ARROW_EXPORT
Expression call(std::string function, std::vector<Expression> arguments,
std::shared_ptr<FunctionOptions> options = NULLPTR);
std::shared_ptr<FunctionOptions> options = NULLPTR,
bool is_special_form = false);

template <typename Options, typename = typename std::enable_if<
std::is_base_of<FunctionOptions, Options>::value>::type>
Expression call(std::string function, std::vector<Expression> arguments,
Options options) {
Expression call(std::string function, std::vector<Expression> arguments, Options options,
bool is_special_form = false) {
Comment on lines +179 to +180
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Special forms are not "called", they are evaluated. They are more general expressions than calls. if (true) f(x) is equivalent to a call to f with x that you discover while trying to evaluate the conditional.

return call(std::move(function), std::move(arguments),
std::make_shared<Options>(std::move(options)));
std::make_shared<Options>(std::move(options)), is_special_form);
}

/// Assemble a list of all fields referenced by an Expression at any depth.
Expand Down
37 changes: 37 additions & 0 deletions cpp/src/arrow/compute/expression_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "arrow/compute/api_scalar.h"
#include "arrow/compute/cast.h"
#include "arrow/compute/cast_internal.h"
#include "arrow/compute/exec_internal.h"
#include "arrow/compute/registry.h"
#include "arrow/record_batch.h"
#include "arrow/table.h"
Expand Down Expand Up @@ -290,5 +291,41 @@ inline Result<std::shared_ptr<compute::Function>> GetFunction(
return GetCastFunction(*to_type);
}

// Execute a call expression with all arguments already evaluated.
inline Result<Datum> ExecuteCallNonRecursive(const Expression::Call& call,
const ExecBatch& input,
const std::vector<Datum>& arguments,
ExecContext* exec_context) {
auto executor = compute::detail::KernelExecutor::MakeScalar();

compute::KernelContext kernel_context(exec_context, call.kernel);
kernel_context.SetState(call.kernel_state.get());

const Kernel* kernel = call.kernel;
std::vector<TypeHolder> types = GetTypes(arguments);
auto options = call.options.get();
RETURN_NOT_OK(executor->Init(&kernel_context, {kernel, types, options}));

bool all_scalar = std::all_of(arguments.begin(), arguments.end(),
[](const Datum& value) { return value.is_scalar(); });
int64_t input_length;
if (!arguments.empty() && all_scalar) {
// all inputs are scalar, so use a 1-long batch to avoid
// computing input.length equivalent outputs
input_length = 1;
} else {
input_length = input.length;
}
ExecBatch arguments_batch(std::move(arguments), input_length);
arguments_batch.selection_vector = input.selection_vector;
compute::detail::DatumAccumulator listener;
RETURN_NOT_OK(executor->Execute(std::move(arguments_batch), &listener));
const auto out = executor->WrapResults(arguments, listener.values());
#ifndef NDEBUG
DCHECK_OK(executor->CheckResultType(out, call.function_name.c_str()));
#endif
return out;
}

} // namespace compute
} // namespace arrow
27 changes: 27 additions & 0 deletions cpp/src/arrow/compute/expression_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include "arrow/compute/registry.h"
#include "arrow/testing/gtest_util.h"
#include "arrow/testing/matchers.h"
#include "arrow/util/vector.h"

using testing::Eq;
using testing::HasSubstr;
Expand Down Expand Up @@ -1775,5 +1776,31 @@ TEST(Projection, AugmentWithKnownValues) {
}));
}

TEST(Expression, IfElseSpecialForm) {
const std::shared_ptr<Schema> input_schema =
schema({field("a", int64()), field("b", int64()), field("c", int64()),
field("d", int64())});
auto input = RecordBatchFromJSON(input_schema, R"([
{"a": 0, "b": -1, "c": 0, "d": -1},
{"a": 1, "b": -1, "c": 0, "d": -1},
{"a": 0, "b": -1, "c": 1, "d": -1},
{"a": 1, "b": -1, "c": 1, "d": -1}
])");
auto a = field_ref("a");
auto b = field_ref("b");
auto c = field_ref("c");
auto d = field_ref("d");
auto zero = literal(0ll);
auto one = literal(1ll);
auto a_not_zero = not_equal(a, zero);
auto one_div_a = call("divide", {one, a});
auto if_else_inner = call("if_else", {a_not_zero, one_div_a, zero}, nullptr, true);
auto if_else_inner_not_zero = not_equal(if_else_inner, zero);
auto if_else_outer =
call("if_else", {if_else_inner_not_zero, one_div_a, zero}, nullptr, true);
ASSERT_OK_AND_ASSIGN(auto expr, if_else_outer.Bind(*input_schema));
ASSERT_OK_AND_ASSIGN(auto result, ExecuteScalarExpression(expr, *input_schema, input));
}

} // namespace compute
} // namespace arrow
Loading