limbo/bindings/go/connection.go

230 lines
5.5 KiB
Go

package limbo
import (
"context"
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"sync"
"github.com/ebitengine/purego"
)
func init() {
err := ensureLibLoaded()
if err != nil {
panic(err)
}
sql.Register(driverName, &limboDriver{})
}
type limboDriver struct {
sync.Mutex
}
var (
libOnce sync.Once
limboLib uintptr
loadErr error
dbOpen func(string) uintptr
dbClose func(uintptr) uintptr
connPrepare func(uintptr, string) uintptr
connGetError func(uintptr) uintptr
freeBlobFunc func(uintptr)
freeStringFunc func(uintptr)
rowsGetColumns func(uintptr) int32
rowsGetColumnName func(uintptr, int32) uintptr
rowsGetValue func(uintptr, int32) uintptr
rowsGetError func(uintptr) uintptr
closeRows func(uintptr) uintptr
rowsNext func(uintptr) uintptr
stmtQuery func(stmtPtr uintptr, argsPtr uintptr, argCount uint64) uintptr
stmtExec func(stmtPtr uintptr, argsPtr uintptr, argCount uint64, changes uintptr) int32
stmtParamCount func(uintptr) int32
stmtGetError func(uintptr) uintptr
stmtClose func(uintptr) int32
)
// Register all the symbols on library load
func ensureLibLoaded() error {
libOnce.Do(func() {
limboLib, loadErr = loadLibrary()
if loadErr != nil {
return
}
purego.RegisterLibFunc(&dbOpen, limboLib, FfiDbOpen)
purego.RegisterLibFunc(&dbClose, limboLib, FfiDbClose)
purego.RegisterLibFunc(&connPrepare, limboLib, FfiDbPrepare)
purego.RegisterLibFunc(&connGetError, limboLib, FfiDbGetError)
purego.RegisterLibFunc(&freeBlobFunc, limboLib, FfiFreeBlob)
purego.RegisterLibFunc(&freeStringFunc, limboLib, FfiFreeCString)
purego.RegisterLibFunc(&rowsGetColumns, limboLib, FfiRowsGetColumns)
purego.RegisterLibFunc(&rowsGetColumnName, limboLib, FfiRowsGetColumnName)
purego.RegisterLibFunc(&rowsGetValue, limboLib, FfiRowsGetValue)
purego.RegisterLibFunc(&closeRows, limboLib, FfiRowsClose)
purego.RegisterLibFunc(&rowsNext, limboLib, FfiRowsNext)
purego.RegisterLibFunc(&rowsGetError, limboLib, FfiRowsGetError)
purego.RegisterLibFunc(&stmtQuery, limboLib, FfiStmtQuery)
purego.RegisterLibFunc(&stmtExec, limboLib, FfiStmtExec)
purego.RegisterLibFunc(&stmtParamCount, limboLib, FfiStmtParameterCount)
purego.RegisterLibFunc(&stmtGetError, limboLib, FfiStmtGetError)
purego.RegisterLibFunc(&stmtClose, limboLib, FfiStmtClose)
})
return loadErr
}
func (d *limboDriver) Open(name string) (driver.Conn, error) {
d.Lock()
conn, err := openConn(name)
d.Unlock()
if err != nil {
return nil, err
}
return conn, nil
}
type limboConn struct {
sync.Mutex
ctx uintptr
}
func openConn(dsn string) (*limboConn, error) {
ctx := dbOpen(dsn)
if ctx == 0 {
return nil, fmt.Errorf("failed to open database for dsn=%q", dsn)
}
return &limboConn{
sync.Mutex{},
ctx,
}, loadErr
}
func (c *limboConn) Close() error {
if c.ctx == 0 {
return nil
}
c.Lock()
dbClose(c.ctx)
c.Unlock()
c.ctx = 0
return nil
}
func (c *limboConn) getError() error {
if c.ctx == 0 {
return errors.New("connection closed")
}
err := connGetError(c.ctx)
if err == 0 {
return nil
}
defer freeStringFunc(err)
cpy := fmt.Sprintf("%s", GoString(err))
return errors.New(cpy)
}
func (c *limboConn) Prepare(query string) (driver.Stmt, error) {
if c.ctx == 0 {
return nil, errors.New("connection closed")
}
c.Lock()
defer c.Unlock()
stmtPtr := connPrepare(c.ctx, query)
if stmtPtr == 0 {
return nil, c.getError()
}
return newStmt(stmtPtr, query), nil
}
// 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
}