github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/dsess/transactions.go (about) 1 // Copyright 2021 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package dsess 16 17 import ( 18 "context" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "strings" 23 "sync" 24 "time" 25 26 "github.com/dolthub/go-mysql-server/sql" 27 "github.com/dolthub/vitess/go/mysql" 28 "github.com/sirupsen/logrus" 29 30 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" 31 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb/durable" 32 "github.com/dolthub/dolt/go/libraries/doltcore/merge" 33 "github.com/dolthub/dolt/go/libraries/doltcore/table/editor" 34 "github.com/dolthub/dolt/go/store/datas" 35 "github.com/dolthub/dolt/go/store/hash" 36 "github.com/dolthub/dolt/go/store/prolly" 37 ) 38 39 const ( 40 maxTxCommitRetries = 5 41 ) 42 43 var ErrRetryTransaction = errors.New("this transaction conflicts with a committed transaction from another client") 44 45 var ErrUnresolvedConflictsCommit = errors.New("Merge conflict detected, transaction rolled back. Merge conflicts must be resolved using the dolt_conflicts and dolt_schema_conflicts tables before committing a transaction. To commit transactions with merge conflicts, set @@dolt_allow_commit_conflicts = 1") 46 47 var ErrUnresolvedConflictsAutoCommit = errors.New("Merge conflict detected, @autocommit transaction rolled back. @autocommit must be disabled so that merge conflicts can be resolved using the dolt_conflicts and dolt_schema_conflicts tables before manually committing the transaction. Alternatively, to commit transactions with merge conflicts, set @@dolt_allow_commit_conflicts = 1") 48 49 var ErrUnresolvedConstraintViolationsCommit = errors.New("Committing this transaction resulted in a working set with constraint violations, transaction rolled back. " + 50 "This constraint violation may be the result of a previous merge or the result of transaction sequencing. " + 51 "Constraint violations from a merge can be resolved using the dolt_constraint_violations table before committing the transaction. " + 52 "To allow transactions to be committed with constraint violations from a merge or transaction sequencing set @@dolt_force_transaction_commit=1.") 53 54 // TODO: remove this 55 func TransactionsDisabled(ctx *sql.Context) bool { 56 enabled, err := ctx.GetSessionVariable(ctx, TransactionsDisabledSysVar) 57 if err != nil { 58 panic(err) 59 } 60 61 switch enabled.(int8) { 62 case 0: 63 return false 64 case 1: 65 return true 66 default: 67 panic(fmt.Sprintf("Unexpected value %v", enabled)) 68 } 69 } 70 71 // DisabledTransaction is a no-op transaction type that lets us feature-gate transaction logic changes 72 type DisabledTransaction struct{} 73 74 func (d DisabledTransaction) String() string { 75 return "Disabled transaction" 76 } 77 78 func (d DisabledTransaction) IsReadOnly() bool { 79 return false 80 } 81 82 type DoltTransaction struct { 83 dbStartPoints map[string]dbRoot 84 savepoints []savepoint 85 tCharacteristic sql.TransactionCharacteristic 86 } 87 88 type dbRoot struct { 89 dbName string 90 rootHash hash.Hash 91 db *doltdb.DoltDB 92 } 93 94 type savepoint struct { 95 name string 96 // TODO: we need a root value per DB here 97 roots map[string]doltdb.RootValue 98 } 99 100 func NewDoltTransaction( 101 ctx *sql.Context, 102 dbs []SqlDatabase, 103 tCharacteristic sql.TransactionCharacteristic, 104 ) (*DoltTransaction, error) { 105 106 startPoints := make(map[string]dbRoot) 107 for _, db := range dbs { 108 nomsRoot, err := db.DbData().Ddb.NomsRoot(ctx) 109 if err != nil { 110 return nil, err 111 } 112 113 baseName, _ := SplitRevisionDbName(db.Name()) 114 startPoints[strings.ToLower(baseName)] = dbRoot{ 115 dbName: baseName, 116 rootHash: nomsRoot, 117 db: db.DbData().Ddb, 118 } 119 } 120 121 return &DoltTransaction{ 122 dbStartPoints: startPoints, 123 tCharacteristic: tCharacteristic, 124 }, nil 125 } 126 127 // AddDb adds the database named to the transaction. Only necessary in the case when new databases are added to an 128 // existing transaction (as when cloning a database on a read replica when it is first referenced). 129 func (tx DoltTransaction) AddDb(ctx *sql.Context, db SqlDatabase) error { 130 nomsRoot, err := db.DbData().Ddb.NomsRoot(ctx) 131 if err != nil { 132 return err 133 } 134 135 tx.dbStartPoints[strings.ToLower(db.Name())] = dbRoot{ 136 dbName: db.Name(), 137 rootHash: nomsRoot, 138 db: db.DbData().Ddb, 139 } 140 141 return nil 142 } 143 144 func (tx DoltTransaction) String() string { 145 // TODO: return more info (hashes need caching) 146 return "DoltTransaction" 147 } 148 149 func (tx DoltTransaction) IsReadOnly() bool { 150 return tx.tCharacteristic == sql.ReadOnly 151 } 152 153 // GetInitialRoot returns the noms root hash for the db named, established when the transaction began. The dbName here 154 // is always the base name of the database, not the revision qualified one. 155 func (tx DoltTransaction) GetInitialRoot(dbName string) (hash.Hash, bool) { 156 dbName, _ = SplitRevisionDbName(dbName) 157 startPoint, ok := tx.dbStartPoints[strings.ToLower(dbName)] 158 return startPoint.rootHash, ok 159 } 160 161 var txLock sync.Mutex 162 163 // Commit attempts to merge the working set given into the current working set. 164 // Uses the same algorithm as merge.RootMerger: 165 // |current working set working root| is the root 166 // |workingSet.workingRoot| is the mergeRoot 167 // |tx.startRoot| is ancRoot 168 // if workingSet.workingRoot == ancRoot, attempt a fast-forward merge 169 // TODO: Non-working roots aren't merged into the working set and just stomp any changes made there. We need merge 170 // strategies for staged as well as merge state. 171 func (tx *DoltTransaction) Commit(ctx *sql.Context, workingSet *doltdb.WorkingSet, dbName string) (*doltdb.WorkingSet, error) { 172 ws, _, err := tx.doCommit(ctx, workingSet, nil, txCommit, dbName) 173 return ws, err 174 } 175 176 // transactionWrite is the logic to write an updated working set (and optionally a commit) to the database 177 type transactionWrite func(ctx *sql.Context, 178 tx *DoltTransaction, // the transaction being written 179 doltDb *doltdb.DoltDB, // the database to write to 180 startState *doltdb.WorkingSet, // the starting working set 181 commit *doltdb.PendingCommit, // optional 182 workingSet *doltdb.WorkingSet, // must be provided 183 hash hash.Hash, // hash of the current working set to be written 184 mergeOps editor.Options, // editor options for merges 185 ) (*doltdb.WorkingSet, *doltdb.Commit, error) 186 187 // doltCommit is a transactionWrite function that updates the working set and commits a pending commit atomically 188 func doltCommit(ctx *sql.Context, 189 tx *DoltTransaction, // the transaction being written 190 doltDb *doltdb.DoltDB, // the database to write to 191 startState *doltdb.WorkingSet, // the starting working set 192 commit *doltdb.PendingCommit, // optional 193 workingSet *doltdb.WorkingSet, // must be provided 194 currHash hash.Hash, // hash of the current working set to be written 195 mergeOpts editor.Options, // editor options for merges 196 ) (*doltdb.WorkingSet, *doltdb.Commit, error) { 197 pending := *commit 198 199 headRef, err := workingSet.Ref().ToHeadRef() 200 if err != nil { 201 return nil, nil, err 202 } 203 204 headSpec, _ := doltdb.NewCommitSpec("HEAD") 205 optCmt, err := doltDb.Resolve(ctx, headSpec, headRef) 206 if err != nil { 207 return nil, nil, err 208 } 209 curHead, ok := optCmt.ToCommit() 210 if !ok { 211 return nil, nil, doltdb.ErrGhostCommitRuntimeFailure 212 } 213 214 // We already got a new staged root via merge or ff via the doCommit method, so now apply it to the STAGED value 215 // we're about to commit. 216 pending.Roots.Staged = workingSet.StagedRoot() 217 218 // We check if the branch HEAD has changed since our transaction started and perform an additional merge if so. The 219 // non-dolt-commit transaction logic only merges working sets and doesn't consider the HEAD value. 220 if curHead != nil { 221 curRootVal, err := curHead.ResolveRootValue(ctx) 222 if err != nil { 223 return nil, nil, err 224 } 225 curRootValHash, err := curRootVal.HashOf() 226 if err != nil { 227 return nil, nil, err 228 } 229 headRootValHash, err := pending.Roots.Head.HashOf() 230 if err != nil { 231 return nil, nil, err 232 } 233 234 if curRootValHash != headRootValHash { 235 // If the branch head changed since our transaction started, then we merge 236 // the existing branch head (curRootVal) into our staged root value. We 237 // treat the HEAD of the branch when our transaction started as the common 238 // ancestor (TODO: This will not be true in the case of destructive branch 239 // updates). The merged root value becomes our new Staged root value which 240 // is the value which we are trying to commit. 241 start := time.Now() 242 243 result, err := merge.MergeRoots( 244 ctx, 245 pending.Roots.Staged, 246 curRootVal, 247 pending.Roots.Head, 248 curHead, 249 startState, 250 mergeOpts, 251 merge.MergeOpts{}) 252 if err != nil { 253 return nil, nil, err 254 } 255 pending.Roots.Staged = result.Root 256 257 // We also need to update the working set to reflect the new staged root value 258 workingSet = workingSet.WithStagedRoot(pending.Roots.Staged) 259 260 logrus.Tracef("staged and HEAD merge took %s", time.Since(start)) 261 } 262 } 263 264 workingSet = workingSet.ClearMerge() 265 266 var rsc doltdb.ReplicationStatusController 267 newCommit, err := doltDb.CommitWithWorkingSet(ctx, headRef, workingSet.Ref(), &pending, workingSet, currHash, tx.WorkingSetMeta(ctx), &rsc) 268 WaitForReplicationController(ctx, rsc) 269 return workingSet, newCommit, err 270 } 271 272 // txCommit is a transactionWrite function that updates the working set 273 func txCommit(ctx *sql.Context, 274 tx *DoltTransaction, // the transaction being written 275 doltDb *doltdb.DoltDB, // the database to write to 276 _ *doltdb.WorkingSet, // the starting working set 277 _ *doltdb.PendingCommit, // optional 278 workingSet *doltdb.WorkingSet, // must be provided 279 hash hash.Hash, // hash of the current working set to be written 280 _ editor.Options, // editor options for merges 281 ) (*doltdb.WorkingSet, *doltdb.Commit, error) { 282 var rsc doltdb.ReplicationStatusController 283 err := doltDb.UpdateWorkingSet(ctx, workingSet.Ref(), workingSet, hash, tx.WorkingSetMeta(ctx), &rsc) 284 WaitForReplicationController(ctx, rsc) 285 return workingSet, nil, err 286 } 287 288 // DoltCommit commits the working set and creates a new DoltCommit as specified, in one atomic write 289 func (tx *DoltTransaction) DoltCommit( 290 ctx *sql.Context, 291 workingSet *doltdb.WorkingSet, 292 commit *doltdb.PendingCommit, 293 dbName string, 294 ) (*doltdb.WorkingSet, *doltdb.Commit, error) { 295 return tx.doCommit(ctx, workingSet, commit, doltCommit, dbName) 296 } 297 298 func WaitForReplicationController(ctx *sql.Context, rsc doltdb.ReplicationStatusController) { 299 if len(rsc.Wait) == 0 { 300 return 301 } 302 _, timeout, ok := sql.SystemVariables.GetGlobal(DoltClusterAckWritesTimeoutSecs) 303 if !ok { 304 return 305 } 306 timeoutI := timeout.(int64) 307 if timeoutI == 0 { 308 return 309 } 310 311 cCtx, cancel := context.WithCancelCause(ctx) 312 var wg sync.WaitGroup 313 wg.Add(len(rsc.Wait)) 314 for i, f := range rsc.Wait { 315 f := f 316 i := i 317 go func() { 318 defer wg.Done() 319 err := f(cCtx) 320 if err == nil { 321 rsc.Wait[i] = nil 322 } 323 }() 324 } 325 326 done := make(chan struct{}) 327 go func() { 328 wg.Wait() 329 close(done) 330 }() 331 332 waitFailed := false 333 select { 334 case <-time.After(time.Duration(timeoutI) * time.Second): 335 // We timed out before all the waiters were done. 336 // First we make certain to finalize everything. 337 cancel(doltdb.ErrReplicationWaitFailed) 338 <-done 339 waitFailed = true 340 case <-done: 341 cancel(context.Canceled) 342 } 343 344 // Just because our waiters all completed does not mean they all 345 // returned nil errors. Any non-nil entries in rsc.Wait returned an 346 // error. We turn those into warnings here. 347 numFailed := 0 348 for i, f := range rsc.Wait { 349 if f != nil { 350 numFailed += 1 351 if waitFailed { 352 rsc.NotifyWaitFailed[i]() 353 } 354 } 355 } 356 if numFailed > 0 { 357 ctx.Session.Warn(&sql.Warning{ 358 Level: "Warning", 359 Code: mysql.ERQueryTimeout, 360 Message: fmt.Sprintf("Timed out replication of commit to %d out of %d replicas.", numFailed, len(rsc.Wait)), 361 }) 362 } 363 } 364 365 // doCommit commits this transaction with the write function provided. It takes the same params as DoltCommit 366 func (tx *DoltTransaction) doCommit( 367 ctx *sql.Context, 368 workingSet *doltdb.WorkingSet, 369 commit *doltdb.PendingCommit, 370 writeFn transactionWrite, 371 dbName string, 372 ) (*doltdb.WorkingSet, *doltdb.Commit, error) { 373 sess := DSessFromSess(ctx.Session) 374 branchState, ok, err := sess.lookupDbState(ctx, dbName) 375 if err != nil { 376 return nil, nil, err 377 } 378 if !ok { 379 return nil, nil, fmt.Errorf("database %s unknown to transaction, this is a bug", dbName) 380 } 381 382 // Load the start state for this working set from the noms root at tx start 383 // Get the base DB name from the db state, not the branch state 384 startPoint, ok := tx.dbStartPoints[strings.ToLower(branchState.dbState.dbName)] 385 if !ok { 386 return nil, nil, fmt.Errorf("database %s unknown to transaction, this is a bug", dbName) 387 } 388 389 startState, err := startPoint.db.ResolveWorkingSetAtRoot(ctx, workingSet.Ref(), startPoint.rootHash) 390 if err != nil { 391 return nil, nil, err 392 } 393 394 // TODO: no-op if the working set hasn't changed since the transaction started 395 396 mergeOpts := branchState.EditOpts() 397 398 for i := 0; i < maxTxCommitRetries; i++ { 399 updatedWs, newCommit, err := func() (*doltdb.WorkingSet, *doltdb.Commit, error) { 400 // Serialize commits, since only one can possibly succeed at a time anyway 401 txLock.Lock() 402 defer txLock.Unlock() 403 404 newWorkingSet := false 405 406 existingWs, err := startPoint.db.ResolveWorkingSet(ctx, workingSet.Ref()) 407 if err == doltdb.ErrWorkingSetNotFound { 408 // This is to handle the case where an existing DB pre working sets is committing to this HEAD for the 409 // first time. Can be removed and called an error post 1.0 410 existingWs = doltdb.EmptyWorkingSet(workingSet.Ref()) 411 newWorkingSet = true 412 } else if err != nil { 413 return nil, nil, err 414 } 415 416 existingWSHash, err := existingWs.HashOf() 417 if err != nil { 418 return nil, nil, err 419 } 420 421 if newWorkingSet || workingAndStagedEqual(existingWs, startState) { 422 // ff merge 423 err = tx.validateWorkingSetForCommit(ctx, workingSet, isFfMerge) 424 if err != nil { 425 return nil, nil, err 426 } 427 428 var newCommit *doltdb.Commit 429 workingSet, newCommit, err = writeFn(ctx, tx, startPoint.db, startState, commit, workingSet, existingWSHash, mergeOpts) 430 if err == datas.ErrOptimisticLockFailed { 431 // this is effectively a `continue` in the loop 432 return nil, nil, nil 433 } else if err != nil { 434 return nil, nil, err 435 } 436 437 return workingSet, newCommit, nil 438 } 439 440 // otherwise (not a ff), merge the working sets together 441 start := time.Now() 442 mergedWorkingSet, err := tx.mergeRoots(ctx, startState, existingWs, workingSet, mergeOpts) 443 if err != nil { 444 return nil, nil, err 445 } 446 logrus.Tracef("working set merge took %s", time.Since(start)) 447 448 err = tx.validateWorkingSetForCommit(ctx, mergedWorkingSet, notFfMerge) 449 if err != nil { 450 return nil, nil, err 451 } 452 453 var newCommit *doltdb.Commit 454 mergedWorkingSet, newCommit, err = writeFn(ctx, tx, startPoint.db, startState, commit, mergedWorkingSet, existingWSHash, mergeOpts) 455 if err == datas.ErrOptimisticLockFailed { 456 // this is effectively a `continue` in the loop 457 return nil, nil, nil 458 } else if err != nil { 459 return nil, nil, err 460 } 461 462 return mergedWorkingSet, newCommit, nil 463 }() 464 465 if err != nil { 466 return nil, nil, err 467 } else if updatedWs != nil { 468 return updatedWs, newCommit, nil 469 } 470 } 471 472 // TODO: different error type for retries exhausted 473 return nil, nil, datas.ErrOptimisticLockFailed 474 } 475 476 // mergeRoots merges the roots in the existing working set with the one being committed and returns the resulting 477 // working set. Conflicts are automatically resolved with "accept ours" if the session settings dictate it. 478 // Currently merges working and staged roots as necessary. HEAD root is only handled by the DoltCommit function. 479 func (tx *DoltTransaction) mergeRoots( 480 ctx *sql.Context, 481 startState *doltdb.WorkingSet, 482 existingWorkingSet *doltdb.WorkingSet, 483 workingSet *doltdb.WorkingSet, 484 mergeOpts editor.Options, 485 ) (*doltdb.WorkingSet, error) { 486 487 if !rootsEqual(existingWorkingSet.WorkingRoot(), workingSet.WorkingRoot()) { 488 result, err := merge.MergeRoots( 489 ctx, 490 existingWorkingSet.WorkingRoot(), 491 workingSet.WorkingRoot(), 492 startState.WorkingRoot(), 493 workingSet, 494 startState, 495 mergeOpts, 496 merge.MergeOpts{}) 497 if err != nil { 498 return nil, err 499 } 500 workingSet = workingSet.WithWorkingRoot(result.Root) 501 } 502 503 if !rootsEqual(existingWorkingSet.StagedRoot(), workingSet.StagedRoot()) { 504 result, err := merge.MergeRoots( 505 ctx, 506 existingWorkingSet.StagedRoot(), 507 workingSet.StagedRoot(), 508 startState.StagedRoot(), 509 workingSet, 510 startState, 511 mergeOpts, 512 merge.MergeOpts{}) 513 if err != nil { 514 return nil, err 515 } 516 workingSet = workingSet.WithStagedRoot(result.Root) 517 } 518 519 return workingSet, nil 520 } 521 522 // rollback attempts a transaction rollback 523 func (tx *DoltTransaction) rollback(ctx *sql.Context) error { 524 sess := DSessFromSess(ctx.Session) 525 rollbackErr := sess.Rollback(ctx, tx) 526 if rollbackErr != nil { 527 return rollbackErr 528 } 529 530 // We also need to cancel out the transaction here so that a new one will begin on the next statement 531 // TODO: it would be better for the engine to handle these details probably, this code is duplicated from the 532 // rollback statement implementation in the engine. 533 ctx.SetTransaction(nil) 534 ctx.SetIgnoreAutoCommit(false) 535 536 return nil 537 } 538 539 type ffMerge bool 540 541 const ( 542 isFfMerge = ffMerge(true) 543 notFfMerge = ffMerge(false) 544 ) 545 546 // validateWorkingSetForCommit validates that the working set given is legal to 547 // commit according to the session settings. Returns an error if the given 548 // working set has conflicts or constraint violations and the session settings 549 // do not allow them. 550 // 551 // If dolt_allow_commit_conflicts = 0 and dolt_force_transaction_commit = 0, and 552 // a transaction's post-commit working set contains a documented conflict 553 // ( either as a result of a merge that occurred inside the transaction, or a 554 // result of a transaction merge) that transaction will be rolled back. 555 // 556 // The justification for this behavior is that we want to protect the working 557 // set from conflicts with the above settings. 558 // 559 // If dolt_force_transaction_commit = 0, and a transaction's post-commit working 560 // set contains a documented constraint violation ( either as a result of a merge 561 // that occurred inside the transaction, or a result of a transaction merge) 562 // that transaction will be rolled back. 563 // 564 // The justification for this behavior is that we want to protect the working 565 // set from constraint violations with the above settings. 566 // TODO: should this validate staged as well? 567 func (tx *DoltTransaction) validateWorkingSetForCommit(ctx *sql.Context, workingSet *doltdb.WorkingSet, isFf ffMerge) error { 568 forceTransactionCommit, err := ctx.GetSessionVariable(ctx, ForceTransactionCommit) 569 if err != nil { 570 return err 571 } 572 573 allowCommitConflicts, err := ctx.GetSessionVariable(ctx, AllowCommitConflicts) 574 if err != nil { 575 return err 576 } 577 578 hasSchemaConflicts := false 579 if workingSet.MergeState() != nil { 580 hasSchemaConflicts = workingSet.MergeState().HasSchemaConflicts() 581 } 582 583 workingRoot := workingSet.WorkingRoot() 584 hasDataConflicts, err := doltdb.HasConflicts(ctx, workingRoot) 585 if err != nil { 586 return err 587 } 588 hasConstraintViolations, err := doltdb.HasConstraintViolations(ctx, workingRoot) 589 if err != nil { 590 return err 591 } 592 593 if hasDataConflicts || hasSchemaConflicts { 594 // TODO: Sometimes this returns the wrong error. Define an internal 595 // merge to be a merge that occurs inside a transaction. Define a 596 // transaction merge to be the merge that resolves changes between two 597 // transactions. If an internal merge creates a documented conflict and 598 // the transaction merge is not a fast-forward, a retry transaction 599 // error will be returned. Instead, an ErrUnresolvedConflictsCommit should 600 // be returned. 601 602 // Conflicts are never acceptable when they resulted from a merge with the existing working set -- it's equivalent 603 // to hitting a write lock (which we didn't take). Always roll back and return an error in this case. 604 if !isFf { 605 rollbackErr := tx.rollback(ctx) 606 if rollbackErr != nil { 607 return rollbackErr 608 } 609 610 return sql.ErrLockDeadlock.New(ErrRetryTransaction.Error()) 611 } 612 613 // If there were conflicts before merge with the persisted working set, whether we allow it to be committed is a 614 // session setting 615 if !(allowCommitConflicts.(int8) == 1 || forceTransactionCommit.(int8) == 1) { 616 rollbackErr := tx.rollback(ctx) 617 if rollbackErr != nil { 618 return rollbackErr 619 } 620 621 // Return a different error message depending on if @autocommit is enabled or not, to help 622 // users understand what steps to take 623 autocommit, err := isSessionAutocommit(ctx) 624 if err != nil { 625 return err 626 } 627 if autocommit { 628 return ErrUnresolvedConflictsAutoCommit 629 } else { 630 return ErrUnresolvedConflictsCommit 631 } 632 } 633 } 634 635 if hasConstraintViolations { 636 // Constraint violations are acceptable in the working set if force 637 // transaction commit is enabled, regardless if an internal merge ( a 638 // merge that occurs inside a transaction) or a transaction merge 639 // created them. 640 641 // TODO: We need to add more granularity in terms of what types of constraint violations can be committed. For example, 642 // in the case of foreign_key_checks=0 you should be able to commit foreign key violations. 643 if forceTransactionCommit.(int8) != 1 { 644 badTbls, err := doltdb.TablesWithConstraintViolations(ctx, workingRoot) 645 if err != nil { 646 return err 647 } 648 649 violations := make([]string, len(badTbls)) 650 for i, name := range badTbls { 651 tbl, _, err := workingRoot.GetTable(ctx, doltdb.TableName{Name: name}) 652 if err != nil { 653 return err 654 } 655 656 artIdx, err := tbl.GetArtifacts(ctx) 657 if err != nil { 658 return err 659 } 660 661 m := durable.ProllyMapFromArtifactIndex(artIdx) 662 itr, err := m.IterAllCVs(ctx) 663 664 for { 665 art, err := itr.Next(ctx) 666 if err != nil { 667 break 668 } 669 670 var meta prolly.ConstraintViolationMeta 671 err = json.Unmarshal(art.Metadata, &meta) 672 if err != nil { 673 return err 674 } 675 676 s := "" 677 switch art.ArtType { 678 case prolly.ArtifactTypeForeignKeyViol: 679 var m merge.FkCVMeta 680 err = json.Unmarshal(meta.VInfo, &m) 681 if err != nil { 682 return err 683 } 684 s = fmt.Sprintf("\n"+ 685 "Type: Foreign Key Constraint Violation\n"+ 686 "\tForeignKey: %s,\n"+ 687 "\tTable: %s,\n"+ 688 "\tReferencedTable: %s,\n"+ 689 "\tIndex: %s,\n"+ 690 "\tReferencedIndex: %s", m.ForeignKey, m.Table, m.ReferencedIndex, m.Index, m.ReferencedIndex) 691 692 case prolly.ArtifactTypeUniqueKeyViol: 693 var m merge.UniqCVMeta 694 err = json.Unmarshal(meta.VInfo, &m) 695 if err != nil { 696 return err 697 } 698 s = fmt.Sprintf("\n"+ 699 "Type: Unique Key Constraint Violation,\n"+ 700 "\tName: %s,\n"+ 701 "\tColumns: %v", m.Name, m.Columns) 702 703 case prolly.ArtifactTypeNullViol: 704 var m merge.NullViolationMeta 705 err = json.Unmarshal(meta.VInfo, &m) 706 if err != nil { 707 return err 708 } 709 s = fmt.Sprintf("\n"+ 710 "Type: Null Constraint Violation,\n"+ 711 "\tColumns: %v", m.Columns) 712 713 case prolly.ArtifactTypeChkConsViol: 714 var m merge.CheckCVMeta 715 err = json.Unmarshal(meta.VInfo, &m) 716 if err != nil { 717 return err 718 } 719 s = fmt.Sprintf("\n"+ 720 "Type: Check Constraint Violation,\n"+ 721 "\tName: %s,\n"+ 722 "\tExpression: %v", m.Name, m.Expression) 723 } 724 if err != nil { 725 return err 726 } 727 728 violations[i] = s 729 } 730 } 731 732 rollbackErr := tx.rollback(ctx) 733 if rollbackErr != nil { 734 return rollbackErr 735 } 736 737 return fmt.Errorf("%s\n"+ 738 "Constraint violations: %s", ErrUnresolvedConstraintViolationsCommit, strings.Join(violations, ", ")) 739 } 740 } 741 742 return nil 743 } 744 745 // CreateSavepoint creates a new savepoint with the name and roots given. If a savepoint with the name given 746 // already exists, it's overwritten. 747 func (tx *DoltTransaction) CreateSavepoint(name string, roots map[string]doltdb.RootValue) { 748 existing := tx.findSavepoint(name) 749 if existing >= 0 { 750 tx.savepoints = append(tx.savepoints[:existing], tx.savepoints[existing+1:]...) 751 } 752 tx.savepoints = append(tx.savepoints, savepoint{name, roots}) 753 } 754 755 // findSavepoint returns the index of the savepoint with the name given, or -1 if it doesn't exist 756 func (tx *DoltTransaction) findSavepoint(name string) int { 757 for i, s := range tx.savepoints { 758 if strings.ToLower(s.name) == strings.ToLower(name) { 759 return i 760 } 761 } 762 return -1 763 } 764 765 // RollbackToSavepoint returns the root values for all applicable databases associated with the savepoint name given, or nil if no such savepoint can 766 // be found. All savepoints created after the one being rolled back to are no longer accessible. 767 func (tx *DoltTransaction) RollbackToSavepoint(name string) map[string]doltdb.RootValue { 768 existing := tx.findSavepoint(name) 769 if existing >= 0 { 770 // Clear out any savepoints past this one 771 tx.savepoints = tx.savepoints[:existing+1] 772 return tx.savepoints[existing].roots 773 } 774 return nil 775 } 776 777 // ClearSavepoint removes the savepoint with the name given and returns whether a savepoint had that name 778 func (tx *DoltTransaction) ClearSavepoint(name string) bool { 779 existing := tx.findSavepoint(name) 780 if existing >= 0 { 781 tx.savepoints = append(tx.savepoints[:existing], tx.savepoints[existing+1:]...) 782 return true 783 } 784 return false 785 } 786 787 // WorkingSetMeta returns the metadata to use for a commit of this transaction 788 func (tx DoltTransaction) WorkingSetMeta(ctx *sql.Context) *datas.WorkingSetMeta { 789 sess := DSessFromSess(ctx.Session) 790 return &datas.WorkingSetMeta{ 791 Name: sess.Username(), 792 Email: sess.Email(), 793 Timestamp: uint64(time.Now().Unix()), 794 Description: "sql transaction", 795 } 796 } 797 798 func rootsEqual(left, right doltdb.RootValue) bool { 799 if left == nil || right == nil { 800 return false 801 } 802 803 lh, err := left.HashOf() 804 if err != nil { 805 return false 806 } 807 808 rh, err := right.HashOf() 809 if err != nil { 810 return false 811 } 812 813 return lh == rh 814 } 815 816 func workingAndStagedEqual(left, right *doltdb.WorkingSet) bool { 817 return rootsEqual(left.WorkingRoot(), right.WorkingRoot()) && rootsEqual(left.StagedRoot(), right.StagedRoot()) 818 } 819 820 // isSessionAutocommit returns true if @autocommit is enabled. 821 func isSessionAutocommit(ctx *sql.Context) (bool, error) { 822 autoCommitSessionVar, err := ctx.GetSessionVariable(ctx, sql.AutoCommitSessionVar) 823 if err != nil { 824 return false, err 825 } 826 return sql.ConvertToBool(ctx, autoCommitSessionVar) 827 }