{"record":{"id":"1bd8eef33242889d","repo":"pola-rs/polars","slug":"join-where-requires-at-least-one-predicate","errorCode":null,"errorMessage":"'join_where' requires at least one predicate","messagePattern":"'join_where' requires at least one predicate","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/polars-plan/src/plans/conversion/dsl_to_ir/join.rs","lineNumber":557,"sourceCode":"\n            last_node = ctxt.lp_arena.add(ir);\n        }\n    } else {\n        // For left and right joins, we cannot lower to cross + filters\n        // as null outputs for missing rows would not be preserved.\n        // We attach the join predicates/conditions to the joins itself\n        // and restore the original `how` join type.\n        let node = resolved\n            .iter()\n            .map(|e| e.node())\n            .reduce(|left, right| {\n                ctxt.expr_arena.add(AExpr::BinaryExpr {\n                    left,\n                    op: Operator::And,\n                    right,\n                })\n            })\n            .expect(\"'join_where' requires at least one predicate\");\n        let predicate = ExprIR::from_node(node, ctxt.expr_arena);\n\n        let IR::Join { options, .. } = ctxt.lp_arena.get(join_node) else {\n            unreachable!()\n        };\n        let mut new_options = (**options).clone();\n        new_options.args.how = how;\n        new_options.options = JoinTypeOptionsIR::CrossAndFilter { predicate };\n\n        let IR::Join { options, .. } = ctxt.lp_arena.get_mut(join_node) else {\n            unreachable!()\n        };\n        *options = Arc::new(new_options);\n    }\n\n    ctxt.conversion_optimizer\n        .optimize_exprs(ctxt.expr_arena, ctxt.lp_arena, last_node, false)\n        .context(\"'join_where' failed\")?;","sourceCodeStart":539,"sourceCodeEnd":575,"githubUrl":"https://github.com/pola-rs/polars/blob/68506541d2de983056c9eb244e1ea05fab377dfc/crates/polars-plan/src/plans/conversion/dsl_to_ir/join.rs#L539-L575","documentation":"polars raises this while converting a join_where (non-equi join) plan from DSL to IR: for left/right joins the engine must fold every predicate into a single AND-ed condition attached to the join node, and it does that with reduce().expect(...), which panics when the predicate list is empty (join.rs:544-554). It means join_where was called with zero predicates — the Python wrapper (LazyFrame.join_where at frame.py:6635) does not check emptiness because predicates are variadic, so the empty list reaches the Rust side unchecked. Note the asymmetry: an inner join_where with zero predicates silently degenerates to a cross join (inner lowers to cross + per-predicate filters), so this panic is specific to how='left'/'right'. In Python it surfaces as polars.exceptions.PanicException at plan-resolution time (collect/explain), not at the call site.","triggerScenarios":"Calling lf.join_where(other) with no predicate expressions; splatting a possibly-empty list, lf.join_where(other, *preds) where preds == []; passing a single empty iterable as the 'predicate' (parse_into_list_of_expressions flattens it to zero exprs); any of the above combined with how='left' or how='right' (the inner path never reaches this expect).","commonSituations":"Config- or user-driven predicate lists that filter down to empty before the call; refactors that move conditions out of join_where but keep the call site; intending an unconditional/cartesian join and reaching for join_where instead of join(how='cross'); tests with placeholder predicate lists that ship empty.","solutions":["Pass at least one boolean predicate over columns of both frames, e.g. pl.col('left_a') > pl.col('right_b').","If predicates are built dynamically, guard the call — raise, skip, or substitute a default predicate when the list is empty.","If you actually want all row combinations, use lf.join(other, how='cross') instead of join_where.","If how='left'/'right' was set by mistake, drop it — plain join_where defaults to an inner non-equi join and needs a condition regardless."],"exampleFix":"# before — panics with \"'join_where' requires at least one predicate\" when preds is empty\nresult = lf.join_where(other, *preds)\n\n# after — fail fast with a clear Python-side error\nif not preds:\n    msg = 'join_where produced no predicates'\n    raise ValueError(msg)\nresult = lf.join_where(other, *preds)","handlingStrategy":"validation","validationCode":"import polars as pl\n\ndef join_where_safe(lf: pl.LazyFrame, other: pl.LazyFrame, predicates: list[pl.Expr]) -> pl.LazyFrame:\n    if not predicates:\n        msg = \"'join_where' requires at least one predicate\"\n        raise ValueError(msg)\n    return lf.join_where(other, *predicates)","typeGuard":null,"tryCatchPattern":"from polars.exceptions import PanicException, InvalidOperationError\n\ntry:\n    out = lf.join_where(other, pl.col(\"a\") > pl.col(\"b\")).collect()\nexcept (PanicException, InvalidOperationError) as exc:\n    # plan-time failure: audit predicate list and dtypes, then re-issue\n    ...","preventionTips":["Never splat an unchecked list into join_where; assert it is non-empty first.","Expect join_where errors to surface at collect()/explain() time, not at the call site — validate early instead of catching late.","Use join(how='cross') for unconditional joins; join_where exists only for non-equi predicates.","Keep each predicate a boolean expression over columns of the two frames; combine ORs inside one expression."],"tags":["polars","rust","join","lazyframe","non-equi-join","python"],"backgroundTag":"missing-required-argument","analyzedSha":"68506541d2de983056c9eb244e1ea05fab377dfc","analyzedAt":"2026-08-23T02:54:19.138Z","contentChangedAt":"2026-08-23T02:54:19.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}