Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2eacf6fbc4 | |||
| 2ff3477812 | |||
| 68f8dc50e0 | |||
| 187ed5987d | |||
| a377448de3 | |||
| 0a177e0b46 | |||
| 82c7f57078 | |||
| 9d5c0bcbb1 |
@@ -7,5 +7,10 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '>=1.24'
|
||||
|
||||
- name: Test
|
||||
run: go test . -v
|
||||
|
||||
92
README.md
Normal file
92
README.md
Normal 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
|
||||
12
database.go
12
database.go
@@ -2,12 +2,14 @@ package mysqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"zombiezen.com/go/sqlite"
|
||||
)
|
||||
|
||||
// Db holds a connection to a SQLite database.
|
||||
type Db struct {
|
||||
Db *sqlite.Conn
|
||||
Db *sqlite.Conn
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// OpenDb opens a new connection to a SQLite database.
|
||||
@@ -35,3 +37,11 @@ func (d *Db) MustClose() {
|
||||
panic(fmt.Sprintf("error closing db: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Db) Lock() {
|
||||
d.lock.Lock()
|
||||
}
|
||||
|
||||
func (d *Db) Unlock() {
|
||||
d.lock.Unlock()
|
||||
}
|
||||
|
||||
29
migrator.go
29
migrator.go
@@ -50,7 +50,7 @@ func (d *Db) MigrateDb(filesystem ReadDirFileFS, directory string) error {
|
||||
return fmt.Errorf("error opening migration script %s: %v", migrationScript, err)
|
||||
}
|
||||
|
||||
err = performSingleMigration(err, d, migrationScript, targetVersion)
|
||||
err = performSingleMigration(d, migrationScript, targetVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -61,21 +61,32 @@ func (d *Db) MigrateDb(filesystem ReadDirFileFS, directory string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func performSingleMigration(err error, d *Db, migrationScript []byte, targetVersion int) error {
|
||||
func performSingleMigration(d *Db, migrationScript []byte, targetVersion int) error {
|
||||
script := string(migrationScript)
|
||||
// Split script based on semicolon
|
||||
statements := strings.Split(script, ";")
|
||||
|
||||
tx, err := d.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error beginning transaction: %v", err)
|
||||
}
|
||||
defer tx.MustRollback()
|
||||
|
||||
err = tx.Query(string(migrationScript)).Exec()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error performing migration: %v", err)
|
||||
}
|
||||
for _, statement := range statements {
|
||||
statement = strings.TrimSpace(statement)
|
||||
if statement == "" {
|
||||
continue
|
||||
}
|
||||
err = tx.Query(statement).Exec()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error performing migration: %v", err)
|
||||
}
|
||||
|
||||
err = tx.Query(fmt.Sprintf("PRAGMA user_version = %d", targetVersion)).Exec()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating version: %v", err)
|
||||
}
|
||||
|
||||
err = tx.Query(fmt.Sprintf("PRAGMA user_version = %d", targetVersion)).Exec()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating version: %v", err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
|
||||
@@ -17,4 +17,8 @@ func TestDb_MigrateDb(t *testing.T) {
|
||||
var count int
|
||||
db.Query("select count(*) from mydata").MustScanSingle(&count)
|
||||
require.Equal(t, 1, count, "incorrect number of rows in database")
|
||||
|
||||
count = 0
|
||||
db.Query("select count(*) from multiTable").MustScanSingle(&count)
|
||||
require.Equal(t, 1, count, "incorrect number of rows in database")
|
||||
}
|
||||
|
||||
26
query.go
26
query.go
@@ -9,10 +9,20 @@ import (
|
||||
|
||||
type Query struct {
|
||||
stmt *sqlite.Stmt
|
||||
err error
|
||||
// Reference to the database. If set, it is assumed that a lock was taken
|
||||
// by the query that should be freed by the query.
|
||||
db *Db
|
||||
err error
|
||||
}
|
||||
|
||||
func (d *Db) Query(query string) *Query {
|
||||
d.Lock()
|
||||
q := d.query(query)
|
||||
q.db = d
|
||||
return q
|
||||
}
|
||||
|
||||
func (d *Db) query(query string) *Query {
|
||||
stmt, remaining, err := d.Db.PrepareTransient(query)
|
||||
if err != nil {
|
||||
return &Query{err: err}
|
||||
@@ -45,8 +55,10 @@ func (q *Query) Bind(args ...any) *Query {
|
||||
}
|
||||
|
||||
func (q *Query) Exec() (rerr error) {
|
||||
defer q.unlock()
|
||||
|
||||
if q.stmt != nil {
|
||||
defer func() { rerr = q.stmt.Finalize() }()
|
||||
defer func() { forwardError(q.stmt.Finalize(), &rerr) }()
|
||||
}
|
||||
if q.err != nil {
|
||||
return q.err
|
||||
@@ -69,9 +81,10 @@ func (q *Query) MustExec() {
|
||||
}
|
||||
|
||||
func (q *Query) ScanSingle(results ...any) (rerr error) {
|
||||
defer q.unlock()
|
||||
// Scan rows
|
||||
if q.stmt != nil {
|
||||
defer func() { rerr = q.stmt.Finalize() }()
|
||||
defer func() { forwardError(q.stmt.Finalize(), &rerr) }()
|
||||
}
|
||||
if q.err != nil {
|
||||
return q.err
|
||||
@@ -114,6 +127,12 @@ func (q *Query) MustScanSingle(results ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Query) unlock() {
|
||||
if q.db != nil {
|
||||
q.db.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
type Rows struct {
|
||||
query *Query
|
||||
}
|
||||
@@ -125,6 +144,7 @@ func (q *Query) ScanMulti() (*Rows, error) {
|
||||
}
|
||||
|
||||
func (r *Rows) Finish() error {
|
||||
defer r.query.unlock()
|
||||
return r.query.stmt.Finalize()
|
||||
}
|
||||
|
||||
|
||||
@@ -66,3 +66,27 @@ func TestQueryWithRange(t *testing.T) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
3
testMigrations/3_multicomment.sql
Normal file
3
testMigrations/3_multicomment.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
create table multiTable(value text);
|
||||
|
||||
insert into multiTable(value) values ('testValue');
|
||||
@@ -7,18 +7,40 @@ type Tx struct {
|
||||
}
|
||||
|
||||
func (d *Db) Begin() (*Tx, error) {
|
||||
err := d.Query("BEGIN").Exec()
|
||||
d.Lock()
|
||||
err := d.query("BEGIN").Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 {
|
||||
defer tx.unlock()
|
||||
return tx.Query("COMMIT").Exec()
|
||||
}
|
||||
|
||||
func (tx *Tx) MustCommit() {
|
||||
err := tx.Commit()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (tx *Tx) Rollback() error {
|
||||
if tx.db == nil {
|
||||
// The transaction was already commited
|
||||
return nil
|
||||
}
|
||||
defer tx.unlock()
|
||||
return tx.Query("ROLLBACK").Exec()
|
||||
}
|
||||
|
||||
@@ -29,6 +51,16 @@ func (tx *Tx) MustRollback() {
|
||||
}
|
||||
}
|
||||
|
||||
func (tx *Tx) Query(query string) *Query {
|
||||
return tx.db.Query(query)
|
||||
func (tx *Tx) unlock() {
|
||||
if tx.db != nil {
|
||||
tx.db.Unlock()
|
||||
tx.db = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (tx *Tx) Query(query string) *Query {
|
||||
if tx.db == nil {
|
||||
panic("query was performed on a transaction after Commit or Rollback")
|
||||
}
|
||||
return tx.db.query(query)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user