| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610 |
- package storage
- import (
- "fmt"
- "sort"
- "strconv"
- "strings"
- )
- // ForeignKeyViolationError reports a row that would leave a foreign key
- // dangling. It is a distinct type so the protocol layers can report SQLSTATE
- // 23503 instead of a generic execution error.
- type ForeignKeyViolationError struct {
- // Table is the child table whose row cannot be written, or whose rows would
- // be orphaned by the parent change.
- Table string
- // Constraint is the label of the violated FOREIGN KEY declaration.
- Constraint string
- }
- func (e *ForeignKeyViolationError) Error() string {
- if e.Constraint != "" {
- return fmt.Sprintf("FOREIGN KEY constraint failed: %s", e.Constraint)
- }
- return "FOREIGN KEY constraint failed: " + e.Table
- }
- // foreignKeyValues returns the formatted values a row holds for the given
- // columns, and reports false when any component is NULL or missing. MATCH
- // SIMPLE semantics: a partially NULL key is never matched against the parent, so
- // the constraint does not apply to that row.
- func foreignKeyValues(row Row, columns []string) ([]string, bool) {
- values := make([]string, len(columns))
- for i, name := range columns {
- value, ok := rowColumnValue(row, name)
- if !ok || value == nil {
- return nil, false
- }
- values[i] = formatIndexValue(value)
- }
- return values, true
- }
- // foreignKeyMatches reports whether row's columns equal want.
- func foreignKeyMatches(row Row, columns, want []string) bool {
- if len(columns) != len(want) {
- return false
- }
- got, ok := foreignKeyValues(row, columns)
- if !ok {
- return false
- }
- for i := range want {
- if got[i] != want[i] {
- return false
- }
- }
- return true
- }
- func foreignKeyValuesEqual(a, b []string) bool {
- if len(a) != len(b) {
- return false
- }
- for i := range a {
- if a[i] != b[i] {
- return false
- }
- }
- return true
- }
- // parentRowExists reports whether table holds a row whose columns match want.
- // extra supplies rows that are not durable yet (the rows of the current
- // statement), so a self-referential row can reference itself or another row of
- // the same multi-row insert. The scan deliberately takes no table gate: callers
- // hold the table's write gate already, and re-entering it would deadlock on a
- // self-referential constraint.
- func (m *TableManager) parentRowExists(table string, extra []Row, columns, want []string) (bool, error) {
- for _, row := range extra {
- if foreignKeyMatches(row, columns, want) {
- return true, nil
- }
- }
- found := false
- err := m.scanRows(table, func(row Row) (bool, error) {
- if foreignKeyMatches(row, columns, want) {
- found = true
- return true, nil
- }
- return false, nil
- })
- return found, err
- }
- // validateInsertForeignKeys enforces every FOREIGN KEY declared on schema for
- // rows that are about to be written. rows holds all rows of the statement so a
- // self-referential insert can reference a sibling row in the same batch.
- func (m *TableManager) validateInsertForeignKeys(schema *Schema, rows []Row) error {
- if len(schema.ForeignKeys) == 0 {
- return nil
- }
- for _, fk := range schema.ForeignKeys {
- for _, row := range rows {
- want, ok := foreignKeyValues(row, fk.Columns)
- if !ok {
- continue
- }
- var extra []Row
- if strings.EqualFold(fk.RefTable, schema.Name) {
- extra = rows
- }
- found, err := m.parentRowExists(fk.RefTable, extra, fk.RefColumns, want)
- if err != nil {
- return fmt.Errorf("validate foreign key %s: %w", fk.Label(), err)
- }
- if !found {
- return &ForeignKeyViolationError{Table: schema.Name, Constraint: fk.Label()}
- }
- }
- }
- return nil
- }
- // validateParentKeyRemoval rejects a parent change that would orphan an existing
- // child row. oldRow is the durable parent image; newRow is the replacement image
- // for an UPDATE (nil for a DELETE). A change that leaves every referenced value
- // untouched cannot orphan anything and is not checked.
- //
- // A self-referential table needs no special exemption for a row that references
- // itself: the row being deleted (or re-keyed) is skipped in the child scan and
- // its replacement image is evaluated instead, so deleting a self-referencing row
- // works while re-keying it into a dangling reference still fails.
- func (m *TableManager) validateParentKeyRemoval(table string, oldRow, newRow Row) error {
- referencing, err := m.schema.referencingForeignKeys(table)
- if err != nil {
- return err
- }
- selfPK := ""
- var selfSchema *Schema
- if len(referencing) > 0 {
- schema, err := m.schema.GetSchema(table)
- if err != nil {
- return err
- }
- selfSchema = schema
- if pk, err := schema.PrimaryKeyForRow(oldRow); err == nil {
- selfPK = pk
- }
- }
- for _, ref := range referencing {
- oldKey, ok := foreignKeyValues(oldRow, ref.FK.RefColumns)
- if !ok {
- continue
- }
- if newRow != nil {
- if newKey, ok := foreignKeyValues(newRow, ref.FK.RefColumns); ok && foreignKeyValuesEqual(oldKey, newKey) {
- continue
- }
- }
- selfTable := strings.EqualFold(ref.Table, table)
- found := false
- err := m.scanRows(ref.Table, func(row Row) (bool, error) {
- if selfTable && selfPK != "" {
- if pk, err := selfSchema.PrimaryKeyForRow(row); err == nil && pk == selfPK {
- return false, nil
- }
- }
- if foreignKeyMatches(row, ref.FK.Columns, oldKey) {
- found = true
- return true, nil
- }
- return false, nil
- })
- if err != nil {
- return fmt.Errorf("validate foreign key %s: %w", ref.FK.Label(), err)
- }
- if !found && selfTable && newRow != nil && foreignKeyMatches(newRow, ref.FK.Columns, oldKey) {
- found = true
- }
- if found {
- return &ForeignKeyViolationError{Table: ref.Table, Constraint: ref.FK.Label()}
- }
- }
- return nil
- }
- // referencingForeignKey is one FOREIGN KEY declaration that targets a table.
- type referencingForeignKey struct {
- Table string
- FK ForeignKeyConstraint
- }
- // foreignKeyCatalog is the cached foreign-key metadata of the database. It is
- // keyed by the schema manager version so a DDL change rebuilds it.
- type foreignKeyCatalog struct {
- version uint64
- participants map[string]bool
- referencing map[string][]referencingForeignKey
- adjacency map[string]map[string]bool
- }
- // foreignKeyCatalogSnapshot returns the current foreign-key metadata, rebuilding
- // it when the schema version changed.
- func (m *SchemaManager) foreignKeyCatalogSnapshot() (*foreignKeyCatalog, error) {
- version := m.Version()
- m.fkCatalogMu.Lock()
- cached := m.fkCatalog
- m.fkCatalogMu.Unlock()
- if cached != nil && cached.version == version {
- return cached, nil
- }
- tables, err := m.ListTables()
- if err != nil {
- return nil, err
- }
- catalog, err := buildForeignKeyCatalog(version, tables, m.GetSchema)
- if err != nil {
- return nil, err
- }
- m.fkCatalogMu.Lock()
- if m.Version() == version {
- m.fkCatalog = catalog
- }
- m.fkCatalogMu.Unlock()
- return catalog, nil
- }
- // buildForeignKeyCatalog derives the foreign-key metadata of a catalog view from
- // its table list and definitions. Both the shared SchemaManager cache and a
- // transaction's staged catalog build it, so a session validates against the
- // constraints it can see.
- func buildForeignKeyCatalog(version uint64, tables []string, getSchema func(string) (*Schema, error)) (*foreignKeyCatalog, error) {
- catalog := &foreignKeyCatalog{
- version: version,
- participants: make(map[string]bool),
- referencing: make(map[string][]referencingForeignKey),
- adjacency: make(map[string]map[string]bool),
- }
- link := func(a, b string) {
- if catalog.adjacency[a] == nil {
- catalog.adjacency[a] = make(map[string]bool)
- }
- catalog.adjacency[a][b] = true
- }
- for _, name := range tables {
- schema, err := getSchema(name)
- if err != nil {
- return nil, err
- }
- child := strings.ToLower(schema.Name)
- for _, fk := range schema.ForeignKeys {
- parent := strings.ToLower(fk.RefTable)
- catalog.participants[child] = true
- catalog.participants[parent] = true
- catalog.referencing[parent] = append(catalog.referencing[parent], referencingForeignKey{Table: schema.Name, FK: fk})
- link(child, parent)
- link(parent, child)
- }
- }
- return catalog, nil
- }
- // closure returns the foreign-key-connected component of the given tables.
- func (c *foreignKeyCatalog) closure(tables []string) map[string]bool {
- closure := make(map[string]bool)
- queue := make([]string, 0, len(tables))
- for _, table := range tables {
- if c.participants[strings.ToLower(table)] {
- queue = append(queue, strings.ToLower(table))
- }
- }
- for len(queue) > 0 {
- table := queue[0]
- queue = queue[1:]
- if closure[table] {
- continue
- }
- closure[table] = true
- neighbors := make([]string, 0, len(c.adjacency[table]))
- for neighbor := range c.adjacency[table] {
- neighbors = append(neighbors, neighbor)
- }
- sort.Strings(neighbors)
- for _, neighbor := range neighbors {
- if !closure[neighbor] {
- queue = append(queue, neighbor)
- }
- }
- }
- return closure
- }
- // referencingForeignKeys returns every FOREIGN KEY declaration whose parent is
- // table.
- func (m *SchemaManager) referencingForeignKeys(table string) ([]referencingForeignKey, error) {
- catalog, err := m.foreignKeyCatalogSnapshot()
- if err != nil {
- return nil, err
- }
- return catalog.referencing[strings.ToLower(table)], nil
- }
- // foreignKeyReferenceName formats a constraint for a DDL error message. The
- // child table is included because an unnamed constraint's label is only unique
- // within its table.
- func foreignKeyReferenceName(child string, fk ForeignKeyConstraint) string {
- return fmt.Sprintf("%s on %s", fk.Label(), child)
- }
- // ForeignKeyColumnDependency returns the constraints that keep a column in
- // place: a FOREIGN KEY this table declares on it, or a FOREIGN KEY (from this
- // table or another one) that references it as a parent column. DROP/RENAME
- // COLUMN cannot rewrite persisted constraint text, so a non-empty result must
- // reject the change instead of silently disabling the constraint.
- func (m *SchemaManager) ForeignKeyColumnDependency(table, column string) ([]string, error) {
- catalog, err := m.foreignKeyCatalogSnapshot()
- if err != nil {
- return nil, err
- }
- schema, err := m.GetSchema(table)
- if err != nil {
- return nil, err
- }
- seen := make(map[string]bool)
- var labels []string
- add := func(child string, fk ForeignKeyConstraint) {
- label := foreignKeyReferenceName(child, fk)
- if seen[label] {
- return
- }
- seen[label] = true
- labels = append(labels, label)
- }
- for _, fk := range schema.ForeignKeys {
- for _, name := range fk.Columns {
- if strings.EqualFold(name, column) {
- add(schema.Name, fk)
- break
- }
- }
- }
- for _, ref := range catalog.referencing[strings.ToLower(table)] {
- for _, name := range ref.FK.RefColumns {
- if strings.EqualFold(name, column) {
- add(ref.Table, ref.FK)
- break
- }
- }
- }
- sort.Strings(labels)
- return labels, nil
- }
- // ForeignKeyTableDependency returns the constraints that name table as their
- // referenced parent, including a self-reference. RENAME TABLE cannot rewrite
- // them, and DROP TABLE would leave them dangling.
- func (m *SchemaManager) ForeignKeyTableDependency(table string) ([]string, error) {
- catalog, err := m.foreignKeyCatalogSnapshot()
- if err != nil {
- return nil, err
- }
- seen := make(map[string]bool)
- var labels []string
- for _, ref := range catalog.referencing[strings.ToLower(table)] {
- label := foreignKeyReferenceName(ref.Table, ref.FK)
- if seen[label] {
- continue
- }
- seen[label] = true
- labels = append(labels, label)
- }
- sort.Strings(labels)
- return labels, nil
- }
- // ForeignKeyParticipants reports whether a write to table can affect referential
- // integrity. A table participates when it declares a FOREIGN KEY or when another
- // table references it.
- func (m *SchemaManager) ForeignKeyParticipants(table string) (bool, error) {
- catalog, err := m.foreignKeyCatalogSnapshot()
- if err != nil {
- return false, err
- }
- return catalog.participants[strings.ToLower(table)], nil
- }
- // ForeignKeyClosure returns the tables whose rows must be re-validated when the
- // given tables change: the foreign-key-connected component of every changed
- // table that participates in a constraint. The result is empty when no changed
- // table participates.
- func (m *SchemaManager) ForeignKeyClosure(tables []string) (map[string]bool, error) {
- catalog, err := m.foreignKeyCatalogSnapshot()
- if err != nil {
- return nil, err
- }
- return catalog.closure(tables), nil
- }
- // lockForeignKeyWrites serializes writes that can affect referential integrity,
- // so a validation scan cannot interleave with a concurrent insert, update, or
- // delete on the other side of a constraint. It must be acquired before any
- // table gate: commits validate several tables at once, and the canonical order
- // (foreign-key gate, then table gates) is what keeps that deadlock-free.
- func (m *TableManager) lockForeignKeyWrites(tables ...string) (func(), error) {
- for _, table := range tables {
- participates, err := m.schema.ForeignKeyParticipants(table)
- if err != nil {
- return nil, err
- }
- if participates {
- m.foreignKeyMu.Lock()
- return m.foreignKeyMu.Unlock, nil
- }
- }
- return func() {}, nil
- }
- // LockForeignKeyWriteGate takes the foreign-key write gate unconditionally.
- // Schema DDL uses it around CREATE TABLE and DROP TABLE: the table that gains or
- // loses a constraint is not part of the foreign-key graph yet (or has just been
- // removed from it), so the participant check would skip the gate exactly when
- // the graph is changing. Callers must take it before any table gate.
- func (m *TableManager) LockForeignKeyWriteGate() func() {
- m.foreignKeyMu.Lock()
- return m.foreignKeyMu.Unlock
- }
- // foreignKeyStateValidator validates the final row state of a set of tables
- // against every FOREIGN KEY declaration among them.
- type foreignKeyStateValidator struct {
- tables map[string][]Row
- keySets map[string]map[string]bool
- schemas map[string]*Schema
- }
- // validateForeignKeyState checks every child row of the given tables against its
- // parent rows. tables maps a lowercased table name to its final row set (durable
- // rows with staged deletes applied plus staged writes).
- func (v *foreignKeyStateValidator) validateForeignKeyState() error {
- if v.keySets == nil {
- v.keySets = make(map[string]map[string]bool)
- }
- for table, rows := range v.tables {
- schema := v.schemas[table]
- if schema == nil {
- continue
- }
- for _, fk := range schema.ForeignKeys {
- parent := strings.ToLower(fk.RefTable)
- parentRows, ok := v.tables[parent]
- if !ok {
- return fmt.Errorf("foreign key %s: parent table %s is not available for validation", fk.Label(), fk.RefTable)
- }
- key := parent + "\x00" + joinForeignKeyValues(fk.RefColumns)
- set, ok := v.keySets[key]
- if !ok {
- set = make(map[string]bool, len(parentRows))
- for _, row := range parentRows {
- values, ok := foreignKeyValues(row, fk.RefColumns)
- if !ok {
- continue
- }
- set[joinForeignKeyValues(values)] = true
- }
- v.keySets[key] = set
- }
- for _, row := range rows {
- values, ok := foreignKeyValues(row, fk.Columns)
- if !ok {
- continue
- }
- if !set[joinForeignKeyValues(values)] {
- return &ForeignKeyViolationError{Table: schema.Name, Constraint: fk.Label()}
- }
- }
- }
- }
- return nil
- }
- func joinForeignKeyValues(values []string) string {
- var b strings.Builder
- for _, value := range values {
- b.WriteString(strconv.Itoa(len(value)))
- b.WriteByte(':')
- b.WriteString(value)
- }
- return b.String()
- }
- // foreignKeyClosure returns the tables a commit must re-validate, or nil when no
- // staged table participates in a foreign key.
- func (s *Session) foreignKeyClosure() (map[string]bool, error) {
- tables := make([]string, 0, len(s.overlay))
- seen := make(map[string]bool, len(s.overlay))
- for table := range s.overlay {
- if seen[table] {
- continue
- }
- seen[table] = true
- tables = append(tables, table)
- }
- // DDL staged by this transaction can add or remove a constraint without
- // staging a row, so its tables join the validation component too.
- if s.schemaTx != nil {
- for table, schema := range s.schemaTx.staged {
- if schema == nil || seen[table] {
- continue
- }
- seen[table] = true
- tables = append(tables, table)
- }
- }
- if len(tables) == 0 {
- return nil, nil
- }
- return s.foreignKeyClosureLocked(tables)
- }
- // foreignKeyClosureLocked computes the constrained component of tables against
- // the catalog this session observes (caller holds the session lock).
- func (s *Session) foreignKeyClosureLocked(tables []string) (map[string]bool, error) {
- if s.schemaTx == nil {
- return s.schema.ForeignKeyClosure(tables)
- }
- catalog, err := s.foreignKeyCatalogLocked()
- if err != nil {
- return nil, err
- }
- return catalog.closure(tables), nil
- }
- // referencingForeignKeysLocked returns the FOREIGN KEY declarations whose parent
- // is table, as this session observes the catalog.
- func (s *Session) referencingForeignKeysLocked(table string) ([]referencingForeignKey, error) {
- if s.schemaTx == nil {
- return s.schema.referencingForeignKeys(table)
- }
- catalog, err := s.foreignKeyCatalogLocked()
- if err != nil {
- return nil, err
- }
- return catalog.referencing[strings.ToLower(table)], nil
- }
- // validateForeignKeyFinalState checks the committed row image of every table in
- // the constrained component. The caller holds the session lock and the table
- // gates of those tables.
- func (s *Session) validateForeignKeyFinalState(closure map[string]bool) error {
- if len(closure) == 0 {
- return nil
- }
- schemas := make(map[string]*Schema, len(closure))
- tables := make(map[string][]Row, len(closure))
- for table := range closure {
- schema, err := s.getSchemaLocked(table)
- if err != nil {
- return err
- }
- rows, err := s.finalRows(table, schema)
- if err != nil {
- return err
- }
- schemas[table] = schema
- tables[table] = rows
- }
- validator := foreignKeyStateValidator{tables: tables, schemas: schemas}
- return validator.validateForeignKeyState()
- }
- // finalRows returns the table's durable rows with the transaction overlay
- // applied: deleted keys are removed, staged rows are added, and a key that was
- // both deleted and re-staged appears once with its staged image. It takes no
- // table gate because the caller holds the table's exclusive gate.
- func (s *Session) finalRows(table string, schema *Schema) ([]Row, error) {
- overlay := s.overlay[strings.ToLower(table)]
- source := s.durableSourceLocked(table)
- rows := make([]Row, 0)
- err := s.table.scanRowsAtPrefix(s.table.dataPrefix(source), func(_ string, row Row, _ uint64) (bool, error) {
- pk, err := schema.PrimaryKeyForRow(row)
- if err != nil {
- return true, err
- }
- if _, staged := overlay[s.table.dataKey(table, pk)]; staged {
- return false, nil
- }
- if err := s.applyStagedRewritesLocked(table, row); err != nil {
- return true, err
- }
- rows = append(rows, row)
- return false, nil
- })
- if err != nil {
- return nil, err
- }
- for _, entry := range overlay {
- if entry.absent {
- continue
- }
- row := cloneRow(entry.row)
- if err := s.applyStagedRewritesLocked(table, row); err != nil {
- return nil, err
- }
- rows = append(rows, row)
- }
- return rows, nil
- }
|