| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589 |
- package storage
- import (
- "fmt"
- "sort"
- "strings"
- "time"
- "github.com/goccy/go-json"
- )
- // rowRewrite is one pending ALTER TABLE row transformation. It reports whether
- // it changed the row and must be idempotent: it runs again on every read of the
- // table while the transaction is open and once more on each durable row when
- // the transaction commits.
- type rowRewrite func(row Row) (bool, error)
- // SchemaTx is the transaction-scoped catalog overlay of one Session. DDL inside
- // an explicit transaction records its catalog and row effects here instead of
- // publishing them: the owning session reads through the overlay, every other
- // session keeps reading the committed catalog, ROLLBACK discards it, and COMMIT
- // merges the overlay into the session's single compare-and-swap batch write, so
- // a multi-statement migration becomes durable atomically or not at all.
- //
- // The overlay starts out empty and reads through to the durable catalog while
- // recording the LSN each touched key was observed at. Those LSNs become the
- // commit's compare checks, so a concurrent DDL that committed after the
- // transaction derived its catalog makes the commit fail with ErrSerialization
- // instead of overwriting the concurrent change.
- //
- // Every method assumes the owning session lock is held: a Session executes one
- // statement at a time, so the overlay needs no lock of its own and can share the
- // session's row overlay directly.
- type SchemaTx struct {
- base *SchemaManager
- sess *Session
- // staged holds the transaction's definition of every touched table, keyed
- // by lowercased name. A nil entry means the name is not visible to this
- // transaction: it was dropped, or moved by RENAME TABLE.
- staged map[string]*Schema
- // stagedIndexes holds the transaction's definition of every touched index,
- // keyed by lowercased name; nil means dropped.
- stagedIndexes map[string]*Index
- // committedSchemas and committedIndexes cache the definitions read from
- // durable storage so the executor's repeated catalog lookups inside one
- // statement do not re-read them. An entry is dropped when the transaction
- // stages a replacement for the same name.
- committedSchemas map[string]*Schema
- committedIndexes map[string]*Index
- // tables and indexList are the staged catalog lists. They are loaded from
- // the committed catalog on first use and rewritten in place afterwards.
- tables []string
- indexList []string
- // catalogDirty and indexListDirty report that the corresponding list is
- // part of the commit.
- catalogDirty bool
- indexListDirty bool
- // reads records the durable LSN every staged catalog key was derived from
- // (0 for a key that was absent). commitChecks turns them into compare
- // checks.
- reads map[string]uint64
- // rewrites holds pending row transformations keyed by the lowercased table
- // name whose rows they apply to, in statement order.
- rewrites map[string][]rowRewrite
- // dataSources maps a staged table name to the durable table name whose data
- // prefix still holds its rows: RENAME TABLE moves the definition without
- // touching rows, so a later ALTER or DROP must look under the old prefix.
- // An entry with an empty source is a table created by this transaction.
- dataSources map[string]string
- // drops holds durable table names whose data prefix is deleted at commit.
- drops map[string]bool
- // renames maps a durable table name to the staged name its rows move to at
- // commit.
- renames map[string]string
- // rekeys records the overlay re-keyings this transaction performed, in
- // order, so a savepoint can undo them.
- rekeys []rekeyPair
- version uint64
- }
- type rekeyPair struct {
- from string
- to string
- }
- // schemaTxSnapshot is the transaction catalog state at a savepoint.
- type schemaTxSnapshot struct {
- version uint64
- staged map[string]*Schema
- stagedIndexes map[string]*Index
- tables []string
- indexList []string
- catalogDirty bool
- indexListDirty bool
- rewrites map[string][]rowRewrite
- dataSources map[string]string
- drops map[string]bool
- renames map[string]string
- rekeys int
- }
- func newSchemaTx(s *Session) *SchemaTx {
- return &SchemaTx{
- base: s.schema,
- sess: s,
- staged: make(map[string]*Schema),
- stagedIndexes: make(map[string]*Index),
- committedSchemas: make(map[string]*Schema),
- committedIndexes: make(map[string]*Index),
- reads: make(map[string]uint64),
- rewrites: make(map[string][]rowRewrite),
- dataSources: make(map[string]string),
- drops: make(map[string]bool),
- renames: make(map[string]string),
- version: s.schema.Version(),
- }
- }
- // snapshot captures the catalog overlay. The maps are copied shallowly: staged
- // definitions are cloned before they are mutated, so the captured pointers stay
- // immutable.
- func (tx *SchemaTx) snapshot() schemaTxSnapshot {
- snap := schemaTxSnapshot{
- version: tx.version,
- staged: make(map[string]*Schema, len(tx.staged)),
- stagedIndexes: make(map[string]*Index, len(tx.stagedIndexes)),
- tables: append([]string(nil), tx.tables...),
- indexList: append([]string(nil), tx.indexList...),
- catalogDirty: tx.catalogDirty,
- indexListDirty: tx.indexListDirty,
- rewrites: make(map[string][]rowRewrite, len(tx.rewrites)),
- dataSources: make(map[string]string, len(tx.dataSources)),
- drops: make(map[string]bool, len(tx.drops)),
- renames: make(map[string]string, len(tx.renames)),
- rekeys: len(tx.rekeys),
- }
- for k, v := range tx.staged {
- snap.staged[k] = v
- }
- for k, v := range tx.stagedIndexes {
- snap.stagedIndexes[k] = v
- }
- for k, v := range tx.rewrites {
- snap.rewrites[k] = append([]rowRewrite(nil), v...)
- }
- for k, v := range tx.dataSources {
- snap.dataSources[k] = v
- }
- for k, v := range tx.drops {
- snap.drops[k] = v
- }
- for k, v := range tx.renames {
- snap.renames[k] = v
- }
- return snap
- }
- // restore returns the overlay to a snapshot and undoes the overlay re-keyings
- // performed after it. The caller holds the session lock.
- func (tx *SchemaTx) restore(snap schemaTxSnapshot) {
- for i := len(tx.rekeys) - 1; i >= snap.rekeys; i-- {
- tx.sess.rekeyOverlayLocked(tx.rekeys[i].to, tx.rekeys[i].from)
- }
- tx.rekeys = tx.rekeys[:snap.rekeys]
- tx.version = snap.version
- tx.staged = snap.staged
- tx.stagedIndexes = snap.stagedIndexes
- tx.tables = snap.tables
- tx.indexList = snap.indexList
- tx.catalogDirty = snap.catalogDirty
- tx.indexListDirty = snap.indexListDirty
- tx.rewrites = snap.rewrites
- tx.dataSources = snap.dataSources
- tx.drops = snap.drops
- tx.renames = snap.renames
- }
- // bump advances the transaction's catalog version so the executor's analyzer
- // catalog resynchronizes after every staged change.
- func (tx *SchemaTx) bump() {
- tx.version++
- }
- func (tx *SchemaTx) recordRead(key string, lsn uint64) {
- if _, ok := tx.reads[key]; !ok {
- tx.reads[key] = lsn
- }
- }
- // refreshCommitted drops the cached committed definitions before the commit's
- // final validation. Another session's DDL may have committed while this
- // transaction was open, so the rows it stages must be validated against the
- // catalog that is current under the commit gates; the staged overlay itself is
- // kept. The catalog lists keep their recorded LSN: a staged list write must
- // still fail its compare check rather than overwrite the concurrent change.
- func (tx *SchemaTx) refreshCommitted() {
- tx.committedSchemas = make(map[string]*Schema)
- tx.committedIndexes = make(map[string]*Index)
- if !tx.indexListDirty {
- tx.indexList = nil
- }
- }
- // getSchema returns the transaction's view of a table.
- func (tx *SchemaTx) getSchema(name string) (*Schema, error) {
- lower := strings.ToLower(name)
- if schema, ok := tx.staged[lower]; ok {
- if schema == nil {
- return nil, fmt.Errorf("table not found: %s", name)
- }
- return cloneSchema(schema), nil
- }
- return tx.loadSchema(name)
- }
- // loadSchema reads the committed definition and records the LSN it observed.
- func (tx *SchemaTx) loadSchema(name string) (*Schema, error) {
- lower := strings.ToLower(name)
- if schema, ok := tx.committedSchemas[lower]; ok {
- return cloneSchema(schema), nil
- }
- key := tx.base.schemaKey(name)
- data, lsn, found, err := tx.base.readDurable(key)
- if err != nil {
- return nil, err
- }
- tx.recordRead(key, lsn)
- if !found {
- return nil, fmt.Errorf("table not found: %s", name)
- }
- var schema Schema
- if err := json.Unmarshal([]byte(data), &schema); err != nil {
- return nil, fmt.Errorf("failed to parse schema: %w", err)
- }
- schema.normalizePrimaryKey()
- tx.committedSchemas[lower] = &schema
- return cloneSchema(&schema), nil
- }
- func (tx *SchemaTx) tableExists(name string) bool {
- _, err := tx.getSchema(name)
- return err == nil
- }
- // listTables returns the staged table catalog in creation order.
- func (tx *SchemaTx) listTables() ([]string, error) {
- if tx.tables == nil {
- tables, lsn, err := tx.base.catalogTablesDurable()
- if err != nil {
- return nil, err
- }
- tx.recordRead(tx.base.catalogKey(), lsn)
- tx.tables = tables
- }
- return append([]string(nil), tx.tables...), nil
- }
- func (tx *SchemaTx) stageTableList(tables []string) {
- tx.tables = tables
- tx.catalogDirty = true
- }
- // getIndex returns the transaction's view of an index definition.
- func (tx *SchemaTx) getIndex(name string) (*Index, error) {
- lower := strings.ToLower(name)
- if index, ok := tx.stagedIndexes[lower]; ok {
- if index == nil {
- return nil, fmt.Errorf("%w: %s", ErrIndexNotFound, name)
- }
- return cloneIndex(index), nil
- }
- if index, ok := tx.committedIndexes[lower]; ok {
- return cloneIndex(index), nil
- }
- key := tx.base.indexKey(name)
- data, lsn, found, err := tx.base.readDurable(key)
- if err != nil {
- return nil, err
- }
- tx.recordRead(key, lsn)
- if !found {
- return nil, fmt.Errorf("%w: %s", ErrIndexNotFound, name)
- }
- var index Index
- if err := json.Unmarshal([]byte(data), &index); err != nil {
- return nil, fmt.Errorf("failed to parse index: %w", err)
- }
- tx.committedIndexes[lower] = &index
- return cloneIndex(&index), nil
- }
- func (tx *SchemaTx) indexExists(name string) bool {
- _, err := tx.getIndex(name)
- return err == nil
- }
- func (tx *SchemaTx) listIndexes() ([]string, error) {
- if tx.indexList == nil {
- indexes, lsn, err := tx.base.indexListDurable()
- if err != nil {
- return nil, err
- }
- tx.recordRead(tx.base.indexListKey(), lsn)
- tx.indexList = indexes
- }
- return append([]string(nil), tx.indexList...), nil
- }
- func (tx *SchemaTx) stageIndexList(indexes []string) {
- tx.indexList = indexes
- tx.indexListDirty = true
- }
- func (tx *SchemaTx) listTableIndexes(table string) ([]*Index, error) {
- names, err := tx.listIndexes()
- if err != nil {
- return nil, err
- }
- var result []*Index
- for _, name := range names {
- idx, err := tx.getIndex(name)
- if err != nil {
- if err == ErrIndexNotFound || strings.Contains(err.Error(), ErrIndexNotFound.Error()) {
- continue
- }
- return nil, err
- }
- if strings.EqualFold(idx.Table, table) {
- result = append(result, idx)
- }
- }
- return result, nil
- }
- // stageSchema records a replacement table definition.
- func (tx *SchemaTx) stageSchema(schema *Schema) {
- lower := strings.ToLower(schema.Name)
- delete(tx.committedSchemas, lower)
- tx.staged[lower] = cloneSchema(schema)
- if _, ok := tx.dataSources[lower]; !ok {
- tx.dataSources[lower] = lower
- }
- tx.recordRead(tx.base.schemaKey(schema.Name), 0)
- tx.bump()
- }
- // stageDroppedTable records that a name stopped being addressable.
- func (tx *SchemaTx) stageDroppedTable(lower string) {
- delete(tx.committedSchemas, lower)
- tx.staged[lower] = nil
- tx.recordRead(tx.base.schemaKey(lower), 0)
- tx.bump()
- }
- // createTable stages CREATE TABLE.
- func (tx *SchemaTx) createTable(schema *Schema) error {
- if tx.tableExists(schema.Name) {
- return fmt.Errorf("table already exists: %s", schema.Name)
- }
- lower := strings.ToLower(schema.Name)
- schema = cloneSchema(schema)
- schema.CreatedAt = time.Now()
- schema.normalizePrimaryKey()
- tx.staged[lower] = schema
- tx.dataSources[lower] = ""
- tables, err := tx.listTables()
- if err != nil {
- return err
- }
- for _, name := range tables {
- if strings.EqualFold(name, schema.Name) {
- return fmt.Errorf("table already exists: %s", schema.Name)
- }
- }
- tx.stageTableList(append(tables, schema.Name))
- tx.bump()
- return nil
- }
- // createIndex stages CREATE INDEX.
- func (tx *SchemaTx) createIndex(index *Index) error {
- if tx.indexExists(index.Name) {
- return fmt.Errorf("index already exists: %s", index.Name)
- }
- if !tx.tableExists(index.Table) {
- return fmt.Errorf("table not found: %s", index.Table)
- }
- index = cloneIndex(index)
- index.CreatedAt = time.Now()
- delete(tx.committedIndexes, strings.ToLower(index.Name))
- tx.stagedIndexes[strings.ToLower(index.Name)] = index
- names, err := tx.listIndexes()
- if err != nil {
- return err
- }
- tx.stageIndexList(append(names, index.Name))
- tx.recordRead(tx.base.indexKey(index.Name), 0)
- tx.bump()
- return nil
- }
- // dropIndex stages DROP INDEX.
- func (tx *SchemaTx) dropIndex(name string) error {
- index, err := tx.getIndex(name)
- if err != nil {
- return fmt.Errorf("index not found: %s", name)
- }
- lower := strings.ToLower(name)
- delete(tx.committedIndexes, lower)
- tx.stagedIndexes[lower] = nil
- names, err := tx.listIndexes()
- if err != nil {
- return err
- }
- filtered := make([]string, 0, len(names))
- for _, existing := range names {
- if !strings.EqualFold(existing, name) {
- filtered = append(filtered, existing)
- }
- }
- tx.stageIndexList(filtered)
- tx.recordRead(tx.base.indexKey(index.Name), 0)
- tx.bump()
- return nil
- }
- // dropTable stages DROP TABLE. The durable rows are removed by the commit; the
- // session's staged rows for the table are discarded immediately because the
- // table is no longer visible to this transaction.
- func (tx *SchemaTx) dropTable(name string) error {
- lower := strings.ToLower(name)
- if _, err := tx.getSchema(name); err != nil {
- return fmt.Errorf("table not found: %s", name)
- }
- source := tx.durableSource(lower)
- if source != "" {
- tx.drops[source] = true
- }
- delete(tx.rewrites, lower)
- delete(tx.dataSources, lower)
- // A name that was renamed inside this transaction has no durable record of
- // its own; the rename already deleted the original key at commit time.
- if _, renamed := tx.renames[lower]; !renamed {
- tx.recordRead(tx.base.schemaKey(lower), 0)
- }
- tx.stageDroppedTable(lower)
- for _, idx := range tx.tableIndexes(lower) {
- if err := tx.dropIndex(idx.Name); err != nil {
- return err
- }
- }
- tables, err := tx.listTables()
- if err != nil {
- return err
- }
- filtered := make([]string, 0, len(tables))
- for _, existing := range tables {
- if !strings.EqualFold(existing, name) {
- filtered = append(filtered, existing)
- }
- }
- tx.stageTableList(filtered)
- tx.sess.dropOverlayTableLocked(lower)
- tx.sess.markTableScanned(lower)
- return nil
- }
- // durableSource returns the committed table name whose data prefix holds the
- // rows of a staged table, or "" when the table was created by this transaction.
- func (tx *SchemaTx) durableSource(lower string) string {
- if source, ok := tx.dataSources[lower]; ok {
- return source
- }
- return lower
- }
- // tableIndexes returns the staged index definitions of a table.
- func (tx *SchemaTx) tableIndexes(lower string) []*Index {
- names, err := tx.listIndexes()
- if err != nil {
- return nil
- }
- var result []*Index
- for _, name := range names {
- idx, err := tx.getIndex(name)
- if err != nil || !strings.EqualFold(idx.Table, lower) {
- continue
- }
- result = append(result, idx)
- }
- return result
- }
- // addColumn stages ALTER TABLE ADD COLUMN. The new column, its CHECK
- // predicates, its UNIQUE index, and the default backfill of existing rows are
- // validated against the rows this session sees (durable rows plus its own
- // staged writes) before anything is staged, so a rejected DDL leaves the
- // transaction's catalog untouched.
- func (tx *SchemaTx) addColumn(table string, column Column, checks []CheckConstraint, unique *Index) error {
- schema, err := tx.getSchema(table)
- if err != nil {
- return err
- }
- if err := checkColumnAbsent(schema, column.Name); err != nil {
- return err
- }
- if unique != nil {
- if !strings.EqualFold(unique.Table, schema.Name) {
- return fmt.Errorf("index %s belongs to table %s, not %s", unique.Name, unique.Table, schema.Name)
- }
- if tx.indexExists(unique.Name) {
- return fmt.Errorf("index already exists: %s", unique.Name)
- }
- }
- now := tx.base.defaultNow()
- // Validation covers the committed rows, mirroring the autocommit DDL path:
- // rows this transaction staged are re-validated against the staged schema at
- // commit (applyOverlaySchemaDefaults), because a staged row may still be
- // rewritten or removed before then.
- rows, err := tx.durableRows(table)
- if err != nil {
- return err
- }
- if !column.Nullable && !columnHasDefault(column) && len(rows) > 0 {
- return &NotNullViolationError{Table: schema.Name, Column: column.Name}
- }
- eval := tx.base.checkEvaluator()
- if len(checks) > 0 && eval == nil {
- return fmt.Errorf("cannot validate CHECK constraints on %s: no CHECK evaluator installed", table)
- }
- seen := make(map[string]struct{})
- for _, row := range rows {
- value, present := rowColumnValue(row, column.Name)
- if !present {
- if fallback, has, defErr := tx.base.columnDefaultValue(column, now); defErr != nil {
- return defErr
- } else if has {
- value = fallback
- present = true
- }
- }
- if len(checks) > 0 {
- probe := cloneRow(row)
- if present {
- probe[column.Name] = value
- }
- for _, check := range checks {
- holds, evalErr := eval(check.Expression, probe)
- if evalErr != nil {
- return fmt.Errorf("evaluate CHECK constraint %s on %s: %w", check.Label(), table, evalErr)
- }
- if !holds {
- return &CheckViolationError{Table: table, Constraint: check.Label()}
- }
- }
- }
- if unique != nil && value != nil {
- encoded := encodeUniqueScalar(value)
- if _, duplicate := seen[encoded]; duplicate {
- return &UniqueViolationError{Constraint: unique.Name}
- }
- seen[encoded] = struct{}{}
- }
- }
- schema = cloneSchema(schema)
- schema.Columns = append(schema.Columns, column)
- schema.Checks = append(schema.Checks, checks...)
- tx.stageSchema(schema)
- if unique != nil {
- tx.stagedIndexes[strings.ToLower(unique.Name)] = cloneIndex(unique)
- names, err := tx.listIndexes()
- if err != nil {
- return err
- }
- tx.stageIndexList(append(names, unique.Name))
- tx.recordRead(tx.base.indexKey(unique.Name), 0)
- }
- if columnHasDefault(column) {
- value, has, err := tx.base.columnDefaultValue(column, now)
- if err != nil {
- return err
- }
- if has {
- name := column.Name
- tx.addRewrite(table, func(row Row) (bool, error) {
- if _, ok := rowColumnValue(row, name); ok {
- return false, nil
- }
- row[name] = value
- return true, nil
- })
- }
- }
- tx.sess.markTableScanned(strings.ToLower(table))
- return nil
- }
- // durableRows returns the committed rows of a table, ignoring this
- // transaction's staged rows and pending rewrites.
- func (tx *SchemaTx) durableRows(table string) ([]Row, error) {
- var rows []Row
- prefix := tx.sess.table.dataPrefix(tx.durableSource(strings.ToLower(table)))
- err := tx.sess.table.scanRowsAtPrefix(prefix, func(_ string, row Row, _ uint64) (bool, error) {
- rows = append(rows, row)
- return false, nil
- })
- return rows, err
- }
- // dropColumn stages ALTER TABLE DROP COLUMN.
- func (tx *SchemaTx) dropColumn(table, columnName string) error {
- schema, err := tx.getSchema(table)
- if err != nil {
- return err
- }
- if schema.IsPrimaryKeyColumn(columnName) {
- return fmt.Errorf("cannot drop primary key column: %s", columnName)
- }
- columns := make([]Column, 0, len(schema.Columns)-1)
- found := false
- for _, column := range schema.Columns {
- if strings.EqualFold(column.Name, columnName) {
- found = true
- continue
- }
- columns = append(columns, column)
- }
- if !found {
- return fmt.Errorf("column not found: %s", columnName)
- }
- schema = cloneSchema(schema)
- schema.Columns = columns
- tx.stageSchema(schema)
- tx.addRewrite(table, func(row Row) (bool, error) {
- for key := range row {
- if strings.EqualFold(key, columnName) {
- delete(row, key)
- return true, nil
- }
- }
- return false, nil
- })
- tx.sess.markTableScanned(strings.ToLower(table))
- return nil
- }
- // renameColumn stages ALTER TABLE RENAME COLUMN, including the column
- // references of the table's index definitions.
- func (tx *SchemaTx) renameColumn(table, oldName, newName string) error {
- schema, err := tx.getSchema(table)
- if err != nil {
- return err
- }
- for _, column := range schema.Columns {
- if strings.EqualFold(column.Name, newName) {
- return fmt.Errorf("column already exists: %s", newName)
- }
- }
- schema = cloneSchema(schema)
- found := false
- for i := range schema.Columns {
- if !strings.EqualFold(schema.Columns[i].Name, oldName) {
- continue
- }
- schema.Columns[i].Name = newName
- found = true
- for j, pk := range schema.PrimaryKeyColumns {
- if strings.EqualFold(pk, oldName) {
- schema.PrimaryKeyColumns[j] = newName
- }
- }
- if strings.EqualFold(schema.PrimaryKey, oldName) {
- schema.PrimaryKey = newName
- }
- break
- }
- if !found {
- return fmt.Errorf("column not found: %s", oldName)
- }
- // CHECK predicates persist SQL text and are not rewritten; the executor
- // rejects a rename that one depends on, matching the autocommit path.
- tx.stageSchema(schema)
- for _, idx := range tx.tableIndexes(strings.ToLower(table)) {
- changed := false
- updated := cloneIndex(idx)
- for i := range updated.Columns {
- if updated.Columns[i].Expression == "" && strings.EqualFold(updated.Columns[i].Name, oldName) {
- updated.Columns[i].Name = newName
- changed = true
- }
- }
- if !changed {
- continue
- }
- tx.stagedIndexes[strings.ToLower(updated.Name)] = updated
- tx.recordRead(tx.base.indexKey(updated.Name), 0)
- }
- tx.addRewrite(table, func(row Row) (bool, error) {
- for key, value := range row {
- if strings.EqualFold(key, oldName) {
- delete(row, key)
- row[newName] = value
- return true, nil
- }
- }
- return false, nil
- })
- tx.sess.markTableScanned(strings.ToLower(table))
- return nil
- }
- // renameTable stages ALTER TABLE RENAME TABLE. Rows stay under their committed
- // data prefix until commit; the session overlay is re-keyed immediately so the
- // transaction reads its own writes under the new name.
- func (tx *SchemaTx) renameTable(oldName, newName string) error {
- schema, err := tx.getSchema(oldName)
- if err != nil {
- return err
- }
- if tx.tableExists(newName) {
- return fmt.Errorf("table already exists: %s", newName)
- }
- oldLower := strings.ToLower(oldName)
- newLower := strings.ToLower(newName)
- source := tx.durableSource(oldLower)
- schema = cloneSchema(schema)
- schema.Name = newName
- delete(tx.committedSchemas, oldLower)
- delete(tx.committedSchemas, newLower)
- tx.staged[oldLower] = nil
- tx.staged[newLower] = schema
- if source == "" {
- tx.dataSources[newLower] = ""
- } else {
- tx.dataSources[newLower] = source
- tx.renames[source] = newName
- }
- delete(tx.dataSources, oldLower)
- // A drop staged before the rename keeps its durable source; the rename
- // moves that source instead, so the drop no longer applies to it.
- delete(tx.drops, newLower)
- if rewrites, ok := tx.rewrites[oldLower]; ok {
- delete(tx.rewrites, oldLower)
- tx.rewrites[newLower] = rewrites
- }
- for _, idx := range tx.tableIndexes(oldLower) {
- updated := cloneIndex(idx)
- updated.Table = newName
- tx.stagedIndexes[strings.ToLower(updated.Name)] = updated
- tx.recordRead(tx.base.indexKey(updated.Name), 0)
- }
- tables, err := tx.listTables()
- if err != nil {
- return err
- }
- found := false
- for i, name := range tables {
- if strings.EqualFold(name, oldName) {
- tables[i] = newName
- found = true
- break
- }
- }
- if !found {
- return fmt.Errorf("table not found in catalog: %s", oldName)
- }
- tx.stageTableList(tables)
- tx.rekeys = append(tx.rekeys, rekeyPair{from: oldLower, to: newLower})
- tx.sess.rekeyOverlayLocked(oldLower, newLower)
- source = tx.durableSource(newLower)
- if source != "" {
- tx.sess.markTableScanned(source)
- }
- tx.bump()
- return nil
- }
- // addRewrite appends a row transformation for a table.
- func (tx *SchemaTx) addRewrite(table string, rewrite rowRewrite) {
- lower := strings.ToLower(table)
- tx.rewrites[lower] = append(tx.rewrites[lower], rewrite)
- }
- // rewriteRow applies every pending transformation of a table to a row.
- func (tx *SchemaTx) rewriteRow(table string, row Row) (bool, error) {
- rewrites, ok := tx.rewrites[strings.ToLower(table)]
- if !ok {
- return false, nil
- }
- changed := false
- for _, rewrite := range rewrites {
- applied, err := rewrite(row)
- if err != nil {
- return changed, err
- }
- changed = changed || applied
- }
- return changed, nil
- }
- // commitCatalogOps returns the durable catalog writes of the transaction and
- // the compare checks that keep them from overwriting a concurrent commit. The
- // caller holds the session lock and the affected table gates.
- func (tx *SchemaTx) commitCatalogOps() ([]BatchOp, []CompareCheck, error) {
- var ops []BatchOp
- keys := make([]string, 0, len(tx.staged)+len(tx.stagedIndexes)+2)
- for lower, schema := range tx.staged {
- if schema == nil {
- if _, renamed := tx.renames[lower]; renamed {
- // RENAME TABLE names the source through renames; the delete of
- // the old schema record is emitted below.
- continue
- }
- ops = append(ops,
- BatchOp{Op: batchDelete, Key: []byte(tx.base.schemaKey(lower))},
- BatchOp{Op: batchDelete, Key: []byte(tx.base.rowIDKey(lower))},
- )
- // The ROWID key is an in-memory allocator input, not catalog state a
- // concurrent statement can change, so its delete needs no compare.
- keys = append(keys, tx.base.schemaKey(lower))
- continue
- }
- data, err := json.Marshal(schema)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to serialize schema: %w", err)
- }
- key := tx.base.schemaKey(schema.Name)
- ops = append(ops, BatchOp{Op: batchPut, Key: []byte(key), Value: data})
- keys = append(keys, key)
- }
- // A staged table definition whose name replaced an older one deletes the
- // old record in the same batch.
- for source, newName := range tx.renames {
- ops = append(ops, BatchOp{Op: batchDelete, Key: []byte(tx.base.schemaKey(source))})
- keys = append(keys, tx.base.schemaKey(source))
- if !strings.EqualFold(source, newName) {
- ops = append(ops, BatchOp{Op: batchDelete, Key: []byte(tx.base.rowIDKey(source))})
- }
- }
- for lower, index := range tx.stagedIndexes {
- if index == nil {
- ops = append(ops, BatchOp{Op: batchDelete, Key: []byte(tx.base.indexKey(lower))})
- keys = append(keys, tx.base.indexKey(lower))
- continue
- }
- data, err := json.Marshal(index)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to serialize index: %w", err)
- }
- key := tx.base.indexKey(index.Name)
- ops = append(ops, BatchOp{Op: batchPut, Key: []byte(key), Value: data})
- keys = append(keys, key)
- }
- if tx.catalogDirty {
- data, err := json.Marshal(tx.tables)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to serialize catalog: %w", err)
- }
- ops = append(ops, BatchOp{Op: batchPut, Key: []byte(tx.base.catalogKey()), Value: data})
- keys = append(keys, tx.base.catalogKey())
- }
- if tx.indexListDirty {
- data, err := json.Marshal(tx.indexList)
- if err != nil {
- return nil, nil, fmt.Errorf("failed to serialize index list: %w", err)
- }
- ops = append(ops, BatchOp{Op: batchPut, Key: []byte(tx.base.indexListKey()), Value: data})
- keys = append(keys, tx.base.indexListKey())
- }
- checks := make([]CompareCheck, 0, len(keys))
- seen := make(map[string]bool, len(keys))
- for _, key := range keys {
- if seen[key] {
- continue
- }
- seen[key] = true
- checks = append(checks, CompareCheck{Key: []byte(key), LSN: tx.reads[key]})
- }
- return ops, checks, nil
- }
- // commitDataOps returns the durable row writes of the transaction: dropped
- // tables lose every row, renamed tables move to their new prefix, and ALTER
- // TABLE transformations are applied to the committed row images. Rows this
- // session staged in its overlay are published from the overlay instead, so they
- // are skipped here. The caller holds the session lock and the exclusive table
- // gates of every affected table.
- func (tx *SchemaTx) commitDataOps() ([]BatchOp, map[string]map[string]*overlayEntry, error) {
- entries := make(map[string]map[string]*overlayEntry)
- var ops []BatchOp
- if len(tx.drops) == 0 && len(tx.renames) == 0 && len(tx.rewrites) == 0 {
- return nil, nil, nil
- }
- affected := make(map[string]bool)
- for table := range tx.drops {
- affected[table] = true
- }
- for table := range tx.renames {
- affected[table] = true
- }
- for table := range tx.rewrites {
- if source := tx.durableSource(table); source != "" && !tx.drops[source] {
- affected[source] = true
- }
- }
- names := make([]string, 0, len(affected))
- for table := range affected {
- names = append(names, table)
- }
- sort.Strings(names)
- for _, source := range names {
- if tx.drops[source] {
- dropped, err := tx.scanRowsForDDL(source, func(key string, row Row) (bool, Row, error) {
- if tx.sess.overlayHolds(strings.ToLower(source), key) {
- return false, nil, nil
- }
- return true, nil, nil
- })
- if err != nil {
- return nil, nil, err
- }
- ops = append(ops, dropped...)
- continue
- }
- newName, renamed := tx.renames[source]
- if renamed {
- updated, err := tx.scanRowsForDDL(source, func(key string, row Row) (bool, Row, error) {
- if _, err := tx.rewriteRow(newName, row); err != nil {
- return false, nil, err
- }
- newKey := tx.sess.table.dataKey(newName, mustRowPK(tx, newName, row))
- if tx.sess.overlayHolds(strings.ToLower(newName), newKey) {
- return false, nil, nil
- }
- return true, row, nil
- })
- if err != nil {
- return nil, nil, err
- }
- lower := strings.ToLower(newName)
- if entries[lower] == nil {
- entries[lower] = make(map[string]*overlayEntry)
- }
- for _, op := range updated {
- oldKey := op.Key
- suffix := strings.TrimPrefix(string(oldKey), tx.sess.table.dataPrefix(source))
- newKey := []byte(tx.sess.table.dataPrefix(newName) + suffix)
- row, err := decodeRow(op.Value)
- if err != nil {
- return nil, nil, err
- }
- entries[lower][string(newKey)] = &overlayEntry{row: row}
- ops = append(ops,
- BatchOp{Op: batchPut, Key: newKey, Value: op.Value},
- BatchOp{Op: batchDelete, Key: oldKey},
- )
- }
- continue
- }
- rewrites := tx.rewrites[source]
- if len(rewrites) == 0 {
- continue
- }
- updated, err := tx.scanRowsForDDL(source, func(key string, row Row) (bool, Row, error) {
- if tx.sess.overlayHolds(strings.ToLower(source), key) {
- return false, nil, nil
- }
- changed, err := tx.rewriteRow(source, row)
- if err != nil {
- return false, nil, err
- }
- return changed, row, nil
- })
- if err != nil {
- return nil, nil, err
- }
- lower := strings.ToLower(source)
- if entries[lower] == nil {
- entries[lower] = make(map[string]*overlayEntry)
- }
- for _, op := range updated {
- row, err := decodeRow(op.Value)
- if err != nil {
- return nil, nil, err
- }
- entries[lower][string(op.Key)] = &overlayEntry{row: row}
- ops = append(ops, op)
- }
- }
- return ops, entries, nil
- }
- // mustRowPK derives the primary-key string of a row the transaction's staged
- // schema describes.
- func mustRowPK(tx *SchemaTx, table string, row Row) string {
- schema, err := tx.getSchema(table)
- if err != nil {
- return ""
- }
- pk, err := schema.PrimaryKeyForRow(row)
- if err != nil {
- return ""
- }
- return pk
- }
- // scanRowsForDDL streams a durable table's rows through transform. When
- // transform reports a change the row is re-encoded and returned as a put under
- // its existing key; when it reports removal the key is returned as a delete.
- func (tx *SchemaTx) scanRowsForDDL(table string, transform func(key string, row Row) (write bool, replacement Row, err error)) ([]BatchOp, error) {
- var ops []BatchOp
- prefix := []byte(tx.sess.table.dataPrefix(table))
- err := tx.base.pool.WithClient(func(client *KVClient) (retErr error) {
- cursor, err := client.Scan(prefix)
- if err != nil {
- return err
- }
- defer func() {
- if cerr := cursor.Close(); retErr == nil {
- retErr = cerr
- }
- }()
- for {
- entries, done, err := cursor.Next()
- if err != nil {
- return err
- }
- for _, entry := range entries {
- row, err := decodeRow(entry.Value)
- if err != nil {
- return err
- }
- write, replacement, err := transform(string(entry.Key), row)
- if err != nil {
- return err
- }
- if !write {
- continue
- }
- if replacement == nil {
- ops = append(ops, BatchOp{Op: batchDelete, Key: append([]byte(nil), entry.Key...)})
- continue
- }
- value, err := encodeRow(replacement)
- if err != nil {
- return err
- }
- ops = append(ops, BatchOp{Op: batchPut, Key: append([]byte(nil), entry.Key...), Value: value})
- }
- if done {
- return nil
- }
- }
- })
- return ops, err
- }
- // touchedTables lists the lowercased table names whose caches the commit must
- // invalidate, including the source names rows moved away from.
- func (tx *SchemaTx) touchedTables() []string {
- set := make(map[string]bool)
- for lower, schema := range tx.staged {
- set[lower] = true
- if schema != nil {
- set[strings.ToLower(schema.Name)] = true
- if source, ok := tx.dataSources[lower]; ok && source != "" {
- set[source] = true
- }
- }
- }
- for table := range tx.drops {
- set[table] = true
- }
- for table := range tx.renames {
- set[table] = true
- }
- for table := range tx.rewrites {
- set[table] = true
- if source := tx.durableSource(table); source != "" {
- set[source] = true
- }
- }
- names := make([]string, 0, len(set))
- for table := range set {
- names = append(names, table)
- }
- sort.Strings(names)
- return names
- }
- // touchedIndexes lists the lowercased index names whose caches the commit must
- // reset.
- func (tx *SchemaTx) touchedIndexes() []string {
- names := make([]string, 0, len(tx.stagedIndexes))
- for lower := range tx.stagedIndexes {
- names = append(names, lower)
- }
- sort.Strings(names)
- return names
- }
- // hasDataEffects reports whether the commit must rewrite or remove durable rows.
- func (tx *SchemaTx) hasDataEffects() bool {
- return len(tx.drops) > 0 || len(tx.renames) > 0 || len(tx.rewrites) > 0
- }
- // hasDDL reports whether the transaction staged a catalog change that the
- // commit has to publish.
- func (tx *SchemaTx) hasDDL() bool {
- return len(tx.staged) > 0 || len(tx.stagedIndexes) > 0 || tx.catalogDirty || tx.indexListDirty || tx.hasDataEffects()
- }
- // Session catalog access
- //
- // A Session is the catalog view of one SQL connection. Outside an explicit
- // transaction every accessor delegates to the shared SchemaManager; while a
- // transaction is open the accessors resolve against its SchemaTx overlay, so
- // the session reads its own DDL without publishing it.
- // GetSchema returns the table definition this session observes.
- func (s *Session) GetSchema(name string) (*Schema, error) {
- s.mu.Lock()
- defer s.mu.Unlock()
- return s.getSchemaLocked(name)
- }
- // getSchemaLocked resolves a table definition (caller holds the session lock).
- func (s *Session) getSchemaLocked(name string) (*Schema, error) {
- if s.schemaTx != nil {
- return s.schemaTx.getSchema(name)
- }
- return s.schema.GetSchema(name)
- }
- // TableExists reports whether the session can resolve a table.
- func (s *Session) TableExists(name string) bool {
- _, err := s.GetSchema(name)
- return err == nil
- }
- // ListTables returns the table catalog this session observes.
- func (s *Session) ListTables() ([]string, error) {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx != nil {
- return s.schemaTx.listTables()
- }
- return s.schema.ListTables()
- }
- // GetIndex returns an index definition this session observes.
- func (s *Session) GetIndex(name string) (*Index, error) {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx != nil {
- return s.schemaTx.getIndex(name)
- }
- return s.schema.GetIndex(name)
- }
- // IndexExists reports whether the session can resolve an index.
- func (s *Session) IndexExists(name string) bool {
- _, err := s.GetIndex(name)
- return err == nil
- }
- // ListIndexes returns the index catalog this session observes.
- func (s *Session) ListIndexes() ([]string, error) {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx != nil {
- return s.schemaTx.listIndexes()
- }
- return s.schema.ListIndexes()
- }
- // ListTableIndexes returns the index definitions of a table as this session
- // observes them.
- func (s *Session) ListTableIndexes(table string) ([]*Index, error) {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx != nil {
- return s.schemaTx.listTableIndexes(table)
- }
- return s.schema.ListTableIndexes(table)
- }
- // Version identifies the catalog generation this session observes, including
- // staged DDL, so the executor resynchronizes its analyzer catalog after a
- // transactional DDL statement and again after the overlay is discarded.
- func (s *Session) Version() uint64 {
- s.mu.Lock()
- defer s.mu.Unlock()
- return s.versionLocked()
- }
- func (s *Session) versionLocked() uint64 {
- if s.schemaTx != nil {
- return s.schemaTx.version
- }
- return s.schema.Version()
- }
- // GetNextRowID allocates from the shared per-table ROWID counter. The counter is
- // derived from durable rows and is not part of the catalog, so a transaction
- // uses the same allocator as autocommit.
- func (s *Session) GetNextRowID(table string) (int64, error) {
- return s.schema.GetNextRowID(table)
- }
- // UpdateMaxRowID raises the shared per-table ROWID counter.
- func (s *Session) UpdateMaxRowID(table string, rowid int64) error {
- return s.schema.UpdateMaxRowID(table, rowid)
- }
- // CreateTable stages CREATE TABLE inside a transaction and publishes it
- // immediately otherwise.
- func (s *Session) CreateTable(schema *Schema) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx == nil {
- return s.schema.CreateTable(schema)
- }
- return s.stageDDL(func(tx *SchemaTx) error { return tx.createTable(schema) })
- }
- // DropTable stages DROP TABLE inside a transaction and publishes it otherwise.
- func (s *Session) DropTable(name string) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx == nil {
- return s.schema.DropTable(name)
- }
- return s.stageDDL(func(tx *SchemaTx) error { return tx.dropTable(name) })
- }
- // CreateIndex stages CREATE INDEX inside a transaction and publishes it
- // otherwise. A UNIQUE index is validated against the rows the session observes,
- // so a transaction that staged duplicate rows fails the statement, not the
- // commit.
- func (s *Session) CreateIndex(index *Index) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx == nil {
- if index.Unique {
- return s.table.CreateUniqueIndex(index)
- }
- return s.schema.CreateIndex(index)
- }
- return s.stageDDL(func(tx *SchemaTx) error {
- if err := tx.createIndex(index); err != nil {
- return err
- }
- if !index.Unique {
- return nil
- }
- return s.validateUniqueIndexLocked(index)
- })
- }
- // validateUniqueIndexLocked rejects a staged UNIQUE index whose indexed values
- // already collide in the rows this session observes (caller holds the session
- // lock).
- func (s *Session) validateUniqueIndexLocked(index *Index) error {
- rows, err := s.selectLocked(index.Table, nil)
- if err != nil {
- return err
- }
- seen := make(map[string]struct{}, len(rows))
- for _, row := range rows {
- encoded, isNull, err := s.table.encodeUniqueValue(row, index.Columns)
- if err != nil {
- return err
- }
- if isNull {
- continue
- }
- if _, duplicate := seen[encoded]; duplicate {
- return &UniqueViolationError{Constraint: index.Name}
- }
- seen[encoded] = struct{}{}
- }
- return nil
- }
- // DropIndex stages DROP INDEX inside a transaction and publishes it otherwise.
- func (s *Session) DropIndex(name string) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx == nil {
- return s.schema.DropIndex(name)
- }
- return s.stageDDL(func(tx *SchemaTx) error { return tx.dropIndex(name) })
- }
- // AddColumn stages ALTER TABLE ADD COLUMN inside a transaction and publishes it
- // otherwise.
- func (s *Session) AddColumn(table string, column Column, checks []CheckConstraint, unique *Index) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx == nil {
- return s.schema.AddColumn(table, column, checks, unique)
- }
- return s.stageDDL(func(tx *SchemaTx) error { return tx.addColumn(table, column, checks, unique) })
- }
- // DropColumn stages ALTER TABLE DROP COLUMN inside a transaction and publishes
- // it otherwise.
- func (s *Session) DropColumn(table, columnName string) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx == nil {
- return s.schema.DropColumn(table, columnName)
- }
- return s.stageDDL(func(tx *SchemaTx) error { return tx.dropColumn(table, columnName) })
- }
- // RenameTable stages ALTER TABLE RENAME TABLE inside a transaction and
- // publishes it otherwise.
- func (s *Session) RenameTable(oldName, newName string) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx == nil {
- return s.schema.RenameTable(oldName, newName)
- }
- return s.stageDDL(func(tx *SchemaTx) error { return tx.renameTable(oldName, newName) })
- }
- // RenameColumn stages ALTER TABLE RENAME COLUMN inside a transaction and
- // publishes it otherwise.
- func (s *Session) RenameColumn(table, oldName, newName string) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx == nil {
- return s.schema.RenameColumn(table, oldName, newName)
- }
- return s.stageDDL(func(tx *SchemaTx) error { return tx.renameColumn(table, oldName, newName) })
- }
- // stageDDL runs one DDL statement against the transaction overlay and restores
- // the catalog, overlay, and read set if it fails, so a rejected statement
- // leaves the transaction exactly as it was (caller holds the session lock).
- func (s *Session) stageDDL(apply func(tx *SchemaTx) error) error {
- savepoint := s.savepointLocked()
- if err := apply(s.schemaTx); err != nil {
- s.rollbackToSavepointLocked(savepoint)
- return err
- }
- return nil
- }
- // overlayHolds reports whether the session staged the given durable key of a
- // table.
- func (s *Session) overlayHolds(lowerTable, key string) bool {
- _, ok := s.overlay[lowerTable][key]
- return ok
- }
- // markTableScanned pins the table's current generation as a read so the commit
- // fails with a serialization error if another session writes the table between
- // the DDL statement and the commit.
- func (s *Session) markTableScanned(table string) {
- lower := strings.ToLower(table)
- if _, ok := s.scanGens[lower]; !ok {
- s.scanGens[lower] = s.table.generation(lower)
- }
- }
- // dropOverlayTableLocked discards the staged rows of a table the transaction
- // dropped, including their mutation-log entries (caller holds the session lock).
- func (s *Session) dropOverlayTableLocked(lower string) {
- delete(s.overlay, lower)
- log := s.log[:0]
- for _, mutation := range s.log {
- if mutation.table != lower {
- log = append(log, mutation)
- }
- }
- s.log = log
- delete(s.scanGens, lower)
- for key, predicate := range s.predicateGens {
- if predicate.table == lower {
- delete(s.predicateGens, key)
- }
- }
- prefix := s.table.dataPrefix(lower)
- for key := range s.reads {
- if strings.HasPrefix(key, prefix) {
- delete(s.reads, key)
- }
- }
- }
- // rekeyOverlayLocked moves the staged rows of a renamed table to the new data
- // prefix so the transaction reads its own rows back under the new name
- // (caller holds the session lock).
- func (s *Session) rekeyOverlayLocked(oldTable, newTable string) {
- oldLower := strings.ToLower(oldTable)
- newLower := strings.ToLower(newTable)
- if oldLower == newLower {
- return
- }
- oldPrefix := s.table.dataPrefix(oldLower)
- newPrefix := s.table.dataPrefix(newLower)
- if entries, ok := s.overlay[oldLower]; ok {
- delete(s.overlay, oldLower)
- moved := make(map[string]*overlayEntry, len(entries))
- for key, entry := range entries {
- moved[newPrefix+strings.TrimPrefix(key, oldPrefix)] = entry
- }
- s.overlay[newLower] = moved
- }
- for i := range s.log {
- if s.log[i].table != oldLower {
- continue
- }
- s.log[i].table = newLower
- s.log[i].key = newPrefix + strings.TrimPrefix(s.log[i].key, oldPrefix)
- }
- if gen, ok := s.scanGens[oldLower]; ok {
- delete(s.scanGens, oldLower)
- s.scanGens[newLower] = gen
- }
- reads := make(map[string]uint64, len(s.reads))
- for key, lsn := range s.reads {
- if strings.HasPrefix(key, oldPrefix) {
- reads[newPrefix+strings.TrimPrefix(key, oldPrefix)] = lsn
- continue
- }
- reads[key] = lsn
- }
- s.reads = reads
- predicates := make(map[string]predicateRead, len(s.predicateGens))
- for key, predicate := range s.predicateGens {
- if predicate.table == oldLower {
- predicate.table = newLower
- }
- if strings.HasPrefix(key, oldPrefix) {
- key = newPrefix + strings.TrimPrefix(key, oldPrefix)
- }
- predicates[key] = predicate
- }
- s.predicateGens = predicates
- }
- // ForeignKeyParticipants reports whether a write to table can affect referential
- // integrity as this session observes the catalog.
- func (s *Session) ForeignKeyParticipants(table string) (bool, error) {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx == nil {
- return s.schema.ForeignKeyParticipants(table)
- }
- catalog, err := s.foreignKeyCatalogLocked()
- if err != nil {
- return false, err
- }
- return catalog.participants[strings.ToLower(table)], nil
- }
- // ForeignKeyColumnDependency returns the constraints that keep a column in
- // place, as this session observes the catalog.
- func (s *Session) ForeignKeyColumnDependency(table, column string) ([]string, error) {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx == nil {
- return s.schema.ForeignKeyColumnDependency(table, column)
- }
- catalog, err := s.foreignKeyCatalogLocked()
- if err != nil {
- return nil, err
- }
- schema, err := s.getSchemaLocked(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, as this session observes the catalog.
- func (s *Session) ForeignKeyTableDependency(table string) ([]string, error) {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx == nil {
- return s.schema.ForeignKeyTableDependency(table)
- }
- catalog, err := s.foreignKeyCatalogLocked()
- 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
- }
- // ForeignKeyClosure returns the constrained component of the given tables as
- // this session observes the catalog.
- func (s *Session) ForeignKeyClosure(tables []string) (map[string]bool, error) {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.schemaTx == nil {
- return s.schema.ForeignKeyClosure(tables)
- }
- catalog, err := s.foreignKeyCatalogLocked()
- if err != nil {
- return nil, err
- }
- return catalog.closure(tables), nil
- }
- // foreignKeyCatalogLocked builds the foreign-key metadata of the session's
- // catalog view (caller holds the session lock).
- func (s *Session) foreignKeyCatalogLocked() (*foreignKeyCatalog, error) {
- version := s.versionLocked()
- if s.fkCatalog != nil && s.fkVersion == version {
- return s.fkCatalog, nil
- }
- tables, err := s.schemaTx.listTables()
- if err != nil {
- return nil, err
- }
- catalog, err := buildForeignKeyCatalog(version, tables, func(name string) (*Schema, error) {
- return s.getSchemaLocked(name)
- })
- if err != nil {
- return nil, err
- }
- s.fkCatalog = catalog
- s.fkVersion = version
- return catalog, nil
- }
|