diff --git a/commands.go b/commands.go index 759151c..2e88ebb 100644 --- a/commands.go +++ b/commands.go @@ -56,6 +56,17 @@ func doDML(ctx context.Context, conn canExec, query string, args []any, w io.Wri return count, nil } +func doTCL(ctx context.Context, ss *session, query string) error { + if ss.tx == nil { + return ErrNoActiveTransaction + } + _, err := ss.tx.ExecContext(ctx, query) + if err == nil { + fmt.Fprintln(ss.stdErr, "Ok") + } + return err +} + func (ss *session) commit() error { var err error if ss.tx != nil { diff --git a/interactive.go b/interactive.go index 6906a03..b4fbbd1 100644 --- a/interactive.go +++ b/interactive.go @@ -49,18 +49,17 @@ func newReservedWordPattern(list ...string) reserveWordPattern { var o = struct{}{} var oneLineCommands = map[string]struct{}{ - `COMMIT`: o, - `DESC`: o, - `EDIT`: o, - `EXIT`: o, - `HISTORY`: o, - `HOST`: o, - `QUIT`: o, - `REM`: o, - `ROLLBACK`: o, - `SPOOL`: o, - `START`: o, - `\D`: o, + `COMMIT`: o, + `DESC`: o, + `EDIT`: o, + `EXIT`: o, + `HISTORY`: o, + `HOST`: o, + `QUIT`: o, + `REM`: o, + `SPOOL`: o, + `START`: o, + `\D`: o, } func isOneLineCommand(cmdLine string) bool { @@ -122,7 +121,7 @@ func (ss *session) newInteractiveIn() *interactiveIn { editor.ResetColor = "\x1B[0m" editor.DefaultColor = "\x1B[39;49;1m" editor.Highlight = []readline.Highlight{ - {Pattern: newReservedWordPattern("HOST", "ALTER", "COMMIT", "CREATE", "DELETE", "DESC", "DROP", "EXIT", "HISTORY", "INSERT", "QUIT", "REM", "ROLLBACK", "SELECT", "SPOOL", "START", "TRUNCATE", "UPDATE", "AND", "FROM", "INTO", "OR", "WHERE"), Sequence: "\x1B[36;49;1m"}, + {Pattern: newReservedWordPattern("HOST", "ALTER", "COMMIT", "CREATE", "DELETE", "DESC", "DROP", "EXIT", "HISTORY", "INSERT", "QUIT", "REM", "ROLLBACK", "SELECT", "SPOOL", "START", "TRUNCATE", "UPDATE", "AND", "FROM", "INTO", "OR", "WHERE", "SAVEPOINT", "TO", "TRANSACTION"), Sequence: "\x1B[36;49;1m"}, {Pattern: regexp.MustCompile(`[0-9]+`), Sequence: "\x1B[35;49;1m"}, {Pattern: regexp.MustCompile(`/\*.*?\*/`), Sequence: "\x1B[33;49;22m"}, {Pattern: regexp.MustCompile(`"[^"]*"|"[^"]*$`), Sequence: "\x1B[31;49;1m"}, diff --git a/internal/sqlcompletion/main.go b/internal/sqlcompletion/main.go index 26f31f3..6735522 100644 --- a/internal/sqlcompletion/main.go +++ b/internal/sqlcompletion/main.go @@ -30,6 +30,7 @@ func getSqlCommands() []string { "quit", "rem", "rollback", + "savepoint", "select", "spool", "start", @@ -108,6 +109,13 @@ func (C *completeType) getCandidates(ctx context.Context, fields []string) ([]st v, _ := completion.PathComplete(fields[:i+1]) return v } + } else if strings.EqualFold(word, "rollback") { + tableListNow = false + lastKeywordAt = i + nextKeyword = nil + candidates = func() []string { + return []string{"to", "transaction"} + } } else { if tableListNow && i < len(fields)-1 { tableNameInline = append(tableNameInline, word) diff --git a/loop.go b/loop.go index c0a0b08..6d8524a 100644 --- a/loop.go +++ b/loop.go @@ -72,6 +72,8 @@ var ( ErrBeginIsNotSupported = errors.New("'BEGIN' is not supported; transactions are managed automatically") ErrNoDataFound = errors.New("no data found") ErrNotSupported = errors.New("not supported") + ErrInvalidRollback = errors.New("invalid ROLLBACK syntax: expected 'TO' or 'TRANSACTION'") + ErrNoActiveTransaction = errors.New("no active transaction") ) func (ss *session) prompt(w io.Writer, i int) (int, error) { @@ -160,6 +162,19 @@ func (ss *session) Loop(ctx context.Context, commandIn commandIn) error { case "SELECT": misc.Echo(ss.spool, query) err = doSelect(ctx, ss, query, nil) + case "ROLLBACK": + misc.Echo(ss.spool, query) + arg, _ = misc.CutField(arg) + if arg == "" { + err = ss.rollback() + } else if strings.EqualFold(arg, "TO") || strings.EqualFold(arg, "TRANSACTION") { + err = doTCL(ctx, ss, query) + } else { + err = ErrInvalidRollback + } + case "SAVEPOINT": + misc.Echo(ss.spool, query) + doTCL(ctx, ss, query) case "DELETE", "INSERT", "UPDATE", "MERGE": misc.Echo(ss.spool, query) isNewTx := (ss.tx == nil) @@ -174,9 +189,6 @@ func (ss *session) Loop(ctx context.Context, commandIn commandIn) error { case "COMMIT": misc.Echo(ss.spool, query) err = ss.commit() - case "ROLLBACK": - misc.Echo(ss.spool, query) - err = ss.rollback() case "EXIT", "QUIT": if ss.tx == nil || commandIn.CanCloseInTransaction() { return nil diff --git a/release_note_en.md b/release_note_en.md index a1922d8..2cadbdc 100644 --- a/release_note_en.md +++ b/release_note_en.md @@ -2,6 +2,9 @@ - Refactor `dialect` subpackage: rename fields and methods for clarity (#8) - Updated `go-readline-ny` to v1.12.2 and `go-ttyadapter` to v0.2.0, and switched API calls to use `go-ttyadapter`.(#9) +- Support `SAVEPOINT` as a TCL command (#11) +- Support `ROLLBACK TO` (or `ROLLBACK TRANSACTION`) as a TCL command (#11) +- Require `;` after `ROLLBACK` to prevent accidental execution (#11) v0.25.0 ======= diff --git a/release_note_ja.md b/release_note_ja.md index ab55a82..df5a8a0 100644 --- a/release_note_ja.md +++ b/release_note_ja.md @@ -2,6 +2,9 @@ - サブパッケージ `dialect` をリファクタリング: フィールド・メソッドを改名 (#8) - `go-readline-ny` を v1.12.2、`go-ttyadapter` を v0.2.0 に更新し、対応する API 呼び出しを `go-ttyadapter` 側に切り替えた。(#9) +- `SAVEPOINT` を TCL コマンドとしてサポート (#11) +- `ROLLBACK TO`(もしくは `ROLLBACK TRANSACTION`)を TCL コマンドとしてサポート (#11) +- 誤操作による実行を防ぐため、`ROLLBACK` には `;` を必須とした (#11) v0.25.0 =======