github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/opt/optbuilder/mutation_builder_fk.go (about) 1 // Copyright 2020 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package optbuilder 12 13 import ( 14 "github.com/cockroachdb/cockroach/pkg/server/telemetry" 15 "github.com/cockroachdb/cockroach/pkg/sql/opt" 16 "github.com/cockroachdb/cockroach/pkg/sql/opt/cat" 17 "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" 18 "github.com/cockroachdb/cockroach/pkg/sql/privilege" 19 "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" 20 "github.com/cockroachdb/cockroach/pkg/sql/sqltelemetry" 21 "github.com/cockroachdb/errors" 22 ) 23 24 // This file contains methods that populate mutationBuilder.checks and cascades. 25 // 26 // -- Checks -- 27 // 28 // The foreign key checks are queries that run after the statement (including 29 // the relevant mutation) completes. They check the integrity of the foreign key 30 // relations that involve modified rows; any row that is returned by these FK 31 // check queries indicates a foreign key violation. 32 // 33 // -- Cacades -- 34 // 35 // The foreign key cascades are "potential" future queries that perform 36 // cascading mutations of child tables. These queries are constructed later as 37 // necessary; mb.cascades stores metadata that include CascadeBuilder instances 38 // which are used to construct these queries. 39 40 // buildFKChecksForInsert builds FK check queries for an insert. 41 // 42 // See the comment at the top of the file for general information on checks and 43 // cascades. 44 // 45 // In the case of insert, each FK check query is an anti-join with the left side 46 // being a WithScan of the mutation input and the right side being the 47 // referenced table. A simple example of an insert with a FK check: 48 // 49 // insert child 50 // ├── ... 51 // ├── input binding: &1 52 // └── f-k-checks 53 // └── f-k-checks-item: child(p) -> parent(p) 54 // └── anti-join (hash) 55 // ├── columns: column2:5!null 56 // ├── with-scan &1 57 // │ ├── columns: column2:5!null 58 // │ └── mapping: 59 // │ └── column2:4 => column2:5 60 // ├── scan parent 61 // │ └── columns: parent.p:6!null 62 // └── filters 63 // └── column2:5 = parent.p:6 64 // 65 // See testdata/fk-checks-insert for more examples. 66 func (mb *mutationBuilder) buildFKChecksForInsert() { 67 if mb.tab.OutboundForeignKeyCount() == 0 { 68 // No relevant FKs. 69 return 70 } 71 if !mb.b.evalCtx.SessionData.OptimizerFKChecks { 72 mb.setFKFallback() 73 return 74 } 75 76 // TODO(radu): if the input is a VALUES with constant expressions, we don't 77 // need to buffer it. This could be a normalization rule, but it's probably 78 // more efficient if we did it in here (or we'd end up building the entire FK 79 // subtrees twice). 80 mb.withID = mb.b.factory.Memo().NextWithID() 81 82 h := &mb.fkCheckHelper 83 for i, n := 0, mb.tab.OutboundForeignKeyCount(); i < n; i++ { 84 if h.initWithOutboundFK(mb, i) { 85 mb.checks = append(mb.checks, h.buildInsertionCheck()) 86 } 87 } 88 telemetry.Inc(sqltelemetry.ForeignKeyChecksUseCounter) 89 } 90 91 // buildFKChecksAndCascadesForDelete builds FK check and cascades for a delete. 92 // 93 // See the comment at the top of the file for general information on checks and 94 // cascades. 95 // 96 // -- Checks -- 97 // 98 // In the case of delete, each FK check query is a semi-join with the left side 99 // being a WithScan of the mutation input and the right side being the 100 // referencing table. For example: 101 // delete parent 102 // ├── ... 103 // ├── input binding: &1 104 // └── f-k-checks 105 // └── f-k-checks-item: child(p) -> parent(p) 106 // └── semi-join (hash) 107 // ├── columns: p:7!null 108 // ├── with-scan &1 109 // │ ├── columns: p:7!null 110 // │ └── mapping: 111 // │ └── parent.p:5 => p:7 112 // ├── scan child 113 // │ └── columns: child.p:9!null 114 // └── filters 115 // └── p:7 = child.p:9 116 // 117 // See testdata/fk-checks-delete for more examples. 118 // 119 // -- Cascades -- 120 // 121 // See onDeleteCascadeBuilder, onDeleteSetBuilder for details. 122 // 123 func (mb *mutationBuilder) buildFKChecksAndCascadesForDelete() { 124 if mb.tab.InboundForeignKeyCount() == 0 { 125 // No relevant FKs. 126 return 127 } 128 if !mb.b.evalCtx.SessionData.OptimizerFKChecks { 129 mb.setFKFallback() 130 return 131 } 132 133 mb.withID = mb.b.factory.Memo().NextWithID() 134 135 for i, n := 0, mb.tab.InboundForeignKeyCount(); i < n; i++ { 136 h := &mb.fkCheckHelper 137 if !h.initWithInboundFK(mb, i) { 138 continue 139 } 140 // The action dictates how a foreign key reference is handled: 141 // - with Cascade/SetNull/SetDefault, we create a cascading mutation to 142 // modify or delete "orphaned" rows in the child table. 143 // - with Restrict/NoAction, we create a check that causes an error if 144 // there are any "orhpaned" rows in the child table. 145 if a := h.fk.DeleteReferenceAction(); a != tree.Restrict && a != tree.NoAction { 146 if !mb.b.evalCtx.SessionData.OptimizerFKCascades { 147 // Bail, so that exec FK checks pick up on FK checks and perform them. 148 mb.setFKFallback() 149 telemetry.Inc(sqltelemetry.ForeignKeyCascadesUseCounter) 150 return 151 } 152 153 var builder memo.CascadeBuilder 154 switch a { 155 case tree.Cascade: 156 builder = newOnDeleteCascadeBuilder(mb.tab, i, h.otherTab) 157 case tree.SetNull, tree.SetDefault: 158 builder = newOnDeleteSetBuilder(mb.tab, i, h.otherTab, a) 159 default: 160 panic(errors.AssertionFailedf("unhandled action type %s", a)) 161 } 162 163 cols := make(opt.ColList, len(h.tabOrdinals)) 164 for i, tabOrd := range h.tabOrdinals { 165 cols[i] = mb.scopeOrdToColID(mb.fetchOrds[tabOrd]) 166 } 167 mb.cascades = append(mb.cascades, memo.FKCascade{ 168 FKName: h.fk.Name(), 169 Builder: builder, 170 WithID: mb.withID, 171 OldValues: cols, 172 NewValues: nil, 173 }) 174 continue 175 } 176 177 fkInput, withScanCols, _ := h.makeFKInputScan(fkInputScanFetchedVals) 178 mb.checks = append(mb.checks, h.buildDeletionCheck(fkInput, withScanCols)) 179 } 180 telemetry.Inc(sqltelemetry.ForeignKeyChecksUseCounter) 181 } 182 183 // buildFKChecksForUpdate builds FK check queries for an update. 184 // 185 // See the comment at the top of the file for general information on checks and 186 // cascades. 187 // 188 // In the case of update, there are two types of FK check queries: 189 // 190 // - insertion-side checks are very similar to the checks we issue for insert; 191 // they are an anti-join with the left side being a WithScan of the "new" 192 // values for each row. For example: 193 // update child 194 // ├── ... 195 // ├── input binding: &1 196 // └── f-k-checks 197 // └── f-k-checks-item: child(p) -> parent(p) 198 // └── anti-join (hash) 199 // ├── columns: column5:6!null 200 // ├── with-scan &1 201 // │ ├── columns: column5:6!null 202 // │ └── mapping: 203 // │ └── column5:5 => column5:6 204 // ├── scan parent 205 // │ └── columns: parent.p:8!null 206 // └── filters 207 // └── column5:6 = parent.p:8 208 // 209 // - deletion-side checks are similar to the checks we issue for delete; they 210 // are a semi-join but the left side input is more complicated: it is an 211 // Except between a WithScan of the "old" values and a WithScan of the "new" 212 // values for each row (this is the set of values that are effectively 213 // removed from the table). For example: 214 // update parent 215 // ├── ... 216 // ├── input binding: &1 217 // └── f-k-checks 218 // └── f-k-checks-item: child(p) -> parent(p) 219 // └── semi-join (hash) 220 // ├── columns: p:8!null 221 // ├── except 222 // │ ├── columns: p:8!null 223 // │ ├── left columns: p:8!null 224 // │ ├── right columns: column7:9 225 // │ ├── with-scan &1 226 // │ │ ├── columns: p:8!null 227 // │ │ └── mapping: 228 // │ │ └── parent.p:5 => p:8 229 // │ └── with-scan &1 230 // │ ├── columns: column7:9!null 231 // │ └── mapping: 232 // │ └── column7:7 => column7:9 233 // ├── scan child 234 // │ └── columns: child.p:11!null 235 // └── filters 236 // └── p:8 = child.p:11 237 // 238 // Only FK relations that involve updated columns result in FK checks. 239 // 240 func (mb *mutationBuilder) buildFKChecksForUpdate() { 241 if mb.tab.OutboundForeignKeyCount() == 0 && mb.tab.InboundForeignKeyCount() == 0 { 242 return 243 } 244 if !mb.b.evalCtx.SessionData.OptimizerFKChecks { 245 mb.setFKFallback() 246 return 247 } 248 249 mb.withID = mb.b.factory.Memo().NextWithID() 250 251 // An Update can be thought of an insertion paired with a deletion, so for an 252 // Update we can emit both semi-joins and anti-joins. 253 254 // Each row input to the Update operator contains both the existing and the 255 // new value for each updated column. From this we can construct the effective 256 // insertion and deletion. 257 258 // Say the table being updated by an update is: 259 // 260 // x | y | z 261 // --+---+-- 262 // 1 | 3 | 5 263 // 264 // And we are executing UPDATE t SET y = 10, then the input to the Update 265 // operator will look like: 266 // 267 // x | y | z | new_y 268 // --+---+---+------ 269 // 1 | 3 | 5 | 10 270 // 271 // The insertion check will happen on the "new" row (x, new_y, z); the deletion 272 // check will happen on the "old" row (x, y, z). 273 274 h := &mb.fkCheckHelper 275 for i, n := 0, mb.tab.OutboundForeignKeyCount(); i < n; i++ { 276 // Verify that at least one FK column is actually updated. 277 if mb.outboundFKColsUpdated(i) { 278 if h.initWithOutboundFK(mb, i) { 279 mb.checks = append(mb.checks, h.buildInsertionCheck()) 280 } 281 } 282 } 283 284 // The "deletion" incurred by an update is the rows deleted for a given 285 // inbound FK minus the rows inserted. 286 for i, n := 0, mb.tab.InboundForeignKeyCount(); i < n; i++ { 287 // Verify that at least one FK column is actually updated. 288 if !mb.inboundFKColsUpdated(i) { 289 continue 290 } 291 if !h.initWithInboundFK(mb, i) { 292 // The FK constraint can safely be ignored. 293 continue 294 } 295 296 if a := h.fk.UpdateReferenceAction(); a != tree.Restrict && a != tree.NoAction { 297 if !mb.b.evalCtx.SessionData.OptimizerFKCascades { 298 // Bail, so that exec FK checks pick up on FK checks and perform them. 299 mb.setFKFallback() 300 telemetry.Inc(sqltelemetry.ForeignKeyCascadesUseCounter) 301 return 302 } 303 builder := newOnUpdateCascadeBuilder(mb.tab, i, h.otherTab, a) 304 305 oldCols := make(opt.ColList, len(h.tabOrdinals)) 306 newCols := make(opt.ColList, len(h.tabOrdinals)) 307 for i, tabOrd := range h.tabOrdinals { 308 fetchOrd := mb.fetchOrds[tabOrd] 309 updateOrd := mb.updateOrds[tabOrd] 310 if updateOrd == -1 { 311 updateOrd = fetchOrd 312 } 313 314 oldCols[i] = mb.scopeOrdToColID(fetchOrd) 315 newCols[i] = mb.scopeOrdToColID(updateOrd) 316 } 317 mb.cascades = append(mb.cascades, memo.FKCascade{ 318 FKName: h.fk.Name(), 319 Builder: builder, 320 WithID: mb.withID, 321 OldValues: oldCols, 322 NewValues: newCols, 323 }) 324 continue 325 } 326 327 // Construct an Except expression for the set difference between "old" 328 // FK values and "new" FK values. 329 // 330 // The simplest example to see why this is necessary is when we are 331 // "updating" a value to the same value, e.g: 332 // UPDATE child SET c = c 333 // Here we are not removing any values from the column, so we must not 334 // check for orphaned rows or we will be generating bogus FK violation 335 // errors. 336 // 337 // There are more complicated cases where one row replaces the value from 338 // another row, e.g. 339 // UPDATE child SET c = c+1 340 // when we have existing consecutive values. These cases are sketchy because 341 // depending on the order in which the mutations are applied, they may or 342 // may not result in unique index violations (but if they go through, the FK 343 // checks should be accurate). 344 // 345 // Note that the same reasoning could be applied to the insertion checks, 346 // but in that case, it is not a correctness issue: it's always ok to 347 // recheck that an existing row is not orphan. It's not really desirable for 348 // performance either: we would be incurring extra cost (more complicated 349 // expressions, scanning the input buffer twice) for a rare case. 350 351 oldRows, colsForOldRow, _ := h.makeFKInputScan(fkInputScanFetchedVals) 352 newRows, colsForNewRow, _ := h.makeFKInputScan(fkInputScanNewVals) 353 354 // The rows that no longer exist are the ones that were "deleted" by virtue 355 // of being updated _from_, minus the ones that were "added" by virtue of 356 // being updated _to_. 357 deletedRows := mb.b.factory.ConstructExcept( 358 oldRows, 359 newRows, 360 &memo.SetPrivate{ 361 LeftCols: colsForOldRow, 362 RightCols: colsForNewRow, 363 OutCols: colsForOldRow, 364 }, 365 ) 366 367 mb.checks = append(mb.checks, h.buildDeletionCheck(deletedRows, colsForOldRow)) 368 } 369 telemetry.Inc(sqltelemetry.ForeignKeyChecksUseCounter) 370 } 371 372 // buildFKChecksForUpsert builds FK check queries for an upsert. 373 // 374 // See the comment at the top of the file for general information on checks and 375 // cascades. 376 // 377 // The case of upsert is very similar to update; see buildFKChecksForUpdate. 378 // The main difference is that for update, the "new" values were readily 379 // available, whereas for upsert, the "new" values can be the result of an 380 // expression of the form: 381 // CASE WHEN canary IS NULL THEN inserter-value ELSE updated-value END 382 // These expressions are already projected as part of the mutation input and are 383 // directly accessible through WithScan. 384 // 385 // Only FK relations that involve updated columns result in deletion-side FK 386 // checks. The insertion-side FK checks are always needed (similar to insert) 387 // because any of the rows might result in an insert rather than an update. 388 // 389 func (mb *mutationBuilder) buildFKChecksForUpsert() { 390 numOutbound := mb.tab.OutboundForeignKeyCount() 391 numInbound := mb.tab.InboundForeignKeyCount() 392 393 if numOutbound == 0 && numInbound == 0 { 394 return 395 } 396 397 if !mb.b.evalCtx.SessionData.OptimizerFKChecks { 398 mb.setFKFallback() 399 return 400 } 401 402 mb.withID = mb.b.factory.Memo().NextWithID() 403 404 h := &mb.fkCheckHelper 405 for i := 0; i < numOutbound; i++ { 406 if h.initWithOutboundFK(mb, i) { 407 mb.checks = append(mb.checks, h.buildInsertionCheck()) 408 } 409 } 410 411 for i := 0; i < numInbound; i++ { 412 // Verify that at least one FK column is updated by the Upsert; columns that 413 // are not updated can get new values (through the insert path) but existing 414 // values are never removed. 415 if !mb.inboundFKColsUpdated(i) { 416 continue 417 } 418 419 if !h.initWithInboundFK(mb, i) { 420 continue 421 } 422 423 if a := h.fk.UpdateReferenceAction(); a != tree.Restrict && a != tree.NoAction { 424 // Bail, so that exec FK checks pick up on FK checks and perform them. 425 mb.setFKFallback() 426 telemetry.Inc(sqltelemetry.ForeignKeyCascadesUseCounter) 427 return 428 } 429 430 // Construct an Except expression for the set difference between "old" FK 431 // values and "new" FK values. See buildFKChecksForUpdate for more details. 432 // 433 // Note that technically, to get "old" values for the updated rows we should 434 // be selecting only the rows that correspond to updates, as opposed to 435 // insertions (using a "canaryCol IS NOT NULL" condition). But the rows we 436 // would filter out have all-null fetched values anyway and will never match 437 // in the semi join. 438 oldRows, colsForOldRow, _ := h.makeFKInputScan(fkInputScanFetchedVals) 439 newRows, colsForNewRow, _ := h.makeFKInputScan(fkInputScanNewVals) 440 441 // The rows that no longer exist are the ones that were "deleted" by virtue 442 // of being updated _from_, minus the ones that were "added" by virtue of 443 // being updated _to_. 444 deletedRows := mb.b.factory.ConstructExcept( 445 oldRows, 446 newRows, 447 &memo.SetPrivate{ 448 LeftCols: colsForOldRow, 449 RightCols: colsForNewRow, 450 OutCols: colsForOldRow, 451 }, 452 ) 453 mb.checks = append(mb.checks, h.buildDeletionCheck(deletedRows, colsForOldRow)) 454 } 455 telemetry.Inc(sqltelemetry.ForeignKeyChecksUseCounter) 456 } 457 458 // outboundFKColsUpdated returns true if any of the FK columns for an outbound 459 // constraint are being updated (according to updateOrds). 460 func (mb *mutationBuilder) outboundFKColsUpdated(fkOrdinal int) bool { 461 fk := mb.tab.OutboundForeignKey(fkOrdinal) 462 for i, n := 0, fk.ColumnCount(); i < n; i++ { 463 if ord := fk.OriginColumnOrdinal(mb.tab, i); mb.updateOrds[ord] != -1 { 464 return true 465 } 466 } 467 return false 468 } 469 470 // inboundFKColsUpdated returns true if any of the FK columns for an inbound 471 // constraint are being updated (according to updateOrds). 472 func (mb *mutationBuilder) inboundFKColsUpdated(fkOrdinal int) bool { 473 fk := mb.tab.InboundForeignKey(fkOrdinal) 474 for i, n := 0, fk.ColumnCount(); i < n; i++ { 475 if ord := fk.ReferencedColumnOrdinal(mb.tab, i); mb.updateOrds[ord] != -1 { 476 return true 477 } 478 } 479 return false 480 } 481 482 // fkCheckHelper is a type associated with a single FK constraint and is used to 483 // build the "leaves" of a FK check expression, namely the WithScan of the 484 // mutation input and the Scan of the other table. 485 type fkCheckHelper struct { 486 mb *mutationBuilder 487 488 fk cat.ForeignKeyConstraint 489 fkOrdinal int 490 fkOutbound bool 491 492 otherTab cat.Table 493 494 // tabOrdinals are the table ordinals of the FK columns in the table that is 495 // being mutated. They correspond 1-to-1 to the columns in the 496 // ForeignKeyConstraint. 497 tabOrdinals []int 498 // otherTabOrdinals are the table ordinals of the FK columns in the "other" 499 // table. They correspond 1-to-1 to the columns in the ForeignKeyConstraint. 500 otherTabOrdinals []int 501 } 502 503 // initWithOutboundFK initializes the helper with an outbound FK constraint. 504 // 505 // Returns false if the FK relation should be ignored (e.g. because the new 506 // values for the FK columns are known to be always NULL). 507 func (h *fkCheckHelper) initWithOutboundFK(mb *mutationBuilder, fkOrdinal int) bool { 508 *h = fkCheckHelper{ 509 mb: mb, 510 fk: mb.tab.OutboundForeignKey(fkOrdinal), 511 fkOrdinal: fkOrdinal, 512 fkOutbound: true, 513 } 514 515 refID := h.fk.ReferencedTableID() 516 ref, isAdding, err := mb.b.catalog.ResolveDataSourceByID(mb.b.ctx, cat.Flags{}, refID) 517 if err != nil { 518 if isAdding { 519 // The other table is in the process of being added; ignore the FK relation. 520 return false 521 } 522 panic(err) 523 } 524 // We need SELECT privileges on the referenced table. 525 mb.b.checkPrivilege(opt.DepByID(refID), ref, privilege.SELECT) 526 h.otherTab = ref.(cat.Table) 527 528 numCols := h.fk.ColumnCount() 529 h.allocOrdinals(numCols) 530 for i := 0; i < numCols; i++ { 531 h.tabOrdinals[i] = h.fk.OriginColumnOrdinal(mb.tab, i) 532 h.otherTabOrdinals[i] = h.fk.ReferencedColumnOrdinal(h.otherTab, i) 533 } 534 535 // Check if we are setting NULL values for the FK columns, like when this 536 // mutation is the result of a SET NULL cascade action. 537 numNullCols := 0 538 for _, tabOrd := range h.tabOrdinals { 539 col := mb.scopeOrdToColID(mb.mapToReturnScopeOrd(tabOrd)) 540 if memo.OutputColumnIsAlwaysNull(mb.outScope.expr, col) { 541 numNullCols++ 542 } 543 } 544 if numNullCols == numCols { 545 // All FK columns are getting NULL values; FK check not needed. 546 return false 547 } 548 if numNullCols > 0 && h.fk.MatchMethod() == tree.MatchSimple { 549 // At least one FK column is getting a NULL value and we are using MATCH 550 // SIMPLE; FK check not needed. 551 return false 552 } 553 554 return true 555 } 556 557 // initWithInboundFK initializes the helper with an inbound FK constraint. 558 // 559 // Returns false if the FK relation should be ignored (because the other table 560 // is in the process of being created). 561 func (h *fkCheckHelper) initWithInboundFK(mb *mutationBuilder, fkOrdinal int) (ok bool) { 562 *h = fkCheckHelper{ 563 mb: mb, 564 fk: mb.tab.InboundForeignKey(fkOrdinal), 565 fkOrdinal: fkOrdinal, 566 fkOutbound: false, 567 } 568 569 originID := h.fk.OriginTableID() 570 ref, isAdding, err := mb.b.catalog.ResolveDataSourceByID(mb.b.ctx, cat.Flags{}, originID) 571 if err != nil { 572 if isAdding { 573 // The other table is in the process of being added; ignore the FK relation. 574 return false 575 } 576 panic(err) 577 } 578 // We need SELECT privileges on the origin table. 579 mb.b.checkPrivilege(opt.DepByID(originID), ref, privilege.SELECT) 580 h.otherTab = ref.(cat.Table) 581 582 numCols := h.fk.ColumnCount() 583 h.allocOrdinals(numCols) 584 for i := 0; i < numCols; i++ { 585 h.tabOrdinals[i] = h.fk.ReferencedColumnOrdinal(mb.tab, i) 586 h.otherTabOrdinals[i] = h.fk.OriginColumnOrdinal(h.otherTab, i) 587 } 588 589 return true 590 } 591 592 type fkInputScanType uint8 593 594 const ( 595 fkInputScanNewVals fkInputScanType = iota 596 fkInputScanFetchedVals 597 ) 598 599 // makeFKInputScan constructs a WithScan that iterates over the input to the 600 // mutation operator. Used in expressions that generate rows for checking for FK 601 // violations. 602 // 603 // The WithScan expression will scan either the new values or the fetched values 604 // for the given table ordinals (which correspond to FK columns). 605 // 606 // Returns the output columns from the WithScan, which map 1-to-1 to 607 // h.tabOrdinals. Also returns the subset of these columns that can be assumed 608 // to be not null (either because they are not null in the mutation input or 609 // because they are non-nullable table columns). 610 // 611 func (h *fkCheckHelper) makeFKInputScan( 612 typ fkInputScanType, 613 ) (scan memo.RelExpr, outCols opt.ColList, notNullOutCols opt.ColSet) { 614 mb := h.mb 615 // inputCols are the column IDs from the mutation input that we are scanning. 616 inputCols := make(opt.ColList, len(h.tabOrdinals)) 617 // outCols will store the newly synthesized output columns for WithScan. 618 outCols = make(opt.ColList, len(inputCols)) 619 for i, tabOrd := range h.tabOrdinals { 620 if typ == fkInputScanNewVals { 621 inputCols[i] = mb.scopeOrdToColID(mb.mapToReturnScopeOrd(tabOrd)) 622 } else { 623 inputCols[i] = mb.scopeOrdToColID(mb.fetchOrds[tabOrd]) 624 } 625 if inputCols[i] == 0 { 626 panic(errors.AssertionFailedf("no value for FK column (tabOrd=%d)", tabOrd)) 627 } 628 629 // Synthesize new column. 630 c := mb.b.factory.Metadata().ColumnMeta(inputCols[i]) 631 outCols[i] = mb.md.AddColumn(c.Alias, c.Type) 632 633 // If a table column is not nullable, NULLs cannot be inserted (the 634 // mutation will fail). So for the purposes of FK checks, we can treat 635 // these columns as not null. 636 if mb.outScope.expr.Relational().NotNullCols.Contains(inputCols[i]) || 637 !mb.tab.Column(tabOrd).IsNullable() { 638 notNullOutCols.Add(outCols[i]) 639 } 640 } 641 642 scan = mb.b.factory.ConstructWithScan(&memo.WithScanPrivate{ 643 With: mb.withID, 644 InCols: inputCols, 645 OutCols: outCols, 646 BindingProps: mb.outScope.expr.Relational(), 647 ID: mb.b.factory.Metadata().NextUniqueID(), 648 }) 649 return scan, outCols, notNullOutCols 650 } 651 652 // buildOtherTableScan builds a Scan of the "other" table. 653 func (h *fkCheckHelper) buildOtherTableScan() (outScope *scope, tabMeta *opt.TableMeta) { 654 otherTabMeta := h.mb.b.addTable(h.otherTab, tree.NewUnqualifiedTableName(h.otherTab.Name())) 655 return h.mb.b.buildScan( 656 otherTabMeta, 657 h.otherTabOrdinals, 658 &tree.IndexFlags{IgnoreForeignKeys: true}, 659 noRowLocking, 660 excludeMutations, 661 h.mb.b.allocScope(), 662 ), otherTabMeta 663 } 664 665 func (h *fkCheckHelper) allocOrdinals(numCols int) { 666 buf := make([]int, numCols*2) 667 h.tabOrdinals = buf[:numCols] 668 h.otherTabOrdinals = buf[numCols:] 669 } 670 671 // buildInsertionCheck creates a FK check for rows which are added to a table. 672 // The input to the insertion check will be produced from the input to the 673 // mutation operator. 674 func (h *fkCheckHelper) buildInsertionCheck() memo.FKChecksItem { 675 fkInput, withScanCols, notNullWithScanCols := h.makeFKInputScan(fkInputScanNewVals) 676 677 numCols := len(withScanCols) 678 f := h.mb.b.factory 679 if notNullWithScanCols.Len() < numCols { 680 // The columns we are inserting might have NULLs. These require special 681 // handling, depending on the match method: 682 // - MATCH SIMPLE: allows any column(s) to be NULL and the row doesn't 683 // need to have a match in the referenced table. 684 // - MATCH FULL: only the case where *all* the columns are NULL is 685 // allowed, and the row doesn't need to have a match in the 686 // referenced table. 687 // 688 // Note that rows that have NULLs will never have a match in the anti 689 // join and will generate errors. To handle these cases, we filter the 690 // mutated rows (before the anti join) to remove those which don't need a 691 // match. 692 // 693 // For SIMPLE, we filter out any rows which have a NULL. For FULL, we 694 // filter out any rows where all the columns are NULL (rows which have 695 // NULLs a subset of columns are let through and will generate FK errors 696 // because they will never have a match in the anti join). 697 switch m := h.fk.MatchMethod(); m { 698 case tree.MatchSimple: 699 // Filter out any rows which have a NULL; build filters of the form 700 // (a IS NOT NULL) AND (b IS NOT NULL) ... 701 filters := make(memo.FiltersExpr, 0, numCols-notNullWithScanCols.Len()) 702 for _, col := range withScanCols { 703 if !notNullWithScanCols.Contains(col) { 704 filters = append(filters, f.ConstructFiltersItem( 705 f.ConstructIsNot( 706 f.ConstructVariable(col), 707 memo.NullSingleton, 708 ), 709 )) 710 } 711 } 712 fkInput = f.ConstructSelect(fkInput, filters) 713 714 case tree.MatchFull: 715 // Filter out any rows which have NULLs on all referencing columns. 716 if !notNullWithScanCols.Empty() { 717 // We statically know that some of the referencing columns can't be 718 // NULL. In this case, we don't need to filter anything (the case 719 // where all the origin columns are NULL is not possible). 720 break 721 } 722 // Build a filter of the form 723 // (a IS NOT NULL) OR (b IS NOT NULL) ... 724 var condition opt.ScalarExpr 725 for _, col := range withScanCols { 726 is := f.ConstructIsNot( 727 f.ConstructVariable(col), 728 memo.NullSingleton, 729 ) 730 if condition == nil { 731 condition = is 732 } else { 733 condition = f.ConstructOr(condition, is) 734 } 735 } 736 fkInput = f.ConstructSelect( 737 fkInput, 738 memo.FiltersExpr{f.ConstructFiltersItem(condition)}, 739 ) 740 741 default: 742 panic(errors.AssertionFailedf("match method %s not supported", m)) 743 } 744 } 745 746 // Build an anti-join, with the origin FK columns on the left and the 747 // referenced columns on the right. 748 749 scanScope, refTabMeta := h.buildOtherTableScan() 750 751 // Build the join filters: 752 // (origin_a = referenced_a) AND (origin_b = referenced_b) AND ... 753 antiJoinFilters := make(memo.FiltersExpr, numCols) 754 for j := 0; j < numCols; j++ { 755 antiJoinFilters[j] = f.ConstructFiltersItem( 756 f.ConstructEq( 757 f.ConstructVariable(withScanCols[j]), 758 f.ConstructVariable(scanScope.cols[j].id), 759 ), 760 ) 761 } 762 antiJoin := f.ConstructAntiJoin( 763 fkInput, scanScope.expr, antiJoinFilters, &memo.JoinPrivate{}, 764 ) 765 766 return f.ConstructFKChecksItem(antiJoin, &memo.FKChecksItemPrivate{ 767 OriginTable: h.mb.tabID, 768 ReferencedTable: refTabMeta.MetaID, 769 FKOutbound: true, 770 FKOrdinal: h.fkOrdinal, 771 KeyCols: withScanCols, 772 OpName: h.mb.opName, 773 }) 774 } 775 776 // buildDeletionCheck creates a FK check for rows which are removed from a 777 // table. deletedRows is used as the input to the deletion check, and deleteCols 778 // is a list of the columns for the rows being deleted, containing values for 779 // the referenced FK columns in the table we are mutating. 780 func (h *fkCheckHelper) buildDeletionCheck( 781 deletedRows memo.RelExpr, deleteCols opt.ColList, 782 ) memo.FKChecksItem { 783 // Build a semi join, with the referenced FK columns on the left and the 784 // origin columns on the right. 785 scanScope, origTabMeta := h.buildOtherTableScan() 786 787 // Note that it's impossible to orphan a row whose FK key columns contain a 788 // NULL, since by definition a NULL never refers to an actual row (in 789 // either MATCH FULL or MATCH SIMPLE). 790 // Build the join filters: 791 // (origin_a = referenced_a) AND (origin_b = referenced_b) AND ... 792 f := h.mb.b.factory 793 semiJoinFilters := make(memo.FiltersExpr, len(deleteCols)) 794 for j := range deleteCols { 795 semiJoinFilters[j] = f.ConstructFiltersItem( 796 f.ConstructEq( 797 f.ConstructVariable(deleteCols[j]), 798 f.ConstructVariable(scanScope.cols[j].id), 799 ), 800 ) 801 } 802 semiJoin := f.ConstructSemiJoin( 803 deletedRows, scanScope.expr, semiJoinFilters, &memo.JoinPrivate{}, 804 ) 805 806 return f.ConstructFKChecksItem(semiJoin, &memo.FKChecksItemPrivate{ 807 OriginTable: origTabMeta.MetaID, 808 ReferencedTable: h.mb.tabID, 809 FKOutbound: false, 810 FKOrdinal: h.fkOrdinal, 811 KeyCols: deleteCols, 812 OpName: h.mb.opName, 813 }) 814 } 815 816 // setFKFallback enables fallback to the legacy foreign key checks and 817 // cascade path. 818 func (mb *mutationBuilder) setFKFallback() { 819 // Clear out any checks or cascades that may have been built already. 820 mb.checks = nil 821 mb.cascades = nil 822 mb.fkFallback = true 823 telemetry.Inc(sqltelemetry.ForeignKeyLegacyUseCounter) 824 }