6 Commits

Author SHA1 Message Date
94ae36305a Add support for pointers to pointers of arguments
All checks were successful
Build / build (push) Successful in 29s
2025-03-16 18:07:58 +01:00
2eacf6fbc4 Improve migration handling and error forwarding in deferred statements
All checks were successful
Build / build (push) Successful in 42s
2025-03-16 11:38:40 +01:00
2ff3477812 Name step
All checks were successful
Build / build (push) Successful in 28s
2025-03-16 11:06:41 +01:00
68f8dc50e0 Use go action
All checks were successful
Build / build (push) Successful in 1m56s
2025-03-16 11:06:12 +01:00
187ed5987d Add update test
All checks were successful
Build / build (push) Successful in 1m13s
2025-03-16 11:04:21 +01:00
a377448de3 Add readme
All checks were successful
Build / build (push) Successful in 1m22s
2025-03-16 09:31:00 +01:00
7 changed files with 204 additions and 14 deletions

View File

@@ -7,5 +7,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '>=1.24'
- name: Test - name: Test
run: go test . -v run: go test . -v

92
README.md Normal file
View File

@@ -0,0 +1,92 @@
# MySQLite
A Go library that provides a convenient wrapper around SQLite with additional functionality for database management, migrations, and transactions.
## Features
- Simple and intuitive SQLite database connection management
- Thread-safe database operations with built-in locking mechanism
- Support for database migrations
- Transaction management
- Built on top of [zombiezen.com/go/sqlite](https://pkg.go.dev/zombiezen.com/go/sqlite)
## Installation
```bash
go get gitea.seeseepuff.be/seeseemelk/mysqlite
```
## Usage
### Opening a Database Connection
```go
import "gitea.seeseepuff.be/seeseemelk/mysqlite"
// Open an in-memory database
db, err := mysqlite.OpenDb(":memory:")
if err != nil {
// Handle error
}
defer db.Close()
// Open a file-based database
db, err := mysqlite.OpenDb("path/to/database.db")
if err != nil {
// Handle error
}
defer db.Close()
```
### Executing Queries
The library provides methods for executing SQL queries and managing transactions:
```go
// Execute a simple query
err := db.Query("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)").Exec()
// Use transactions
tx, err := db.BeginTransaction()
if err != nil {
// Handle error
}
// Perform operations within transaction
// ...
// Commit or rollback
err = tx.Commit() // or tx.Rollback()
```
### Database Migrations
The library includes support for SQL-based migrations. Migrations are SQL files stored in a directory and are executed in order based on their filename prefix:
1. Create a directory for your migrations (e.g., `migrations/`)
2. Add numbered SQL migration files:
```
migrations/
├── 1_initial.sql
├── 2_add_users.sql
├── 3_add_posts.sql
```
3. Embed the migrations in your Go code:
```go
import "embed"
//go:embed migrations/*.sql
var migrations embed.FS
// Apply migrations
err := db.MigrateDb(migrations, "migrations")
if err != nil {
// Handle error
}
```
Each migration file should contain valid SQL statements. The migrations are executed in order and are tracked internally to ensure they only run once.
## Requirements
- Go 1.24 or higher

View File

@@ -74,6 +74,9 @@ func performSingleMigration(d *Db, migrationScript []byte, targetVersion int) er
for _, statement := range statements { for _, statement := range statements {
statement = strings.TrimSpace(statement) statement = strings.TrimSpace(statement)
if statement == "" {
continue
}
err = tx.Query(statement).Exec() err = tx.Query(statement).Exec()
if err != nil { if err != nil {
return fmt.Errorf("error performing migration: %v", err) return fmt.Errorf("error performing migration: %v", err)

View File

@@ -58,7 +58,7 @@ func (q *Query) Exec() (rerr error) {
defer q.unlock() defer q.unlock()
if q.stmt != nil { if q.stmt != nil {
defer func() { rerr = q.stmt.Finalize() }() defer func() { forwardError(q.stmt.Finalize(), &rerr) }()
} }
if q.err != nil { if q.err != nil {
return q.err return q.err
@@ -84,7 +84,7 @@ func (q *Query) ScanSingle(results ...any) (rerr error) {
defer q.unlock() defer q.unlock()
// Scan rows // Scan rows
if q.stmt != nil { if q.stmt != nil {
defer func() { rerr = q.stmt.Finalize() }() defer func() { forwardError(q.stmt.Finalize(), &rerr) }()
} }
if q.err != nil { if q.err != nil {
return q.err return q.err
@@ -173,23 +173,47 @@ func (r *Rows) MustNext() bool {
func (r *Rows) Scan(results ...any) error { func (r *Rows) Scan(results ...any) error {
for i, arg := range results { for i, arg := range results {
if asString, ok := arg.(*string); ok { err := r.scanArgument(i, arg)
*asString = r.query.stmt.ColumnText(i) if err != nil {
} else if asInt, ok := arg.(*int); ok { return err
*asInt = r.query.stmt.ColumnInt(i)
} else if asBool, ok := arg.(*bool); ok {
*asBool = r.query.stmt.ColumnBool(i)
} else {
if reflect.TypeOf(arg).Kind() != reflect.Ptr {
return fmt.Errorf("unsupported column type %s at index %d (it should be a pointer)", reflect.TypeOf(arg).Name(), i)
}
name := reflect.Indirect(reflect.ValueOf(arg)).Type().Name()
return fmt.Errorf("unsupported column type *%s at index %d", name, i)
} }
} }
return nil return nil
} }
func (r *Rows) scanArgument(i int, arg any) error {
if asString, ok := arg.(*string); ok {
*asString = r.query.stmt.ColumnText(i)
} else if asInt, ok := arg.(*int); ok {
*asInt = r.query.stmt.ColumnInt(i)
} else if asBool, ok := arg.(*bool); ok {
*asBool = r.query.stmt.ColumnBool(i)
} else if reflect.TypeOf(arg).Kind() == reflect.Ptr && reflect.TypeOf(arg).Elem().Kind() == reflect.Ptr {
return r.handleNullableType(i, arg)
} else {
if reflect.TypeOf(arg).Kind() != reflect.Ptr {
return fmt.Errorf("unsupported column type %s at index %d (it should be a pointer)", reflect.TypeOf(arg).Name(), i)
}
name := reflect.Indirect(reflect.ValueOf(arg)).Type().Name()
return fmt.Errorf("unsupported column type *%s at index %d", name, i)
}
return nil
}
func (r *Rows) handleNullableType(i int, asPtr any) error {
if r.query.stmt.ColumnIsNull(i) {
reflect.ValueOf(asPtr).Elem().Set(reflect.Zero(reflect.TypeOf(asPtr).Elem()))
} else {
value := reflect.New(reflect.TypeOf(asPtr).Elem().Elem()).Interface()
err := r.scanArgument(i, value)
if err != nil {
return err
}
reflect.ValueOf(asPtr).Elem().Set(reflect.ValueOf(value))
}
return nil
}
func (r *Rows) MustScan(results ...any) { func (r *Rows) MustScan(results ...any) {
err := r.Scan(results...) err := r.Scan(results...)
if err != nil { if err != nil {

View File

@@ -66,3 +66,47 @@ func TestQueryWithRange(t *testing.T) {
} }
require.NoError(t, err) require.NoError(t, err)
} }
func TestUpdateQuery(t *testing.T) {
db := openTestDb(t)
func() {
tx := db.MustBegin()
defer tx.MustRollback()
tx.Query("insert into mytable(key, value) values ('lorem', 'bar')").MustExec()
value := "ipsum"
key := "lorem"
tx.Query("update mytable set value = ? where key = ?").Bind(value, key).MustExec()
tx.MustCommit()
}()
var value string
db.Query("select value from mytable where key = 'lorem'").MustScanSingle(&value)
require.Equal(t, "ipsum", value)
}
func TestUpdateQueryWithWrongArguments(t *testing.T) {
db := openTestDb(t)
value := "ipsum"
err := db.Query("insert into mytable(key, value) values ('lorem', ?)").Bind(&value).Exec()
require.Error(t, err)
}
func TestQueryWithPointerStringArguments(t *testing.T) {
db := openTestDb(t)
var result *string
err := db.Query("select value from mytable where key = 'foo'").ScanSingle(&result)
require.NoError(t, err)
require.NotNil(t, result)
require.Equal(t, "bar", *result)
}
func TestQueryWithPointerStringArgumentsCanSetToNull(t *testing.T) {
db := openTestDb(t)
db.Query("update mytable set value=NULL where key = 'foo'").MustExec()
myString := "some string"
var result *string
result = &myString
err := db.Query("select value from mytable where key = 'foo'").ScanSingle(&result)
require.NoError(t, err)
require.Nil(t, result)
}

View File

@@ -15,11 +15,26 @@ func (d *Db) Begin() (*Tx, error) {
return &Tx{db: d}, nil return &Tx{db: d}, nil
} }
func (d *Db) MustBegin() *Tx {
tx, err := d.Begin()
if err != nil {
panic(err)
}
return tx
}
func (tx *Tx) Commit() error { func (tx *Tx) Commit() error {
defer tx.unlock() defer tx.unlock()
return tx.Query("COMMIT").Exec() return tx.Query("COMMIT").Exec()
} }
func (tx *Tx) MustCommit() {
err := tx.Commit()
if err != nil {
panic(err)
}
}
func (tx *Tx) Rollback() error { func (tx *Tx) Rollback() error {
if tx.db == nil { if tx.db == nil {
// The transaction was already commited // The transaction was already commited

7
util.go Normal file
View File

@@ -0,0 +1,7 @@
package mysqlite
func forwardError(from error, to *error) {
if from != nil {
*to = from
}
}