Skip to content

Commit 5b5330b

Browse files
craig[bot]yuzefovich
andcommitted
Merge #155541
155541: stmtbundle: include skipped FKs into `schema.sql` r=yuzefovich a=yuzefovich Earlier this year we refactored how we handle FKs in stmt bundles. Namely, we now use IGNORE_FOREIGN_KEYS option of SHOW CREATE TABLE (which produces DDLs with FK constraints omitted) and then we include only "relevant" FKs as ALTER TABLE ... ADD CONSTRAINT stmts later. This behavior can be confusing since now CREATE TABLE stmt from the bundle cannot be relied upon as the source of truth for the table's schema. In order to partially remedy the situation, this commit extends the logic to also include ALTER TABLE stmts for "skipped" FKs where the origin (i.e. referencing) table is included into the bundle, but these ALTERs are commented out (to keep the bundle being recreatable). Informs: https://github.com/cockroachlabs/support/issues/3459 Epic: None Release note: None Co-authored-by: Yahor Yuzefovich <[email protected]>
2 parents 78a1681 + ad922c6 commit 5b5330b

File tree

4 files changed

+107
-38
lines changed

4 files changed

+107
-38
lines changed

pkg/sql/explain_bundle.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ func (b *stmtBundleBuilder) addEnv(ctx context.Context) {
599599
// update this logic to not include virtual tables into schema.sql but still
600600
// create stats files for them.
601601
var tables, sequences, views []tree.TableName
602-
var addFKs []*tree.AlterTable
602+
var addFKs, skipFKs []*tree.AlterTable
603603
err := b.db.Txn(ctx, func(ctx context.Context, txn *kv.Txn) error {
604604
// Catalog objects can show up multiple times in our lists, so
605605
// deduplicate them.
@@ -778,9 +778,13 @@ func (b *stmtBundleBuilder) addEnv(ctx context.Context) {
778778
include = hasDelete || hasUpdate || hasUpsert
779779
},
780780
)
781-
addFKs = opt.GetAllFKsAmongTables(refTables, func(t cat.Table) (tree.TableName, error) {
782-
return b.plan.catalog.fullyQualifiedNameWithTxn(ctx, t, txn)
783-
})
781+
addFKs, skipFKs = opt.GetAllFKs(
782+
ctx,
783+
b.plan.catalog,
784+
refTables,
785+
func(t cat.Table) (tree.TableName, error) {
786+
return b.plan.catalog.fullyQualifiedNameWithTxn(ctx, t, txn)
787+
})
784788
var err error
785789
tables, err = getNames(len(refTables), func(i int) cat.DataSource {
786790
return refTables[i]
@@ -890,6 +894,10 @@ func (b *stmtBundleBuilder) addEnv(ctx context.Context) {
890894
for _, addFK := range addFKs {
891895
fmt.Fprintf(&buf, "%s;\n", addFK)
892896
}
897+
// Include FK constraints that were skipped in commented out form.
898+
for _, skipFK := range skipFKs {
899+
fmt.Fprintf(&buf, "-- %s;\n", skipFK)
900+
}
893901
}
894902
for i := range views {
895903
blankLine()

pkg/sql/explain_bundle_test.go

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -536,9 +536,15 @@ CREATE TABLE users(id UUID DEFAULT gen_random_uuid() PRIMARY KEY, promo_id INT R
536536
r.Exec(t, "CREATE TABLE child2 (pk INT PRIMARY KEY, fk INT REFERENCES parent(pk));")
537537
r.Exec(t, "CREATE TABLE grandchild1 (pk INT PRIMARY KEY, fk INT REFERENCES child1(pk));")
538538
r.Exec(t, "CREATE TABLE grandchild2 (pk INT PRIMARY KEY, fk INT REFERENCES child2(pk));")
539+
getFK := func(table string) string {
540+
if table == "parent" {
541+
return ""
542+
}
543+
return fmt.Sprintf("ALTER TABLE defaultdb.public.%s ADD CONSTRAINT", table)
544+
}
539545
// Only the target tables should be included since we perform a
540546
// read-only stmt.
541-
getContentCheckFn := func(targetTableNames, targetFKs []string) func(name, contents string) error {
547+
getContentCheckFn := func(targetTableNames, addFKs, skipFKs []string) func(name, contents string) error {
542548
return func(name, contents string) error {
543549
if name == "schema.sql" {
544550
for _, targetTableName := range targetTableNames {
@@ -563,14 +569,27 @@ CREATE TABLE users(id UUID DEFAULT gen_random_uuid() PRIMARY KEY, promo_id INT R
563569
"unexpectedly found non-target table 'USE defaultdb;\nCREATE TABLE public.%s' in schema.sql:\n%s", tableName, contents)
564570
}
565571
}
566-
// Now confirm that only relevant FKs are included.
572+
// Sanity-check that all FKs in the output are either in the
573+
// "added" or "skipped" set.
567574
numFoundFKs := strings.Count(contents, "FOREIGN KEY")
568-
if numFoundFKs != len(targetFKs) {
569-
return errors.Newf("found %d FKs, expected %d\n%s", numFoundFKs, len(targetFKs), contents)
575+
if numFoundFKs != len(addFKs)+len(skipFKs) {
576+
return errors.Newf(
577+
"found %d FKs total whereas %d added and %d skipped were passed\n%s",
578+
numFoundFKs, len(addFKs), len(skipFKs), contents,
579+
)
580+
}
581+
// Now check that all expected added and skipped FKs are
582+
// present.
583+
for _, addFK := range addFKs {
584+
if !strings.Contains(contents, addFK) {
585+
return errors.Newf("didn't find added FK: %s\n%s", addFK, contents)
586+
} else if strings.Contains(contents, "-- "+addFK) {
587+
return errors.Newf("added FK shouldn't be commented out: %s\n%s", addFK, contents)
588+
}
570589
}
571-
for _, fk := range targetFKs {
572-
if !strings.Contains(contents, fk) {
573-
return errors.Newf("didn't find target FK: %s\n%s", fk, contents)
590+
for _, skipFK := range skipFKs {
591+
if !strings.Contains(contents, "-- "+skipFK) {
592+
return errors.Newf("didn't find skipped FK in commented out form: %s\n%s", skipFK, contents)
574593
}
575594
}
576595
}
@@ -580,8 +599,12 @@ CREATE TABLE users(id UUID DEFAULT gen_random_uuid() PRIMARY KEY, promo_id INT R
580599
// First read each table separately.
581600
for _, tableName := range tableNames {
582601
targetTableName := tableName
583-
// There should be no FKs included.
584-
contentCheck := getContentCheckFn([]string{targetTableName}, nil /* targetFKs */)
602+
// No FKs should be added, but 1 FK might be skipped.
603+
var skipFKs []string
604+
if skipFK := getFK(tableName); skipFK != "" {
605+
skipFKs = []string{skipFK}
606+
}
607+
contentCheck := getContentCheckFn([]string{targetTableName}, nil /* addFKS */, skipFKs)
585608
rows := r.QueryStr(t, "EXPLAIN ANALYZE (DEBUG) SELECT * FROM "+targetTableName)
586609
checkBundle(
587610
t, fmt.Sprint(rows), targetTableName, contentCheck, false, /* expectErrors */
@@ -590,26 +613,27 @@ CREATE TABLE users(id UUID DEFAULT gen_random_uuid() PRIMARY KEY, promo_id INT R
590613
)
591614
}
592615
// Now read different combinations of tables which will influence
593-
// whether ADD CONSTRAINT ... FOREIGN KEY statements are included.
594-
contentCheck := getContentCheckFn([]string{"parent", "child1"}, []string{"ALTER TABLE defaultdb.public.child1 ADD CONSTRAINT"})
616+
// whether ADD CONSTRAINT ... FOREIGN KEY statements are added or
617+
// skipped.
618+
contentCheck := getContentCheckFn([]string{"parent", "child1"}, []string{getFK("child1")}, nil)
595619
rows := r.QueryStr(t, "EXPLAIN ANALYZE (DEBUG) SELECT * FROM parent, child1")
596620
checkBundle(
597621
t, fmt.Sprint(rows), "parent", contentCheck, false, /* expectErrors */
598622
base, plans, "stats-defaultdb.public.parent.sql stats-defaultdb.public.child1.sql distsql.html vec.txt vec-v.txt",
599623
)
600624

601-
// There should be no FKs since there isn't a direct link between the
602-
// tables.
603-
contentCheck = getContentCheckFn([]string{"parent", "grandchild1"}, nil /* targetFKs */)
625+
// There should be no added FKs since there isn't a direct link between
626+
// the tables.
627+
contentCheck = getContentCheckFn([]string{"parent", "grandchild1"}, nil /* addFKS */, []string{getFK("grandchild1")})
604628
rows = r.QueryStr(t, "EXPLAIN ANALYZE (DEBUG) SELECT * FROM parent, grandchild1")
605629
checkBundle(
606630
t, fmt.Sprint(rows), "parent", contentCheck, false, /* expectErrors */
607631
base, plans, "stats-defaultdb.public.parent.sql stats-defaultdb.public.grandchild1.sql distsql.html vec.txt vec-v.txt",
608632
)
609633

610-
// Note that we omit the FK from grandchild1 since the FK referenced
634+
// Note that we skip the FK from grandchild1 since the FK referenced
611635
// table isn't being read.
612-
contentCheck = getContentCheckFn([]string{"parent", "child2", "grandchild1"}, []string{"ALTER TABLE defaultdb.public.child2 ADD CONSTRAINT"})
636+
contentCheck = getContentCheckFn([]string{"parent", "child2", "grandchild1"}, []string{getFK("child2")}, []string{getFK("grandchild1")})
613637
rows = r.QueryStr(t, "EXPLAIN ANALYZE (DEBUG) SELECT * FROM parent, child2, grandchild1")
614638
checkBundle(
615639
t, fmt.Sprint(rows), "parent", contentCheck, false, /* expectErrors */
@@ -618,10 +642,9 @@ CREATE TABLE users(id UUID DEFAULT gen_random_uuid() PRIMARY KEY, promo_id INT R
618642

619643
contentCheck = getContentCheckFn(
620644
[]string{"parent", "child1", "grandchild1"},
621-
[]string{
622-
"ALTER TABLE defaultdb.public.child1 ADD CONSTRAINT",
623-
"ALTER TABLE defaultdb.public.grandchild1 ADD CONSTRAINT",
624-
})
645+
[]string{getFK("child1"), getFK("grandchild1")},
646+
nil, /* skipFKs */
647+
)
625648
rows = r.QueryStr(t, "EXPLAIN ANALYZE (DEBUG) SELECT * FROM parent, child1, grandchild1")
626649
checkBundle(
627650
t, fmt.Sprint(rows), "parent", contentCheck, false, /* expectErrors */

pkg/sql/opt/exec/execbuilder/relational.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4353,12 +4353,23 @@ func (b *Builder) getEnvData() (exec.ExplainEnvData, error) {
43534353
refTableIncluded.Add(int(table.ID()))
43544354
},
43554355
)
4356-
envOpts.AddFKs = opt.GetAllFKsAmongTables(
4356+
var skipFKs []*tree.AlterTable
4357+
envOpts.AddFKs, skipFKs = opt.GetAllFKs(
4358+
b.ctx,
4359+
b.catalog,
43574360
refTables,
43584361
func(t cat.Table) (tree.TableName, error) {
43594362
return b.catalog.FullyQualifiedName(b.ctx, t)
43604363
},
43614364
)
4365+
// Given that we include all FK-related tables when visiting them, we should
4366+
// not skip any FKs - we'll return an error if we find any in test-only
4367+
// builds.
4368+
if buildutil.CrdbTestBuild {
4369+
if len(skipFKs) > 0 {
4370+
return envOpts, errors.AssertionFailedf("unexpectedly skipped adding FKs in EXPLAIN (OPT): %v", skipFKs)
4371+
}
4372+
}
43624373
var err error
43634374
envOpts.Tables, err = getNames(len(refTables), func(i int) cat.DataSource {
43644375
return refTables[i]

pkg/sql/opt/util.go

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -104,26 +104,53 @@ func VisitFKReferenceTables(
104104
}
105105
}
106106

107-
// GetAllFKsAmongTables returns a list of ALTER statements that corresponds to
108-
// all FOREIGN KEY constraints where both the origin and the referenced tables
109-
// are present in the given set of tables. List of the given tables is assumed
110-
// to be unique.
111-
func GetAllFKsAmongTables(
112-
tables []cat.Table, fullyQualifiedName func(cat.Table) (tree.TableName, error),
113-
) []*tree.AlterTable {
107+
// GetAllFKs returns a list of ALTER statements that corresponds to all FOREIGN
108+
// KEY constraints where both the origin and the referenced tables are present
109+
// in the given set of tables as well as a list of ALTER statements for all
110+
// FOREIGN KEY constraints where the origin table is in the given set while the
111+
// referenced one isn't.
112+
//
113+
// List of the given tables is assumed to be unique.
114+
func GetAllFKs(
115+
ctx context.Context,
116+
catalog cat.Catalog,
117+
tables []cat.Table,
118+
fullyQualifiedName func(cat.Table) (tree.TableName, error),
119+
) (addFKs, skipFKs []*tree.AlterTable) {
114120
idToTable := make(map[cat.StableID]cat.Table)
115121
for _, table := range tables {
116122
idToTable[table.ID()] = table
117123
}
118-
var addFKs []*tree.AlterTable
124+
skippedIDToTable := make(map[cat.StableID]cat.Table)
125+
resolveSkippedTable := func(tabID cat.StableID) cat.Table {
126+
if table, ok := skippedIDToTable[tabID]; ok {
127+
return table
128+
}
129+
ds, _, err := catalog.ResolveDataSourceByID(ctx, cat.Flags{}, tabID)
130+
if err != nil {
131+
return nil
132+
}
133+
t, ok := ds.(cat.Table)
134+
if !ok {
135+
return nil
136+
}
137+
skippedIDToTable[tabID] = t
138+
return t
139+
}
119140
for _, origTable := range tables {
120141
for i := 0; i < origTable.OutboundForeignKeyCount(); i++ {
142+
includeInto := &addFKs
121143
fk := origTable.OutboundForeignKey(i)
122144
refTable, ok := idToTable[fk.ReferencedTableID()]
123145
if !ok {
124-
// The referenced table is not in the given list, so we skip
125-
// this FK constraint.
126-
continue
146+
// The referenced table is not in the given list, so we include
147+
// this FK into the skipped list.
148+
includeInto = &skipFKs
149+
refTable = resolveSkippedTable(fk.ReferencedTableID())
150+
if refTable == nil {
151+
// This is a best-effort attempt to get skipped FKs.
152+
continue
153+
}
127154
}
128155
fromCols, toCols := make(tree.NameList, fk.ColumnCount()), make(tree.NameList, fk.ColumnCount())
129156
for j := range fromCols {
@@ -138,7 +165,7 @@ func GetAllFKsAmongTables(
138165
if err != nil {
139166
continue
140167
}
141-
addFKs = append(addFKs, &tree.AlterTable{
168+
*includeInto = append(*includeInto, &tree.AlterTable{
142169
Table: origTableName.ToUnresolvedObjectName(),
143170
Cmds: []tree.AlterTableCmd{
144171
&tree.AlterTableAddConstraint{
@@ -158,7 +185,7 @@ func GetAllFKsAmongTables(
158185
})
159186
}
160187
}
161-
return addFKs
188+
return addFKs, skipFKs
162189
}
163190

164191
// FamiliesForCols returns the set of column families for the set of cols.

0 commit comments

Comments
 (0)