Simplify an `is_in` call against an inequality guarantee. We avoid the complexity of fully simplifying EQUAL comparisons to true literals (e.g., 'x is_in [1, 2, 3]' given the guarantee 'x = 2') due to potential complications with null matching behavior. This is ok for the predicate pushdown use case because the overall aim is to simplify to an unsatisfiable expression. \pre `is_in_call` is a cal
| 1255 | /// \pre `is_in_call` is a call to the `is_in` function |
| 1256 | /// \return a simplified expression, or nullopt if no simplification occurred |
| 1257 | static Result<std::optional<Expression>> SimplifyIsIn( |
| 1258 | const Inequality& guarantee, const Expression::Call* is_in_call) { |
| 1259 | DCHECK_EQ(is_in_call->function_name, "is_in"); |
| 1260 | |
| 1261 | auto options = checked_pointer_cast<SetLookupOptions>(is_in_call->options); |
| 1262 | |
| 1263 | // The maximum number of values in the is_in expression set of values |
| 1264 | // in order to use the simplification. |
| 1265 | // If the set is large there are performance implications, see: |
| 1266 | // https://github.com/apache/arrow/issues/46777 |
| 1267 | constexpr int16_t kIsInSimplificationMaxValueSet = 50; |
| 1268 | if (options->value_set.length() > kIsInSimplificationMaxValueSet) { |
| 1269 | return std::nullopt; |
| 1270 | } |
| 1271 | |
| 1272 | const auto& lhs = Comparison::StripOrderPreservingCasts(is_in_call->arguments[0]); |
| 1273 | if (!lhs.field_ref()) return std::nullopt; |
| 1274 | if (*lhs.field_ref() != guarantee.target) return std::nullopt; |
| 1275 | |
| 1276 | FilterOptions::NullSelectionBehavior null_selection{}; |
| 1277 | switch (options->null_matching_behavior) { |
| 1278 | case SetLookupOptions::MATCH: |
| 1279 | null_selection = |
| 1280 | guarantee.nullable ? FilterOptions::EMIT_NULL : FilterOptions::DROP; |
| 1281 | break; |
| 1282 | case SetLookupOptions::SKIP: |
| 1283 | null_selection = FilterOptions::DROP; |
| 1284 | break; |
| 1285 | case SetLookupOptions::EMIT_NULL: |
| 1286 | if (guarantee.nullable) return std::nullopt; |
| 1287 | null_selection = FilterOptions::DROP; |
| 1288 | break; |
| 1289 | case SetLookupOptions::INCONCLUSIVE: |
| 1290 | if (guarantee.nullable) return std::nullopt; |
| 1291 | ARROW_ASSIGN_OR_RAISE(Datum is_null, IsNull(options->value_set)); |
| 1292 | ARROW_ASSIGN_OR_RAISE(Datum any_null, Any(is_null)); |
| 1293 | if (any_null.scalar_as<BooleanScalar>().value) return std::nullopt; |
| 1294 | null_selection = FilterOptions::DROP; |
| 1295 | break; |
| 1296 | } |
| 1297 | |
| 1298 | std::string func_name = Comparison::GetName(guarantee.cmp); |
| 1299 | DCHECK_NE(func_name, "na"); |
| 1300 | std::vector<Datum> args{options->value_set, guarantee.bound}; |
| 1301 | ARROW_ASSIGN_OR_RAISE(Datum filter_mask, CallFunction(func_name, args)); |
| 1302 | FilterOptions filter_options(null_selection); |
| 1303 | ARROW_ASSIGN_OR_RAISE(Datum simplified_value_set, |
| 1304 | Filter(options->value_set, filter_mask, filter_options)); |
| 1305 | |
| 1306 | if (simplified_value_set.length() == 0) return literal(false); |
| 1307 | if (simplified_value_set.length() == options->value_set.length()) return std::nullopt; |
| 1308 | |
| 1309 | ExecContext exec_context; |
| 1310 | Expression::Call simplified_call; |
| 1311 | simplified_call.function_name = "is_in"; |
| 1312 | simplified_call.arguments = is_in_call->arguments; |
| 1313 | simplified_call.options = std::make_shared<SetLookupOptions>( |
| 1314 | simplified_value_set, options->null_matching_behavior); |