@@ -577,3 +577,188 @@ func TestConcurrentQueryInDifferentDatabases(t *testing.T) {
577577 }
578578 require .NoError (t , g .Wait ())
579579}
580+
581+ type notJoinTestAttr string
582+
583+ func (a notJoinTestAttr ) String () string { return string (a ) }
584+
585+ // TestNotJoinSubqueryDepthWithNonEntityVariables tests that notJoin subqueries
586+ // execute at the correct depth when they depend on non-entity variables that
587+ // are bound by entities at different join depths.
588+ //
589+ // This test catches a bug where notJoin subqueries would execute too early,
590+ // before their required non-entity variables were bound. The fix ensures that
591+ // we track which entity provides each variable through facts, so notJoin
592+ // subqueries wait until all their input variables are available.
593+ func TestNotJoinSubqueryDepthWithNonEntityVariables (t * testing.T ) {
594+ defer leaktest .AfterTest (t )()
595+
596+ type FirstEntity struct {
597+ ID int
598+ SharedID int
599+ }
600+ type SecondEntity struct {
601+ ID int
602+ SharedID int
603+ Value string
604+ }
605+ type ThirdEntity struct {
606+ ID int
607+ SharedID int
608+ Flag int
609+ }
610+
611+ schema := rel .MustSchema ("test_notjoin_depth" ,
612+ rel .EntityMapping (reflect .TypeOf ((* FirstEntity )(nil )),
613+ rel .EntityAttr (notJoinTestAttr ("id" ), "ID" ),
614+ rel .EntityAttr (notJoinTestAttr ("shared_id" ), "SharedID" ),
615+ ),
616+ rel .EntityMapping (reflect .TypeOf ((* SecondEntity )(nil )),
617+ rel .EntityAttr (notJoinTestAttr ("id" ), "ID" ),
618+ rel .EntityAttr (notJoinTestAttr ("shared_id" ), "SharedID" ),
619+ rel .EntityAttr (notJoinTestAttr ("value" ), "Value" ),
620+ ),
621+ rel .EntityMapping (reflect .TypeOf ((* ThirdEntity )(nil )),
622+ rel .EntityAttr (notJoinTestAttr ("id" ), "ID" ),
623+ rel .EntityAttr (notJoinTestAttr ("shared_id" ), "SharedID" ),
624+ rel .EntityAttr (notJoinTestAttr ("flag" ), "Flag" ),
625+ ),
626+ )
627+
628+ // Define a notJoin rule that depends on a non-entity variable (shared_id).
629+ // This rule checks if there's no ThirdEntity with the given shared_id and flag=1.
630+ noThirdWithFlag := schema .DefNotJoin1 ("no_third_with_flag" , "shared_id_var" , func (
631+ sharedIDVar rel.Var ,
632+ ) rel.Clauses {
633+ return rel.Clauses {
634+ rel .Var ("third" ).Type ((* ThirdEntity )(nil )),
635+ rel .Var ("third" ).AttrEqVar (notJoinTestAttr ("shared_id" ), sharedIDVar ),
636+ rel .Var ("third" ).AttrEq (notJoinTestAttr ("flag" ), 1 ),
637+ }
638+ })
639+
640+ first1 := & FirstEntity {ID : 1 , SharedID : 100 }
641+ second1 := & SecondEntity {ID : 2 , SharedID : 100 , Value : "test" }
642+ third1 := & ThirdEntity {ID : 3 , SharedID : 100 , Flag : 0 }
643+ third2 := & ThirdEntity {ID : 4 , SharedID : 200 , Flag : 1 }
644+
645+ db , err := rel .NewDatabase (schema ,
646+ rel.Index {Attrs : []rel.Attr {rel .Type }},
647+ rel.Index {Attrs : []rel.Attr {rel .Self }},
648+ rel.Index {Attrs : []rel.Attr {notJoinTestAttr ("id" )}},
649+ rel.Index {Attrs : []rel.Attr {notJoinTestAttr ("shared_id" )}},
650+ rel.Index {Attrs : []rel.Attr {notJoinTestAttr ("flag" )}},
651+ rel.Index {Attrs : []rel.Attr {notJoinTestAttr ("value" )}},
652+ )
653+ require .NoError (t , err )
654+ require .NoError (t , db .Insert (first1 ))
655+ require .NoError (t , db .Insert (second1 ))
656+ require .NoError (t , db .Insert (third1 ))
657+ require .NoError (t , db .Insert (third2 ))
658+
659+ // Test case 1: Query where shared_id is bound by SecondEntity (at depth 2).
660+ // The notJoin should execute after SecondEntity is joined.
661+ t .Run ("notjoin_executes_after_second_entity" , func (t * testing.T ) {
662+ q , err := rel .NewQuery (schema ,
663+ // FirstEntity is joined first (depth 1).
664+ rel .Var ("first" ).Type ((* FirstEntity )(nil )),
665+ rel .Var ("first" ).AttrEq (notJoinTestAttr ("id" ), 1 ),
666+ // SecondEntity is joined second (depth 2) and binds shared_id_var.
667+ rel .Var ("second" ).Type ((* SecondEntity )(nil )),
668+ rel .Var ("second" ).AttrEqVar (notJoinTestAttr ("shared_id" ), "shared_id_var" ),
669+ // Join FirstEntity and SecondEntity on shared_id.
670+ rel .Var ("first" ).AttrEqVar (notJoinTestAttr ("shared_id" ), "shared_id_var" ),
671+ // This notJoin depends on shared_id_var, which is bound by SecondEntity
672+ // It should execute at depth 2 or later, not before
673+ noThirdWithFlag ("shared_id_var" ),
674+ )
675+ require .NoError (t , err )
676+
677+ var results [][]interface {}
678+ err = q .Iterate (db , nil , func (r rel.Result ) error {
679+ results = append (results , []interface {}{
680+ r .Var (rel .Var ("first" )),
681+ r .Var (rel .Var ("second" )),
682+ r .Var (rel .Var ("shared_id_var" )),
683+ })
684+ return nil
685+ })
686+ require .NoError (t , err )
687+ // Should find the combination where shared_id=100.
688+ require .Len (t , results , 1 )
689+ require .Equal (t , first1 , results [0 ][0 ])
690+ require .Equal (t , second1 , results [0 ][1 ])
691+ require .Equal (t , 100 , results [0 ][2 ])
692+ })
693+
694+ // Test case 2: Query where shared_id would cause the notJoin to fail.
695+ t .Run ("notjoin_filters_results_correctly" , func (t * testing.T ) {
696+ // Add a FirstEntity with shared_id=200.
697+ first2 := & FirstEntity {ID : 5 , SharedID : 200 }
698+ second2 := & SecondEntity {ID : 6 , SharedID : 200 , Value : "test2" }
699+ require .NoError (t , db .Insert (first2 ))
700+ require .NoError (t , db .Insert (second2 ))
701+
702+ q , err := rel .NewQuery (schema ,
703+ rel .Var ("first" ).Type ((* FirstEntity )(nil )),
704+ rel .Var ("first" ).AttrEq (notJoinTestAttr ("id" ), 5 ),
705+ rel .Var ("second" ).Type ((* SecondEntity )(nil )),
706+ rel .Var ("second" ).AttrEqVar (notJoinTestAttr ("shared_id" ), "shared_id_var" ),
707+ rel .Var ("first" ).AttrEqVar (notJoinTestAttr ("shared_id" ), "shared_id_var" ),
708+ noThirdWithFlag ("shared_id_var" ),
709+ )
710+ require .NoError (t , err )
711+
712+ var results [][]interface {}
713+ err = q .Iterate (db , nil , func (r rel.Result ) error {
714+ results = append (results , []interface {}{
715+ r .Var (rel .Var ("first" )),
716+ r .Var (rel .Var ("second" )),
717+ })
718+ return nil
719+ })
720+ require .NoError (t , err )
721+ // Should find no results because third2 has shared_id=200 and flag=1.
722+ require .Empty (t , results )
723+ })
724+
725+ // Test case 3: Complex case with non-entity variable bound at depth 3.
726+ t .Run ("notjoin_with_variable_bound_at_depth_3" , func (t * testing.T ) {
727+ // Define a more complex notJoin that uses two variables.
728+ complexNotJoin := schema .DefNotJoin2 ("complex_not_join" , "sid" , "val" , func (
729+ sidVar , valVar rel.Var ,
730+ ) rel.Clauses {
731+ return rel.Clauses {
732+ rel .Var ("e" ).Type ((* SecondEntity )(nil )),
733+ rel .Var ("e" ).AttrEqVar (notJoinTestAttr ("shared_id" ), sidVar ),
734+ rel .Var ("e" ).AttrEqVar (notJoinTestAttr ("value" ), valVar ),
735+ }
736+ })
737+
738+ q , err := rel .NewQuery (schema ,
739+ // Depth 1
740+ rel .Var ("f1" ).Type ((* FirstEntity )(nil )),
741+ // Depth 2
742+ rel .Var ("f2" ).Type ((* FirstEntity )(nil )),
743+ rel .Var ("f2" ).AttrNeq (notJoinTestAttr ("id" ), 1 ),
744+ // Depth 3 - this is where shared_id and value are bound.
745+ rel .Var ("s" ).Type ((* SecondEntity )(nil )),
746+ rel .Var ("s" ).AttrEqVar (notJoinTestAttr ("shared_id" ), "sid" ),
747+ rel .Var ("s" ).AttrEqVar (notJoinTestAttr ("value" ), "val" ),
748+ // The notJoin should only execute after depth 3.
749+ complexNotJoin ("sid" , "val" ),
750+ // Add a filter to limit results.
751+ rel .Filter ("limit" , "f1" )(func (e * FirstEntity ) bool {
752+ return e .ID == 1
753+ }),
754+ )
755+ require .NoError (t , err )
756+
757+ // This should execute without "unbound variable" errors
758+ // even though the notJoin depends on variables bound at depth 3
759+ err = q .Iterate (db , nil , func (r rel.Result ) error {
760+ return nil
761+ })
762+ require .NoError (t , err )
763+ })
764+ }
0 commit comments