Skip to content

Commit 2c3b545

Browse files
craig[bot]rafiss
andcommitted
157031: schemachanger/rel: fix notJoin subquery depth calculation for non-entity variables r=rafiss a=rafiss Previously, the subquery depth calculation in setSubqueryDepth only considered direct entity dependencies when determining when a notJoin subquery should execute. It tracked the maximum entity slot among the subquery's inputs but ignored when non-entity variables (like table-id, index-id) were actually bound. This caused notJoin subqueries to execute too early, before all their required variables were available, leading to "unbound variable" errors. This commit fixes the depth calculation by tracking which entity provides each variable through the facts. When a subquery needs a non-entity variable, we now find which entity binds that variable and use that entity's slot to determine the correct execution depth. This ensures notJoin subqueries only execute after all their input variables have been bound by the appropriate entities in the join order. The fix is needed for rules that pass non-entity variables to notJoin predicates, particularly in constraint rename scenarios where variables may be bound at different join depths. (At the time of this writing, we don't yet have rules like this, but we soon will add one.) informs cockroachdb#148341 Release note: None Co-authored-by: Rafi Shamim <[email protected]>
2 parents 8c97d77 + 74753fd commit 2c3b545

File tree

3 files changed

+213
-5
lines changed

3 files changed

+213
-5
lines changed

pkg/sql/schemachanger/rel/query_build.go

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -427,19 +427,41 @@ func (p *queryBuilder) setSubQueryDepths(entitySlots []slotIdx) {
427427
}
428428

429429
func (p *queryBuilder) setSubqueryDepth(s *subQuery, entitySlots []slotIdx) {
430-
var max int
430+
// First, figure out which entity binds each variable.
431+
// slotToEntityProvider maps slot -> entity slot that provides it.
432+
slotToEntityProvider := make(map[int]int)
433+
for _, f := range p.facts {
434+
if p.slotIsEntity[f.variable] {
435+
// This fact is about an entity, so it might bind a variable
436+
slotToEntityProvider[int(f.value)] = int(f.variable)
437+
}
438+
}
439+
440+
var maxEntitySlot int
431441
s.inputSlotMappings.ForEach(func(key, _ int) {
432-
if p.slotIsEntity[key] && key > max {
433-
max = key
442+
if p.slotIsEntity[key] {
443+
if key > maxEntitySlot {
444+
maxEntitySlot = key
445+
}
446+
} else {
447+
// For non-entity variables, find which entity provides them
448+
provider, hasProvider := slotToEntityProvider[key]
449+
if hasProvider {
450+
if provider > maxEntitySlot {
451+
maxEntitySlot = provider
452+
}
453+
}
434454
}
435455
})
456+
436457
got := sort.Search(len(entitySlots), func(i int) bool {
437-
return int(entitySlots[i]) >= max
458+
return int(entitySlots[i]) >= maxEntitySlot
438459
})
439460
if got == len(entitySlots) {
440461
panic(errors.AssertionFailedf("failed to find maximum entity in entitySlots: %v not in %v",
441-
max, entitySlots))
462+
maxEntitySlot, entitySlots))
442463
}
464+
443465
s.depth = queryDepth(got + 1)
444466
}
445467

pkg/sql/schemachanger/rel/query_eval.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ func (ec *evalContext) maybeVisitSubqueries() (nextSubQuery int, done bool, erro
450450
func (ec *evalContext) visitSubquery(query int) (done bool, _ error) {
451451
sub := ec.q.notJoins[query]
452452
sec := sub.query.getEvalContext()
453+
453454
defer sub.query.putEvalContext(sec)
454455
defer func() { // reset the slots populated to run the subquery
455456
sub.inputSlotMappings.ForEach(func(_, subSlot int) {

pkg/sql/schemachanger/rel/rel_test.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)