mirror of
https://github.com/tursodatabase/limbo.git
synced 2025-08-03 17:48:17 +00:00
Merge 'Implement transaction support in Go adapter' from Jonathan Ness
This PR implements basic transaction support in the Limbo Go adapter by adding the required methods to fulfill the `driver.Tx` interface. ## Changes - Add `Begin()` method to `limboConn` to start a transaction - Add `BeginTx()` method with context support and proper handling of transaction options - Implement `Commit()` method to commit transaction changes - Implement `Rollback()` method with appropriate error handling - Add transaction tests ## Implementation Details - Uses the standard SQLite transaction commands (BEGIN, COMMIT, ROLLBACK) - Follows the same pattern as other SQL operations in the adapter (prepare-execute-close) - Maintains consistent locking and error handling patterns ## Limitations - Currently, ROLLBACK operations will return an error as they're not yet fully supported in the underlying Limbo implementation - Only the default isolation level is supported; all other isolation levels return `driver.ErrSkip` - Read-only transactions are not supported and return `driver.ErrSkip` ## Testing - Added basic transaction tests that verify BEGIN and COMMIT operations - Adjusted tests to work with the current Limbo implementation capabilities These transaction methods enable the Go adapter to be used in applications that require transaction support while providing clear error messages when unsupported features are requested. I'll add to it when Limbo supports ROLLBACK and/or additional isolation levels. Closes #1435
This commit is contained in:
commit
a105c20f69
3 changed files with 167 additions and 4 deletions
|
@ -67,4 +67,4 @@ cargo build ${CARGO_ARGS} --package limbo-go
|
|||
echo "Copying $OUTPUT_NAME to $OUTPUT_DIR/"
|
||||
cp "../../target/${TARGET_DIR}/$OUTPUT_NAME" "$OUTPUT_DIR/"
|
||||
|
||||
echo "Library built successfully for $PLATFORM ($BUILD_TYPE build)"
|
||||
echo "Library built successfully for $PLATFORM ($BUILD_TYPE build)"
|
|
@ -1,6 +1,7 @@
|
|||
package limbo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
|
@ -136,7 +137,94 @@ func (c *limboConn) Prepare(query string) (driver.Stmt, error) {
|
|||
return newStmt(stmtPtr, query), nil
|
||||
}
|
||||
|
||||
// begin is needed to implement driver.Conn.. for now not implemented
|
||||
func (c *limboConn) Begin() (driver.Tx, error) {
|
||||
return nil, errors.New("transactions not implemented")
|
||||
// limboTx implements driver.Tx
|
||||
type limboTx struct {
|
||||
conn *limboConn
|
||||
}
|
||||
|
||||
// Begin starts a new transaction with default isolation level
|
||||
func (c *limboConn) Begin() (driver.Tx, error) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
if c.ctx == 0 {
|
||||
return nil, errors.New("connection closed")
|
||||
}
|
||||
|
||||
// Execute BEGIN statement
|
||||
stmtPtr := connPrepare(c.ctx, "BEGIN")
|
||||
if stmtPtr == 0 {
|
||||
return nil, c.getError()
|
||||
}
|
||||
|
||||
stmt := newStmt(stmtPtr, "BEGIN")
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.Exec(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &limboTx{conn: c}, nil
|
||||
}
|
||||
|
||||
// BeginTx starts a transaction with the specified options.
|
||||
// Currently only supports default isolation level and non-read-only transactions.
|
||||
func (c *limboConn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
|
||||
// Skip handling non-default isolation levels and read-only mode
|
||||
// for now, letting database/sql package handle these cases
|
||||
if opts.Isolation != driver.IsolationLevel(sql.LevelDefault) || opts.ReadOnly {
|
||||
return nil, driver.ErrSkip
|
||||
}
|
||||
|
||||
// Check for context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
return c.Begin()
|
||||
}
|
||||
}
|
||||
|
||||
// Commit commits the transaction
|
||||
func (tx *limboTx) Commit() error {
|
||||
tx.conn.Lock()
|
||||
defer tx.conn.Unlock()
|
||||
|
||||
if tx.conn.ctx == 0 {
|
||||
return errors.New("connection closed")
|
||||
}
|
||||
|
||||
stmtPtr := connPrepare(tx.conn.ctx, "COMMIT")
|
||||
if stmtPtr == 0 {
|
||||
return tx.conn.getError()
|
||||
}
|
||||
|
||||
stmt := newStmt(stmtPtr, "COMMIT")
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.Exec(nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// Rollback aborts the transaction.
|
||||
// Note: This operation is not currently fully supported by Limbo and will return an error.
|
||||
func (tx *limboTx) Rollback() error {
|
||||
tx.conn.Lock()
|
||||
defer tx.conn.Unlock()
|
||||
|
||||
if tx.conn.ctx == 0 {
|
||||
return errors.New("connection closed")
|
||||
}
|
||||
|
||||
stmtPtr := connPrepare(tx.conn.ctx, "ROLLBACK")
|
||||
if stmtPtr == 0 {
|
||||
return tx.conn.getError()
|
||||
}
|
||||
|
||||
stmt := newStmt(stmtPtr, "ROLLBACK")
|
||||
defer stmt.Close()
|
||||
|
||||
_, err := stmt.Exec(nil)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -282,6 +282,81 @@ func TestDriverRowsErrorMessages(t *testing.T) {
|
|||
t.Log("Rows error behavior test passed")
|
||||
}
|
||||
|
||||
func TestTransaction(t *testing.T) {
|
||||
// Open database connection
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Create a test table
|
||||
_, err = db.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating table: %v", err)
|
||||
}
|
||||
|
||||
// Insert initial data
|
||||
_, err = db.Exec("INSERT INTO test (id, name) VALUES (1, 'Initial')")
|
||||
if err != nil {
|
||||
t.Fatalf("Error inserting initial data: %v", err)
|
||||
}
|
||||
|
||||
// Begin a transaction
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatalf("Error starting transaction: %v", err)
|
||||
}
|
||||
|
||||
// Insert data within the transaction
|
||||
_, err = tx.Exec("INSERT INTO test (id, name) VALUES (2, 'Transaction')")
|
||||
if err != nil {
|
||||
t.Fatalf("Error inserting data in transaction: %v", err)
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
t.Fatalf("Error committing transaction: %v", err)
|
||||
}
|
||||
|
||||
// Verify both rows are visible after commit
|
||||
rows, err := db.Query("SELECT id, name FROM test ORDER BY id")
|
||||
if err != nil {
|
||||
t.Fatalf("Error querying data after commit: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
expected := []struct {
|
||||
id int
|
||||
name string
|
||||
}{
|
||||
{1, "Initial"},
|
||||
{2, "Transaction"},
|
||||
}
|
||||
|
||||
i := 0
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var name string
|
||||
if err := rows.Scan(&id, &name); err != nil {
|
||||
t.Fatalf("Error scanning row: %v", err)
|
||||
}
|
||||
|
||||
if id != expected[i].id || name != expected[i].name {
|
||||
t.Errorf("Row %d: expected (%d, %s), got (%d, %s)",
|
||||
i, expected[i].id, expected[i].name, id, name)
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
if i != 2 {
|
||||
t.Fatalf("Expected 2 rows, got %d", i)
|
||||
}
|
||||
|
||||
t.Log("Transaction test passed")
|
||||
}
|
||||
|
||||
func TestVectorOperations(t *testing.T) {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue