// execStmtInAbortedTxn executes a statement in a txn that's in state // Aborted or RestartWait. All statements cause errors except: // - COMMIT / ROLLBACK: aborts the current transaction. // - ROLLBACK TO SAVEPOINT / SAVEPOINT: reopens the current transaction, // allowing it to be retried. func (e *Executor) execStmtInAbortedTxn( stmt parser.Statement, txnState *txnState, planMaker *planner, ) (Result, error) { if txnState.State != Aborted && txnState.State != RestartWait { panic("execStmtInAbortedTxn called outside of an aborted txn") } // TODO(andrei/cuongdo): Figure out what statements to count here. switch s := stmt.(type) { case *parser.CommitTransaction, *parser.RollbackTransaction: if txnState.State == RestartWait { return rollbackSQLTransaction(txnState, planMaker), nil } // Reset the state to allow new transactions to start. // The KV txn has already been rolled back when we entered the Aborted state. // Note: postgres replies to COMMIT of failed txn with "ROLLBACK" too. result := Result{PGTag: (*parser.RollbackTransaction)(nil).StatementTag()} txnState.resetStateAndTxn(NoTxn) return result, nil case *parser.RollbackToSavepoint, *parser.Savepoint: // We accept both the "ROLLBACK TO SAVEPOINT cockroach_restart" and the // "SAVEPOINT cockroach_restart" commands to indicate client intent to // retry a transaction in a RestartWait state. var spName string switch n := s.(type) { case *parser.RollbackToSavepoint: spName = n.Savepoint case *parser.Savepoint: spName = n.Name default: panic("unreachable") } if err := parser.ValidateRestartCheckpoint(spName); err != nil { return Result{Err: err}, err } if txnState.State == RestartWait { // Reset the state. Txn is Open again. txnState.State = Open txnState.retrying = true // TODO(andrei/cdo): add a counter for user-directed retries. return Result{}, nil } err := sqlbase.NewTransactionAbortedError(fmt.Sprintf( "SAVEPOINT %s has not been used or a non-retriable error was encountered.", parser.RestartSavepointName)) return Result{Err: err}, err default: err := sqlbase.NewTransactionAbortedError("") return Result{Err: err}, err } }
// execStmtInOpenTxn executes one statement in the context // of the planner's transaction (which is assumed to exist). // It handles statements that affect the transaction state (BEGIN, COMMIT) // and delegates everything else to `execStmt`. // It binds placeholders. // // The current transaction might be committed/rolled back when this returns. // It might also have transitioned to the aborted or RestartWait state. // // Args: // implicitTxn: set if the current transaction was implicitly // created by the system (i.e. the client sent the statement outside of // a transaction). // COMMIT/ROLLBACK statements are rejected if set. Also, the transaction // might be auto-committed in this function. // firstInTxn: set for the first statement in a transaction. Used // so that nested BEGIN statements are caught. // stmtTimestamp: Used as the statement_timestamp(). // // Returns: // - a Result // - an error, if any. In case of error, the result returned also reflects this error. func (e *Executor) execStmtInOpenTxn( stmt parser.Statement, planMaker *planner, implicitTxn bool, firstInTxn bool, txnState *txnState, ) (Result, error) { if txnState.State != Open { panic("execStmtInOpenTxn called outside of an open txn") } if planMaker.txn == nil { panic("execStmtInOpenTxn called with a txn not set on the planner") } planMaker.evalCtx.SetTxnTimestamp(txnState.sqlTimestamp) planMaker.evalCtx.SetStmtTimestamp(e.cfg.Clock.PhysicalTime()) session := planMaker.session log.Eventf(session.context, "%s", stmt) // TODO(cdo): Figure out how to not double count on retries. e.updateStmtCounts(stmt) switch s := stmt.(type) { case *parser.BeginTransaction: if !firstInTxn { txnState.updateStateAndCleanupOnErr(errTransactionInProgress, e) return Result{Err: errTransactionInProgress}, errTransactionInProgress } case *parser.CommitTransaction: if implicitTxn { return e.noTransactionHelper(txnState) } // CommitTransaction is executed fully here; there's no planNode for it // and the planner is not involved at all. res, err := commitSQLTransaction(txnState, planMaker, commit, e) return res, err case *parser.ReleaseSavepoint: if implicitTxn { return e.noTransactionHelper(txnState) } if err := parser.ValidateRestartCheckpoint(s.Savepoint); err != nil { return Result{Err: err}, err } // ReleaseSavepoint is executed fully here; there's no planNode for it // and the planner is not involved at all. res, err := commitSQLTransaction(txnState, planMaker, release, e) return res, err case *parser.RollbackTransaction: if implicitTxn { return e.noTransactionHelper(txnState) } // RollbackTransaction is executed fully here; there's no planNode for it // and the planner is not involved at all. // Notice that we don't return any errors on rollback. return rollbackSQLTransaction(txnState, planMaker), nil case *parser.SetTransaction: if implicitTxn { return e.noTransactionHelper(txnState) } case *parser.Savepoint: if implicitTxn { return e.noTransactionHelper(txnState) } if err := parser.ValidateRestartCheckpoint(s.Name); err != nil { return Result{Err: err}, err } // We want to disallow SAVEPOINTs to be issued after a transaction has // started running, but such enforcement is problematic in the // presence of transaction retries (since the transaction proto is // necessarily reused). To work around this, we keep track of the // transaction's retrying state and special-case SAVEPOINT when it is // set. // // TODO(andrei): the check for retrying is a hack - we erroneously // allow SAVEPOINT to be issued at any time during a retry, not just // in the beginning. We should figure out how to track whether we // started using the transaction during a retry. if txnState.txn.Proto.IsInitialized() && !txnState.retrying { err := fmt.Errorf("SAVEPOINT %s needs to be the first statement in a transaction", parser.RestartSavepointName) txnState.updateStateAndCleanupOnErr(err, e) return Result{Err: err}, err } // Note that Savepoint doesn't have a corresponding plan node. // This here is all the execution there is. txnState.retryIntent = true return Result{}, nil case *parser.RollbackToSavepoint: err := parser.ValidateRestartCheckpoint(s.Savepoint) if err == nil { // Can't restart if we didn't get an error first, which would've put the // txn in a different state. err = errNotRetriable } txnState.updateStateAndCleanupOnErr(err, e) return Result{Err: err}, err case *parser.Prepare: err := util.UnimplementedWithIssueErrorf(7568, "Prepared statements are supported only via the Postgres wire protocol") txnState.updateStateAndCleanupOnErr(err, e) return Result{Err: err}, err case *parser.Execute: err := util.UnimplementedWithIssueErrorf(7568, "Executing prepared statements is supported only via the Postgres wire protocol") txnState.updateStateAndCleanupOnErr(err, e) return Result{Err: err}, err case *parser.Deallocate: if s.Name == "" { planMaker.session.PreparedStatements.DeleteAll() } else { if found := planMaker.session.PreparedStatements.Delete(string(s.Name)); !found { err := fmt.Errorf("prepared statement %s does not exist", s.Name) txnState.updateStateAndCleanupOnErr(err, e) return Result{Err: err}, err } } return Result{PGTag: s.StatementTag()}, nil } autoCommit := implicitTxn && !e.cfg.TestingKnobs.DisableAutoCommit result, err := e.execStmt(stmt, planMaker, autoCommit) if err != nil { if result.Rows != nil { result.Rows.Close() result.Rows = nil } if traceSQL { log.ErrEventf(txnState.txn.Context, "ERROR: %v", err) } log.ErrEventf(session.context, "ERROR: %v", err) txnState.updateStateAndCleanupOnErr(err, e) return Result{Err: err}, err } tResult := &traceResult{tag: result.PGTag, count: -1} switch result.Type { case parser.RowsAffected: tResult.count = result.RowsAffected case parser.Rows: tResult.count = result.Rows.Len() } if traceSQL { log.Eventf(txnState.txn.Context, "%s done", tResult) } log.Eventf(session.context, "%s done", tResult) return result, nil }