Pure Go database driver for SQLite using ebitengine/purego
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: first pass

First pass implementing a database/sql/driver.Driver using purego

Steven vanZyl (May 15, 2026, 6:24 PM EDT) 50bbb0f6 b97af1e1

+640
+1
.gitignore
··· 1 + __debug*
+5
README.md
··· 1 1 # PureGo SQLite 2 + 3 + Pure Go database driver for [SQLite](https://sqlite.org) using 4 + [`purego`](https://github.com/ebitengine/purego). 5 + 6 + Requires the SQLite shared library installed. You probably already have it.
+399
driver.go
··· 1 + package sqlite 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "database/sql/driver" 7 + "encoding/json" 8 + "errors" 9 + "fmt" 10 + "hash/maphash" 11 + "io" 12 + "runtime" 13 + "strings" 14 + ) 15 + 16 + var ( 17 + ErrDeprecated = errors.New("This function is deprecated by [database/sql/driver]") 18 + ErrAlreadyClosed = errors.Join(driver.ErrBadConn, errors.New("Already closed")) 19 + ErrBadColumn = errors.New("Bad column name or index") 20 + ) 21 + 22 + func init() { 23 + sql.Register("sqlite", Driver{}) 24 + } 25 + 26 + type Driver struct { 27 + lib *Library 28 + } 29 + 30 + // Open implements [driver.Driver]. 31 + func (d Driver) Open(name string) (driver.Conn, error) { 32 + return nil, ErrDeprecated 33 + } 34 + 35 + // OpenConnector implements [driver.DriverContext]. 36 + func (d Driver) OpenConnector(name string) (driver.Connector, error) { 37 + lib, err := Open() 38 + if err != nil { 39 + return nil, err 40 + } 41 + d.lib = lib 42 + 43 + return Connector{name, &d}, nil 44 + } 45 + 46 + type Connector struct { 47 + name string 48 + driver *Driver 49 + } 50 + 51 + // Connect implements [driver.Connector]. 52 + func (c Connector) Connect(context.Context) (driver.Conn, error) { 53 + var handle DbHandle 54 + errno := c.driver.lib.Open(c.name, &handle) 55 + if errno != Ok { 56 + return nil, errno 57 + } 58 + 59 + return Conn{ 60 + lib: c.driver.lib, 61 + handle: handle, 62 + seed: maphash.MakeSeed(), 63 + prepared: make(map[uint64]Stmt), 64 + }, nil 65 + } 66 + 67 + // Driver implements [driver.Connector]. 68 + func (c Connector) Driver() driver.Driver { 69 + return c.driver 70 + } 71 + 72 + type Conn struct { 73 + lib *Library 74 + handle DbHandle 75 + seed maphash.Seed 76 + prepared map[uint64]Stmt 77 + } 78 + 79 + // Begin implements [driver.Conn]. 80 + func (c Conn) Begin() (driver.Tx, error) { 81 + return nil, ErrDeprecated 82 + } 83 + 84 + // Close implements [driver.Conn]. 85 + func (c Conn) Close() error { 86 + if c.handle == nil { 87 + return ErrAlreadyClosed 88 + } 89 + 90 + errno := c.lib.Close(c.handle) 91 + if errno != Ok { 92 + return errno 93 + } 94 + 95 + c.handle = nil 96 + return nil 97 + } 98 + 99 + // Prepare implements [driver.Conn]. 100 + func (c Conn) Prepare(query string) (driver.Stmt, error) { 101 + return nil, ErrDeprecated 102 + } 103 + 104 + // PrepareContext implements [driver.ConnPrepareContext]. 105 + func (c Conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, error) { 106 + if c.handle == nil { 107 + return nil, ErrAlreadyClosed 108 + } 109 + 110 + // Hash and check manually to avoid storing query strings 111 + h := maphash.String(c.seed, strings.TrimSpace(query)) 112 + stmt, ok := c.prepared[h] 113 + if ok { 114 + return stmt, nil 115 + } 116 + 117 + var stmtHandle StmtHandle 118 + errno := c.lib.Prepare(c.handle, query, len(query), &stmtHandle, nil) 119 + if errno != Ok { 120 + return nil, errno 121 + } 122 + 123 + stmt = Stmt{&c, stmtHandle} 124 + // Make sure that statements are fully cleaned up 125 + // https://pkg.go.dev/runtime#AddCleanup 126 + runtime.AddCleanup(&stmt, func(handle StmtHandle) { 127 + errno = c.lib.Stmt.Finalize(handle) 128 + if errno != Ok { 129 + panic(errno) 130 + } 131 + }, stmt.handle) 132 + c.prepared[h] = stmt 133 + 134 + return stmt, nil 135 + } 136 + 137 + type Tx struct { 138 + conn *Conn 139 + } 140 + 141 + // Commit implements [driver.Tx]. 142 + func (t Tx) Commit() error { 143 + errno := t.conn.lib.Exec(t.conn.handle, "COMMIT;", nil, nil, nil) 144 + if errno != Ok { 145 + return errno 146 + } 147 + return nil 148 + } 149 + 150 + // Rollback implements [driver.Tx]. 151 + func (t Tx) Rollback() error { 152 + errno := t.conn.lib.Exec(t.conn.handle, "ROLLBACK;", nil, nil, nil) 153 + if errno != Ok { 154 + return errno 155 + } 156 + return nil 157 + } 158 + 159 + // BeginTx implements [driver.ConnBeginTx]. 160 + func (c Conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) { 161 + errno := c.lib.Exec(c.handle, "BEGIN;", nil, nil, nil) 162 + if errno != Ok { 163 + return nil, errno 164 + } 165 + 166 + return Tx{&c}, nil 167 + } 168 + 169 + type Stmt struct { 170 + conn *Conn 171 + handle StmtHandle 172 + } 173 + 174 + // Exec implements [driver.Stmt]. 175 + func (s Stmt) Exec(args []driver.Value) (driver.Result, error) { 176 + return nil, ErrDeprecated 177 + } 178 + 179 + // Query implements [driver.Stmt]. 180 + func (s Stmt) Query(args []driver.Value) (driver.Rows, error) { 181 + return nil, ErrDeprecated 182 + } 183 + 184 + type Result struct { 185 + count int64 186 + last_id int64 187 + } 188 + 189 + // LastInsertId implements [driver.Result]. 190 + func (r Result) LastInsertId() (int64, error) { 191 + return r.last_id, nil 192 + } 193 + 194 + // RowsAffected implements [driver.Result]. 195 + func (r Result) RowsAffected() (int64, error) { 196 + return r.count, nil 197 + } 198 + 199 + // ExecContext implements [driver.StmtExecContext]. 200 + func (s Stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) { 201 + var lib = s.conn.lib 202 + 203 + err := s.bindArgs(ctx, args) 204 + if err != nil { 205 + return nil, err 206 + } 207 + 208 + // Eagerly performs the full query stepping 209 + for { 210 + errno := lib.Step(s.handle) 211 + switch errno { 212 + case Ok, Row: 213 + continue 214 + case Done: 215 + return Result{ 216 + count: lib.Changes(s.conn.handle), 217 + last_id: lib.LastId(s.conn.handle), 218 + }, nil 219 + default: 220 + return nil, errno 221 + } 222 + } 223 + } 224 + 225 + type Rows struct { 226 + stmt *Stmt 227 + } 228 + 229 + // Close implements [driver.Rows]. 230 + func (r Rows) Close() error { 231 + if r.stmt == nil { 232 + return ErrAlreadyClosed 233 + } 234 + 235 + r.stmt.Close() 236 + r.stmt = nil 237 + return nil 238 + } 239 + 240 + // Columns implements [driver.Rows]. 241 + func (r Rows) Columns() []string { 242 + lib := r.stmt.conn.lib 243 + count := lib.Stmt.Column.Count(r.stmt.handle) 244 + out := make([]string, count) 245 + for i := range count { 246 + str := lib.Stmt.Column.Name(r.stmt.handle, i) 247 + out = append(out, str) 248 + } 249 + return out 250 + } 251 + 252 + // Next implements [driver.Rows]. 253 + func (r Rows) Next(dest []driver.Value) error { 254 + lib := r.stmt.conn.lib 255 + handle := r.stmt.handle 256 + 257 + errno := lib.Step(handle) 258 + if errno == Done { 259 + return io.EOF 260 + } 261 + if errno != Ok { 262 + return errno 263 + } 264 + 265 + count := lib.Stmt.Column.Count(handle) 266 + for i := range count { 267 + dataType := lib.Stmt.Column.Type(handle, i) 268 + switch dataType { 269 + case Int: 270 + dest[i] = lib.Stmt.Column.Int(handle, i) 271 + case Double: 272 + dest[i] = lib.Stmt.Column.Double(handle, i) 273 + case Text: 274 + dest[i] = lib.Stmt.Column.Text(handle, i) 275 + case Blob: 276 + dest[i] = lib.Stmt.Column.Blob(handle, i) 277 + case Null: 278 + dest[i] = nil 279 + } 280 + } 281 + 282 + return nil 283 + } 284 + 285 + // QueryContext implements [driver.StmtQueryContext]. 286 + func (s Stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) { 287 + err := s.bindArgs(ctx, args) 288 + if err != nil { 289 + return nil, err 290 + } 291 + 292 + return Rows{&s}, nil 293 + } 294 + 295 + func (s Stmt) bindArgs(ctx context.Context, args []driver.NamedValue) error { 296 + lib := s.conn.lib 297 + for _, arg := range args { 298 + // Handle deadline 299 + if ctx.Err() != nil { 300 + return ctx.Err() 301 + } 302 + 303 + index := arg.Ordinal 304 + if arg.Name != "" { 305 + index = lib.Stmt.Index(s.handle, arg.Name) 306 + if index == 0 { 307 + return ErrBadColumn 308 + } 309 + } 310 + 311 + if arg.Value == nil { 312 + lib.Stmt.Bind.Null(s.handle, index) 313 + continue 314 + } 315 + 316 + // See [Stmt.CheckNamedValue] for conversion process 317 + var errno ErrCode 318 + switch arg.Value.(type) { 319 + case int64: 320 + errno = lib.Stmt.Bind.Int(s.handle, index, arg.Value.(int64)) 321 + case float64: 322 + errno = lib.Stmt.Bind.Double(s.handle, index, arg.Value.(float64)) 323 + case string: 324 + str := arg.Value.(string) 325 + errno = lib.Stmt.Bind.Text(s.handle, index, str, len(str), Transient) 326 + case []byte: 327 + blob := arg.Value.([]byte) 328 + errno = lib.Stmt.Bind.Blob(s.handle, index, blob, len(blob), Transient) 329 + default: 330 + return fmt.Errorf("Unsupported value: %s", arg.Value) 331 + } 332 + if errno != Ok { 333 + return errno 334 + } 335 + } 336 + 337 + return nil 338 + } 339 + 340 + // Close implements [driver.Stmt]. 341 + func (s Stmt) Close() error { 342 + if s.handle == nil { 343 + return ErrAlreadyClosed 344 + } 345 + 346 + errno := s.conn.lib.Stmt.Reset(s.handle) 347 + if errno != Ok { 348 + return errno 349 + } 350 + errno = s.conn.lib.Stmt.Reset(s.handle) 351 + if errno != Ok { 352 + return errno 353 + } 354 + 355 + s.handle = nil 356 + return nil 357 + } 358 + 359 + // NumInput implements [driver.Stmt]. 360 + func (s Stmt) NumInput() int { 361 + return s.conn.lib.Stmt.Count(s.handle) 362 + } 363 + 364 + // CheckNamedValue implements [driver.NamedValueChecker] 365 + func (s Stmt) CheckNamedValue(arg *driver.NamedValue) error { 366 + switch v := arg.Value.(type) { 367 + // The types we don't need to convert 368 + case int64, float64, string, []byte: 369 + break 370 + case int8: 371 + arg.Value = int64(v) 372 + case int16: 373 + arg.Value = int64(v) 374 + case int32: 375 + arg.Value = int64(v) 376 + case uint: 377 + arg.Value = int64(v) 378 + case uint8: 379 + arg.Value = int64(v) 380 + case uint16: 381 + arg.Value = int64(v) 382 + case uint32: 383 + arg.Value = int64(v) 384 + case uint64: 385 + arg.Value = int64(v) 386 + case uintptr: 387 + arg.Value = int64(v) 388 + case float32: 389 + arg.Value = float64(v) 390 + default: 391 + // All else fails we use JSON 392 + data, err := json.Marshal(arg.Value) 393 + if err != nil { 394 + return err 395 + } 396 + arg.Value = data 397 + } 398 + return nil 399 + }
+57
driver_test.go
··· 1 + package sqlite 2 + 3 + import ( 4 + "database/sql" 5 + "testing" 6 + ) 7 + 8 + func Test_Registered(t *testing.T) { 9 + db, err := sql.Open("sqlite", ":memory:") 10 + if err != nil { 11 + t.Error(err) 12 + } 13 + db.Close() 14 + } 15 + 16 + func Test_OpenConnector(t *testing.T) { 17 + connector, err := Driver{}.OpenConnector(":memory:") 18 + if err != nil { 19 + t.Error(err) 20 + } 21 + db := sql.OpenDB(connector) 22 + db.Close() 23 + } 24 + 25 + func Test_CreateInsertQuery(t *testing.T) { 26 + db, err := sql.Open("sqlite", ":memory:") 27 + if err != nil { 28 + t.Error(err) 29 + } 30 + defer db.Close() 31 + 32 + _, err = db.Exec("CREATE TABLE test (foo INT, bar BLOB);") 33 + if err != nil { 34 + t.Error(err) 35 + } 36 + _, err = db.Exec("INSERT INTO test (foo, bar) VALUES (?, ?);", 10, "blah blah") 37 + if err != nil { 38 + t.Error(err) 39 + } 40 + 41 + /* */ 42 + row := db.QueryRow("SELECT * FROM test;") 43 + if row.Err() != nil { 44 + t.Error(row.Err()) 45 + } 46 + for err == Row { 47 + var ( 48 + foo int 49 + bar []byte 50 + ) 51 + err = row.Scan(&foo, &bar) 52 + t.Logf("%d %s", foo, bar) 53 + } 54 + if err != nil { 55 + t.Error(err) 56 + } 57 + }
+2
go.mod
··· 1 1 module tangled.org/rushsteve1/purego-sqlite 2 2 3 3 go 1.26.3 4 + 5 + require github.com/ebitengine/purego v0.10.0
+2
go.sum
··· 1 + github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= 2 + github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+160
library.go
··· 1 + package sqlite 2 + 3 + import ( 4 + "fmt" 5 + "runtime" 6 + "unsafe" 7 + 8 + "github.com/ebitengine/purego" 9 + ) 10 + 11 + type LibraryHandle = uintptr 12 + type DbHandle unsafe.Pointer 13 + type StmtHandle unsafe.Pointer 14 + 15 + // https://sqlite.org/rescode.html 16 + type ErrCode int 17 + 18 + const ( 19 + Ok ErrCode = 0 20 + Done ErrCode = 101 21 + Row ErrCode = 100 22 + ) 23 + 24 + // https://sqlite.org/c3ref/c_static.html 25 + var Transient unsafe.Pointer = unsafe.Pointer(^uintptr(0)) 26 + 27 + // https://sqlite.org/c3ref/c_blob.html 28 + type DataType int 29 + 30 + const ( 31 + Int DataType = 1 32 + Double DataType = 2 33 + Text DataType = 3 34 + Blob DataType = 4 35 + Null DataType = 5 36 + ) 37 + 38 + // Store a copy of the loaded library. loading it again is meaningless 39 + var memoizedLibrary *Library 40 + 41 + // Error implements [error] 42 + func (c ErrCode) Error() string { 43 + if memoizedLibrary != nil { 44 + return fmt.Sprintf("SQLite error [%d]: %s", c, memoizedLibrary.ErrStr(c)) 45 + } else { 46 + return fmt.Sprintf("SQLite error [%d] (without loaded library?)", c) 47 + } 48 + } 49 + 50 + // Vtable of functions loaded from shared library 51 + type Library struct { 52 + handle LibraryHandle 53 + // https://sqlite.org/c3ref/open.html 54 + Open func(name string, handle *DbHandle) ErrCode 55 + // https://sqlite.org/c3ref/close.html 56 + Close func(handle DbHandle) ErrCode 57 + // https://sqlite.org/c3ref/exec.html 58 + Exec func(handle DbHandle, sql string, cb func(unsafe.Pointer, int, *string, *string), cba unsafe.Pointer, err *string) ErrCode 59 + // https://sqlite.org/c3ref/prepare.html 60 + Prepare func(handle DbHandle, sql string, len int, stmt *StmtHandle, tail *unsafe.Pointer) ErrCode 61 + // https://sqlite.org/c3ref/step.html 62 + Step func(handle StmtHandle) ErrCode 63 + // https://sqlite.org/c3ref/errcode.html 64 + ErrStr func(code ErrCode) string 65 + // https://sqlite.org/c3ref/changes.html 66 + Changes func(handle DbHandle) int64 67 + // https://sqlite.org/c3ref/last_insert_rowid.html 68 + LastId func(handle DbHandle) int64 69 + 70 + Stmt struct { 71 + // https://sqlite.org/c3ref/bind_parameter_count.html 72 + Count func(handle StmtHandle) int 73 + // https://sqlite.org/c3ref/reset.html 74 + Reset func(handle StmtHandle) ErrCode 75 + // https://sqlite.org/c3ref/clear_bindings.html 76 + Clear func(handle StmtHandle) ErrCode 77 + // https://sqlite.org/c3ref/finalize.html 78 + Finalize func(handle StmtHandle) ErrCode 79 + // https://sqlite.org/c3ref/bind_parameter_index.html 80 + Index func(handle StmtHandle, name string) int 81 + // https://sqlite.org/c3ref/bind_blob.html 82 + Bind struct { 83 + Int func(handle StmtHandle, index int, value int64) ErrCode 84 + Double func(handle StmtHandle, index int, value float64) ErrCode 85 + Text func(handle StmtHandle, index int, value string, len int, cb unsafe.Pointer) ErrCode 86 + Blob func(handle StmtHandle, index int, value []byte, len int, cb unsafe.Pointer) ErrCode 87 + Null func(handle StmtHandle, index int) ErrCode 88 + } 89 + 90 + // https://sqlite.org/c3ref/column_blob.html 91 + Column struct { 92 + // https://sqlite.org/c3ref/column_count.html 93 + Count func(handle StmtHandle) int 94 + // https://sqlite.org/c3ref/column_name.html 95 + Name func(handle StmtHandle, index int) string 96 + Type func(handle StmtHandle, index int) DataType 97 + 98 + Int func(handle StmtHandle, index int) int 99 + Double func(handle StmtHandle, index int) float64 100 + Text func(handle StmtHandle, index int) string 101 + Blob func(handle StmtHandle, index int) []byte 102 + } 103 + } 104 + } 105 + 106 + // Opens the shared library and binds to the vtable [Library] 107 + func Open() (*Library, error) { 108 + if memoizedLibrary != nil { 109 + return memoizedLibrary, nil 110 + } 111 + 112 + var path string 113 + switch runtime.GOOS { 114 + case "darwin": 115 + path = "libsqlite3.0.dylib" 116 + case "linux": 117 + path = "libsqlite3.0.so" 118 + } 119 + 120 + handle, err := purego.Dlopen(path, 0) 121 + if err != nil { 122 + return nil, err 123 + } 124 + 125 + var lib Library 126 + lib.handle = handle 127 + 128 + purego.RegisterLibFunc(&lib.Open, lib.handle, "sqlite3_open") 129 + purego.RegisterLibFunc(&lib.Close, lib.handle, "sqlite3_close_v2") 130 + purego.RegisterLibFunc(&lib.Exec, lib.handle, "sqlite3_exec") 131 + purego.RegisterLibFunc(&lib.Prepare, lib.handle, "sqlite3_prepare_v2") 132 + purego.RegisterLibFunc(&lib.Step, lib.handle, "sqlite3_step") 133 + purego.RegisterLibFunc(&lib.ErrStr, lib.handle, "sqlite3_errstr") 134 + purego.RegisterLibFunc(&lib.Changes, lib.handle, "sqlite3_changes") 135 + purego.RegisterLibFunc(&lib.LastId, lib.handle, "sqlite3_last_insert_rowid") 136 + 137 + purego.RegisterLibFunc(&lib.Stmt.Count, lib.handle, "sqlite3_bind_parameter_count") 138 + purego.RegisterLibFunc(&lib.Stmt.Reset, lib.handle, "sqlite3_reset") 139 + purego.RegisterLibFunc(&lib.Stmt.Clear, lib.handle, "sqlite3_clear_bindings") 140 + purego.RegisterLibFunc(&lib.Stmt.Finalize, lib.handle, "sqlite3_finalize") 141 + purego.RegisterLibFunc(&lib.Stmt.Index, lib.handle, "sqlite3_bind_parameter_index") 142 + 143 + purego.RegisterLibFunc(&lib.Stmt.Bind.Int, lib.handle, "sqlite3_bind_int64") 144 + purego.RegisterLibFunc(&lib.Stmt.Bind.Double, lib.handle, "sqlite3_bind_double") 145 + purego.RegisterLibFunc(&lib.Stmt.Bind.Text, lib.handle, "sqlite3_bind_text") 146 + purego.RegisterLibFunc(&lib.Stmt.Bind.Blob, lib.handle, "sqlite3_bind_blob64") 147 + purego.RegisterLibFunc(&lib.Stmt.Bind.Null, lib.handle, "sqlite3_bind_null") 148 + 149 + purego.RegisterLibFunc(&lib.Stmt.Column.Count, lib.handle, "sqlite3_column_count") 150 + purego.RegisterLibFunc(&lib.Stmt.Column.Name, lib.handle, "sqlite3_column_name") 151 + purego.RegisterLibFunc(&lib.Stmt.Column.Type, lib.handle, "sqlite3_column_type") 152 + 153 + purego.RegisterLibFunc(&lib.Stmt.Column.Int, lib.handle, "sqlite3_column_int64") 154 + purego.RegisterLibFunc(&lib.Stmt.Column.Double, lib.handle, "sqlite3_column_double") 155 + purego.RegisterLibFunc(&lib.Stmt.Column.Text, lib.handle, "sqlite3_column_text") 156 + purego.RegisterLibFunc(&lib.Stmt.Column.Blob, lib.handle, "sqlite3_column_blob") 157 + 158 + memoizedLibrary = &lib 159 + return memoizedLibrary, nil 160 + }
+14
library_test.go
··· 1 + package sqlite 2 + 3 + import "testing" 4 + 5 + func Test_LibraryOpen(t *testing.T) { 6 + lib, err := Open() 7 + if err != nil { 8 + t.Error(err) 9 + } 10 + 11 + if lib.Open == nil { 12 + t.Error("Missing SQLite Open") 13 + } 14 + }