| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- package storage
- import (
- "fmt"
- "strings"
- "time"
- )
- // ReferentialAction is one persisted FOREIGN KEY ON DELETE/ON UPDATE action.
- // The catalog stores the SQL spelling so sqlite_master can reproduce the
- // declaration; an omitted clause and NO ACTION behave identically.
- type ReferentialAction string
- const (
- ReferentialActionNoAction ReferentialAction = "NO ACTION"
- ReferentialActionRestrict ReferentialAction = "RESTRICT"
- ReferentialActionCascade ReferentialAction = "CASCADE"
- ReferentialActionSetNull ReferentialAction = "SET NULL"
- ReferentialActionSetDefault ReferentialAction = "SET DEFAULT"
- )
- // DeleteAction returns the action applied when the parent row is deleted.
- func (fk ForeignKeyConstraint) DeleteAction() ReferentialAction {
- return parseReferentialAction(fk.OnDelete)
- }
- // UpdateAction returns the action applied when a referenced parent key changes.
- func (fk ForeignKeyConstraint) UpdateAction() ReferentialAction {
- return parseReferentialAction(fk.OnUpdate)
- }
- // parseReferentialAction maps the persisted spelling to its action. Anything
- // unrecognized is NO ACTION, the default the catalog uses when the clause was
- // omitted, so a hand-edited catalog cannot silently disable enforcement.
- func parseReferentialAction(text string) ReferentialAction {
- switch strings.ToUpper(strings.TrimSpace(text)) {
- case "CASCADE":
- return ReferentialActionCascade
- case "SET NULL":
- return ReferentialActionSetNull
- case "SET DEFAULT":
- return ReferentialActionSetDefault
- case "RESTRICT":
- return ReferentialActionRestrict
- default:
- return ReferentialActionNoAction
- }
- }
- // rewritesReferencingRows reports whether the action changes or removes the
- // child rows instead of rejecting the parent change.
- func (a ReferentialAction) rewritesReferencingRows() bool {
- switch a {
- case ReferentialActionCascade, ReferentialActionSetNull, ReferentialActionSetDefault:
- return true
- default:
- return false
- }
- }
- // HasCascadingReference reports whether table is the referenced parent of a
- // constraint whose DELETE action (forDelete) or UPDATE action rewrites the
- // referencing rows. Callers use it to choose the transactional write path, so a
- // statement whose only actions are NO ACTION/RESTRICT keeps the optimized
- // point-write path.
- func (m *SchemaManager) HasCascadingReference(table string, forDelete bool) (bool, error) {
- refs, err := m.referencingForeignKeys(table)
- if err != nil {
- return false, err
- }
- for _, ref := range refs {
- action := ref.FK.UpdateAction()
- if forDelete {
- action = ref.FK.DeleteAction()
- }
- if action.rewritesReferencingRows() {
- return true, nil
- }
- }
- return false, nil
- }
- // beginCascadingPointWrite decides whether an autocommit point write must be
- // staged so its cascades publish with the parent change, and starts that
- // implicit transaction when it is needed.
- //
- // A table outside the foreign-key graph keeps the point-write fast path and
- // never touches the gate. For a participating table the database-wide
- // foreign-key write gate is taken before the cascade classification and held
- // across Begin and the caller's staging. CREATE TABLE ... REFERENCES takes the
- // same unconditional gate, so a constraint cannot be published between deciding
- // that the write is not cascading and opening the transaction that would have
- // applied its actions. The returned release function must be called before
- // Commit, which acquires the same non-reentrant gate.
- //
- // A table gains its first constraint only together with a new child table,
- // which is created empty under the gate and whose rows take the gate on insert.
- // If the participant probe above goes stale, the non-cascading point path
- // re-reads the catalog under the gate and rejects the write with 23503 instead
- // of ignoring the new constraint.
- //
- // The gate is held while Begin and the caller's staging take the session lock.
- // That order cannot invert with Commit: a Session serves one connection and one
- // statement at a time, so Commit on this Session runs after the staging path
- // releases the gate, while other sessions hold their own lock and contend only
- // on the shared gate.
- func (s *Session) beginCascadingPointWrite(table string, forDelete bool) (bool, func(), error) {
- participates, err := s.schema.ForeignKeyParticipants(table)
- if err != nil {
- return false, nil, err
- }
- if !participates {
- return false, func() {}, nil
- }
- unlockFK := s.table.LockForeignKeyWriteGate()
- cascades, err := s.schema.HasCascadingReference(table, forDelete)
- if err != nil {
- unlockFK()
- return false, nil, err
- }
- if !cascades {
- unlockFK()
- return false, func() {}, nil
- }
- if err := s.Begin(); err != nil {
- unlockFK()
- return false, nil, err
- }
- return true, unlockFK, nil
- }
- // foreignKeyRawValues returns the unformatted values a row holds for the given
- // columns, and reports false when a column is missing. A NULL component is
- // returned as nil: on a parent-side update the cascading action copies the new
- // value, including a NULL that clears the reference.
- func foreignKeyRawValues(row Row, columns []string) ([]interface{}, bool) {
- values := make([]interface{}, len(columns))
- for i, name := range columns {
- value, ok := rowColumnValue(row, name)
- if !ok {
- return nil, false
- }
- values[i] = value
- }
- return values, true
- }
- // foreignKeyActionKey formats a row's values for the cascade event key, keeping
- // a NULL component distinguishable so a key that becomes NULL still changes the
- // event identity.
- func foreignKeyActionKey(row Row, columns []string) ([]string, bool) {
- values := make([]string, len(columns))
- for i, name := range columns {
- value, ok := rowColumnValue(row, name)
- if !ok {
- return nil, false
- }
- if value == nil {
- values[i] = "\x00NULL"
- continue
- }
- values[i] = formatIndexValue(value)
- }
- return values, true
- }
- // cascadeExpansion tracks the referential actions one statement already applied.
- // It is the termination guard for self-references and constraint cycles: a row
- // delete is applied at most once, and one (row, constraint, old key, new key)
- // child update at most once. Because the applied event includes the key values
- // involved, a cycle that revisits a row with a different key still terminates:
- // the set of values a cascade can copy is bounded by the rows it starts from.
- type cascadeExpansion struct {
- applied map[string]bool
- // now is the statement instant used to evaluate a SET DEFAULT expression
- // default, so every cascaded row of one statement sees the same value.
- now time.Time
- }
- func newCascadeExpansion(schema *SchemaManager) *cascadeExpansion {
- return &cascadeExpansion{applied: make(map[string]bool), now: schema.defaultNow()}
- }
- func (e *cascadeExpansion) markDelete(table, key string) bool {
- id := "D\x00" + strings.ToLower(table) + "\x00" + key
- if e.applied[id] {
- return false
- }
- e.applied[id] = true
- return true
- }
- func (e *cascadeExpansion) markUpdate(table, key string, fk ForeignKeyConstraint, oldKey, newKey []string) bool {
- id := "U\x00" + strings.ToLower(table) + "\x00" + key + "\x00" + strings.ToLower(fk.RefTable) + "\x00" + joinForeignKeyValues(fk.Columns) + "\x00" + joinForeignKeyValues(oldKey) + "\x00" + joinForeignKeyValues(newKey)
- if e.applied[id] {
- return false
- }
- e.applied[id] = true
- return true
- }
- // referencingRows returns the effective child rows (durable rows with the
- // transaction overlay applied) that reference key through ref. The caller holds
- // the session lock, and selectLocked registers the read so a concurrent write on
- // the child side invalidates the commit instead of interleaving.
- func (s *Session) referencingRows(ref referencingForeignKey, key []string) ([]Row, error) {
- return s.selectLocked(ref.Table, func(row Row) bool {
- return foreignKeyMatches(row, ref.FK.Columns, key)
- })
- }
- // stageCascadeDelete stages a row delete plus the ON DELETE actions of every
- // constraint that names its table as the parent. The row is staged before the
- // child scan, so a self-referential row is not visited as its own child.
- func (s *Session) stageCascadeDelete(table, key string, row Row, exp *cascadeExpansion) error {
- if !exp.markDelete(table, key) {
- return nil
- }
- if entry, ok := s.overlay[strings.ToLower(table)][key]; ok && entry.absent {
- return nil
- }
- s.stageDelete(table, key, row)
- return s.cascadeAfterDelete(table, row, exp)
- }
- // cascadeAfterDelete applies the ON DELETE actions triggered by removing row
- // from table.
- func (s *Session) cascadeAfterDelete(table string, row Row, exp *cascadeExpansion) error {
- refs, err := s.referencingForeignKeysLocked(table)
- if err != nil {
- return err
- }
- for _, ref := range refs {
- action := ref.FK.DeleteAction()
- if !action.rewritesReferencingRows() {
- continue
- }
- key, ok := foreignKeyValues(row, ref.FK.RefColumns)
- if !ok {
- continue
- }
- children, err := s.referencingRows(ref, key)
- if err != nil {
- return err
- }
- for _, child := range children {
- if err := s.applyDeleteAction(ref, child, action, exp); err != nil {
- return err
- }
- }
- }
- return nil
- }
- // applyDeleteAction performs one row's ON DELETE action. CASCADE removes the
- // child and recurses through its own constraints; SET NULL/SET DEFAULT rewrite
- // the child's foreign-key columns and then propagate as an update, because those
- // columns can themselves be a key another table references.
- func (s *Session) applyDeleteAction(ref referencingForeignKey, child Row, action ReferentialAction, exp *cascadeExpansion) error {
- schema, err := s.getSchemaLocked(ref.Table)
- if err != nil {
- return err
- }
- pk, err := schema.PrimaryKeyForRow(child)
- if err != nil {
- return err
- }
- key := s.table.dataKey(ref.Table, pk)
- if action == ReferentialActionCascade {
- return s.stageCascadeDelete(ref.Table, key, child, exp)
- }
- updated, err := s.applySetAction(schema, ref.FK, child, action, exp)
- if err != nil {
- return err
- }
- return s.stageCascadeUpdate(ref.Table, child, updated, exp)
- }
- // applySetAction returns the child row with its foreign-key columns replaced by
- // NULL or by the column defaults. A column without a default receives NULL, the
- // SQL default, which then fails NOT NULL if the column is required.
- func (s *Session) applySetAction(schema *Schema, fk ForeignKeyConstraint, child Row, action ReferentialAction, exp *cascadeExpansion) (Row, error) {
- updated := cloneRow(child)
- for _, name := range fk.Columns {
- if action == ReferentialActionSetNull {
- updated[name] = nil
- continue
- }
- column, ok := schema.GetColumn(name)
- if !ok {
- return nil, fmt.Errorf("foreign key %s: column %s is missing from table %s", fk.Label(), name, schema.Name)
- }
- value, hasDefault, err := s.schema.columnDefaultValue(*column, exp.now)
- if err != nil {
- return nil, err
- }
- if !hasDefault {
- value = nil
- }
- updated[name] = value
- }
- return updated, nil
- }
- // cascadeAfterUpdate applies the ON UPDATE actions triggered by changing a
- // parent row whose referenced key moved from oldRow to newRow.
- func (s *Session) cascadeAfterUpdate(table string, oldRow, newRow Row, exp *cascadeExpansion) error {
- refs, err := s.referencingForeignKeysLocked(table)
- if err != nil {
- return err
- }
- for _, ref := range refs {
- action := ref.FK.UpdateAction()
- if !action.rewritesReferencingRows() {
- continue
- }
- oldKey, ok := foreignKeyValues(oldRow, ref.FK.RefColumns)
- if !ok {
- continue
- }
- newKey, newOK := foreignKeyActionKey(newRow, ref.FK.RefColumns)
- if newOK && foreignKeyValuesEqual(oldKey, newKey) {
- continue
- }
- parentValues, ok := foreignKeyRawValues(newRow, ref.FK.RefColumns)
- if !ok {
- continue
- }
- children, err := s.referencingRows(ref, oldKey)
- if err != nil {
- return err
- }
- childSchema, err := s.getSchemaLocked(ref.Table)
- if err != nil {
- return err
- }
- for _, child := range children {
- childPK, err := childSchema.PrimaryKeyForRow(child)
- if err != nil {
- return err
- }
- childKey := s.table.dataKey(ref.Table, childPK)
- if !exp.markUpdate(ref.Table, childKey, ref.FK, oldKey, newKey) {
- continue
- }
- var updated Row
- switch action {
- case ReferentialActionCascade:
- updated = cloneRow(child)
- for i, column := range ref.FK.Columns {
- updated[column] = parentValues[i]
- }
- case ReferentialActionSetNull, ReferentialActionSetDefault:
- updated, err = s.applySetAction(childSchema, ref.FK, child, action, exp)
- if err != nil {
- return err
- }
- }
- if err := s.stageCascadeUpdate(ref.Table, child, updated, exp); err != nil {
- return err
- }
- }
- }
- return nil
- }
- // stageCascadeUpdate stages one cascaded row rewrite. The row keeps its key
- // unless a cascaded foreign-key column is part of it, in which case the staged
- // image moves and a collision with another row fails like any primary-key move.
- // Required columns and CHECK predicates are validated immediately so a SET NULL
- // on a NOT NULL column fails at the statement with 23502 instead of surfacing at
- // commit; FK and UNIQUE validity are validated against the transaction's final
- // state at commit.
- func (s *Session) stageCascadeUpdate(table string, oldRow, newRow Row, exp *cascadeExpansion) error {
- schema, err := s.getSchemaLocked(table)
- if err != nil {
- return err
- }
- if err := s.schema.validateRowConstraints(schema, newRow); err != nil {
- return err
- }
- oldPK, err := schema.PrimaryKeyForRow(oldRow)
- if err != nil {
- return err
- }
- newPK, err := schema.PrimaryKeyForRow(newRow)
- if err != nil {
- return err
- }
- oldKey := s.table.dataKey(table, oldPK)
- newKey := s.table.dataKey(table, newPK)
- if oldKey != newKey {
- if _, gerr := s.getByPKLocked(table, newPK); gerr == nil {
- return &PrimaryKeyViolationError{Table: table, Key: newPK}
- } else if gerr != ErrKeyNotFound {
- return gerr
- }
- s.stageDelete(table, oldKey, oldRow)
- }
- s.stagePut(table, newKey, newRow)
- return s.cascadeAfterUpdate(table, oldRow, newRow, exp)
- }
- // rollbackToLocked discards mutations after a savepoint position. The caller
- // holds the session lock.
- func (s *Session) rollbackToLocked(pos int) {
- for i := len(s.log) - 1; i >= pos && i >= 0; i-- {
- m := s.log[i]
- if m.prev == nil {
- delete(s.overlay[m.table], m.key)
- if len(s.overlay[m.table]) == 0 {
- delete(s.overlay, m.table)
- }
- } else {
- if s.overlay[m.table] == nil {
- s.overlay[m.table] = make(map[string]*overlayEntry)
- }
- s.overlay[m.table][m.key] = m.prev
- }
- }
- if pos < len(s.log) {
- s.log = s.log[:pos]
- }
- }
|