1919 resolve_expression ,
2020)
2121from substrait .extension_registry import ExtensionRegistry
22- from substrait .type_inference import infer_plan_schema , join_output_names
22+ from substrait .type_inference import (
23+ _join_output_struct ,
24+ infer_plan_schema ,
25+ join_output_names ,
26+ )
2327from substrait .utils import (
2428 merge_extension_declarations ,
2529 merge_extension_urns ,
@@ -487,6 +491,7 @@ def resolve(registry: ExtensionRegistry) -> stp.Plan:
487491 left_ns = infer_plan_schema (bound_left , registry = registry )
488492 right_ns = infer_plan_schema (bound_right , registry = registry )
489493
494+ # The join condition binds against the combined left+right schema.
490495 ns = stt .NamedStruct (
491496 struct = stt .Type .Struct (
492497 types = list (left_ns .struct .types ) + list (right_ns .struct .types ),
@@ -497,11 +502,28 @@ def resolve(registry: ExtensionRegistry) -> stp.Plan:
497502 bound_expression : stee .ExtendedExpression = resolve_expression (
498503 expression , ns , registry
499504 )
500- bound_post = (
501- resolve_expression (post_join_filter , ns , registry )
502- if post_join_filter is not None
503- else None
504- )
505+
506+ # The output names must match the columns the join type actually emits
507+ # (semi/anti drop a side, mark appends a boolean).
508+ type_name = stalg .JoinRel .JoinType .Name (type )
509+ out_names = join_output_names (type_name , left_ns .names , right_ns .names )
510+
511+ # post_join_filter is applied to each output record after
512+ # join-type-specific output formation (semantically a FilterRel above the
513+ # join), so it resolves against the output schema -- which for semi/anti
514+ # joins is a single side, not the combined schema.
515+ bound_post = None
516+ if post_join_filter is not None :
517+ output_ns = stt .NamedStruct (
518+ names = out_names ,
519+ struct = _join_output_struct (
520+ type_name ,
521+ bound_left .relations [- 1 ].root .input ,
522+ bound_right .relations [- 1 ].root .input ,
523+ registry = registry ,
524+ ),
525+ )
526+ bound_post = resolve_expression (post_join_filter , output_ns , registry )
505527
506528 rel = stalg .Rel (
507529 join = stalg .JoinRel (
@@ -516,12 +538,6 @@ def resolve(registry: ExtensionRegistry) -> stp.Plan:
516538 )
517539 )
518540
519- # The join condition resolves against the combined left+right schema, but
520- # the output names must match the columns the join type actually emits
521- # (semi/anti drop a side, mark appends a boolean).
522- out_names = join_output_names (
523- stalg .JoinRel .JoinType .Name (type ), left_ns .names , right_ns .names
524- )
525541 return stp .Plan (
526542 version = default_version ,
527543 relations = [stp .PlanRel (root = stalg .RelRoot (input = rel , names = out_names ))],
@@ -1023,6 +1039,9 @@ def builder(
10231039 left_keys : Iterable [Union [str , int ]],
10241040 right_keys : Iterable [Union [str , int ]],
10251041 type ,
1042+ * ,
1043+ post_join_filter : Optional [ExtendedExpressionOrUnbound ] = None ,
1044+ residual_expression : Optional [ExtendedExpressionOrUnbound ] = None ,
10261045 extension : Optional [AdvancedExtension ] = None ,
10271046 ) -> UnboundPlan :
10281047 def resolve (registry : ExtensionRegistry ) -> stp .Plan :
@@ -1033,24 +1052,65 @@ def resolve(registry: ExtensionRegistry) -> stp.Plan:
10331052 keys = _comparison_join_keys (
10341053 list (left_keys ), list (right_keys ), left_ns , right_ns , registry
10351054 )
1036- names = join_output_names (
1037- rel_cls .JoinType .Name (type ), left_ns .names , right_ns .names
1038- )
1055+ type_name = rel_cls .JoinType .Name (type )
1056+ names = join_output_names (type_name , left_ns .names , right_ns .names )
1057+
1058+ # post_join_filter is applied to each output record after
1059+ # join-type-specific output formation (semantically a FilterRel above
1060+ # the join), so it resolves against the output schema -- which for
1061+ # semi/anti joins is a single side. residual_expression is evaluated
1062+ # on each candidate key-match (both rows present), so it resolves
1063+ # against the combined left+right schema. Each is built only when the
1064+ # corresponding predicate is supplied.
1065+ bound_post = None
1066+ if post_join_filter is not None :
1067+ output_ns = stt .NamedStruct (
1068+ names = names ,
1069+ struct = _join_output_struct (
1070+ type_name ,
1071+ bound_left .relations [- 1 ].root .input ,
1072+ bound_right .relations [- 1 ].root .input ,
1073+ registry = registry ,
1074+ ),
1075+ )
1076+ bound_post = resolve_expression (post_join_filter , output_ns , registry )
1077+
1078+ bound_residual = None
1079+ if residual_expression is not None :
1080+ combined_ns = stt .NamedStruct (
1081+ struct = stt .Type .Struct (
1082+ types = list (left_ns .struct .types ) + list (right_ns .struct .types ),
1083+ nullability = stt .Type .Nullability .NULLABILITY_REQUIRED ,
1084+ ),
1085+ names = list (left_ns .names ) + list (right_ns .names ),
1086+ )
1087+ bound_residual = resolve_expression (
1088+ residual_expression , combined_ns , registry
1089+ )
1090+
10391091 rel = stalg .Rel (
10401092 ** {
10411093 rel_name : rel_cls (
10421094 left = bound_left .relations [- 1 ].root .input ,
10431095 right = bound_right .relations [- 1 ].root .input ,
10441096 keys = keys ,
10451097 type = type ,
1098+ post_join_filter = bound_post .referred_expr [0 ].expression
1099+ if bound_post
1100+ else None ,
1101+ residual_expression = bound_residual .referred_expr [0 ].expression
1102+ if bound_residual
1103+ else None ,
10461104 advanced_extension = extension ,
10471105 )
10481106 }
10491107 )
10501108 return stp .Plan (
10511109 version = default_version ,
10521110 relations = [stp .PlanRel (root = stalg .RelRoot (input = rel , names = names ))],
1053- ** _merge_plan_metadata (bound_left , bound_right ),
1111+ ** _merge_plan_metadata (
1112+ bound_left , bound_right , bound_post , bound_residual
1113+ ),
10541114 )
10551115
10561116 return resolve
0 commit comments