Initial commit

This commit is contained in:
Sebastiaan de Schaetzen 2025-02-18 04:46:58 +01:00
commit 42dbd4e1fa
7 changed files with 290 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea

56
database.go Normal file
View File

@ -0,0 +1,56 @@
package mysqlite
import (
"fmt"
"reflect"
"zombiezen.com/go/sqlite"
)
type Db struct {
Db *sqlite.Conn
}
func OpenDb(databaseSource string) (*Db, error) {
conn, err := sqlite.OpenConn(databaseSource)
if err != nil {
return nil, err
}
return &Db{Db: conn}, nil
}
func (d *Db) Close() error {
return d.Db.Close()
}
func (d *Db) QuerySingle(query string, args ...any) error {
stmt, remaining, err := d.Db.PrepareTransient(query)
if err != nil {
return err
}
defer stmt.Finalize()
if remaining != 0 {
return fmt.Errorf("remaining bytes: %s", remaining)
}
rowReturned, err := stmt.Step()
if err != nil {
return err
}
if !rowReturned {
return fmt.Errorf("did not return any rows")
}
if stmt.ColumnCount() != 1 {
return fmt.Errorf("query returned %d rows while only one was expected", stmt.ColumnCount())
}
for i, arg := range args {
if asString, ok := arg.(*string); ok {
*asString = stmt.ColumnText(i)
} else if asInt, ok := arg.(*int); ok {
*asInt = stmt.ColumnInt(i)
} else if asBool, ok := arg.(*bool); ok {
*asBool = stmt.ColumnBool(i)
} else {
return fmt.Errorf("unsupported column type at index %d", i)
}
}
return nil
}

17
go.mod Normal file
View File

@ -0,0 +1,17 @@
module mysqlite
go 1.24
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.22.0 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.33.1 // indirect
zombiezen.com/go/sqlite v1.4.0 // indirect
)

23
go.sum Normal file
View File

@ -0,0 +1,23 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/sqlite v1.33.1 h1:trb6Z3YYoeM9eDL1O8do81kP+0ejv+YzgyFo+Gwy0nM=
modernc.org/sqlite v1.33.1/go.mod h1:pXV2xHxhzXZsgT/RtTFAPY6JJDEvOTcTdwADQCCWD4k=
zombiezen.com/go/sqlite v1.4.0 h1:N1s3RIljwtp4541Y8rM880qgGIgq3fTD2yks1xftnKU=
zombiezen.com/go/sqlite v1.4.0/go.mod h1:0w9F1DN9IZj9AcLS9YDKMboubCACkwYCGkzoy3eG5ik=

87
migrator.go Normal file
View File

@ -0,0 +1,87 @@
package mysqlite
import (
"database/sql"
"embed"
"fmt"
"io/fs"
"log"
"strconv"
"strings"
"zombiezen.com/go/sqlite"
)
type ReadDirFileFS interface {
fs.ReadDirFS
fs.ReadFileFS
}
func (db *Db) MigrateDb(migrations ReadDirFileFS) error {
// Read all migrations
migrationFiles, err := migrations.ReadDir("")
if err != nil {
log.Fatalf("error reading migration files: %v", err)
}
var migrationsByVersion = make(map[int]string)
latestVersion := 0
for _, f := range migrationFiles {
versionStr := f.Name()
version, err := strconv.Atoi(strings.SplitN(versionStr, "_", 2)[0])
if err != nil {
log.Fatalf("invalid version number for migration script: %v", err)
}
migrationsByVersion[version] = versionStr
latestVersion = max(latestVersion, version)
}
// Get current migration version from user_version
var currentVersion int
err = d.QuerySingle("PRAGMA user_version", &currentVersion)
if err != nil {
log.Fatalf("error getting current version: %v", err)
}
log.Printf("Current database migration version is %d, latest version is %d", currentVersion, latestVersion)
// If we are no up-to-date, bring the db up-to-date
for currentVersion != latestVersion {
targetVersion := currentVersion + 1
migrationFile := migrationsByVersion[targetVersion]
log.Printf("migration to version %s", migrationFile)
migrationScript, err := migrations.ReadFile(migrationFile)
if err != nil {
log.Fatalf("error opening migration script %s: %v", migrationScript, err)
}
tx, err := db.Begin()
if err != nil {
log.Fatalf("error beginning transaction: %v", err)
}
defer tx.MustRollback()
err = tx.QuerySingle(string(migrationScript))
if err != nil {
log.Fatalf("error performing migration: %v", err)
}
err = tx.QuerySingle(fmt.Sprintf("PRAGMA user_version = %d", targetVersion))
if err != nil {
log.Fatalf("error updating version: %v", err)
}
err = tx.Commit()
if err != nil {
log.Fatalf("error commiting transaction: %v", err)
}
currentVersion = targetVersion
}
log.Println("All migrations applied")
return nil
}
func rollbackIgnoringErrors(tx *sql.Tx) {
err := tx.Rollback()
if err != nil {
log.Printf("error rolling back: %v", err)
}
}

72
query.go Normal file
View File

@ -0,0 +1,72 @@
package mysqlite
import (
"fmt"
"zombiezen.com/go/sqlite"
)
type Query struct {
stmt *sqlite.Stmt
err error
}
func (d *Db) Query(query string) *Query {
stmt, remaining, err := d.Db.PrepareTransient(query)
if err != nil {
return &Query{err: err}
}
if remaining != 0 {
return &Query{err: fmt.Errorf("remaining bytes: %s", remaining)}
}
return &Query{stmt: stmt}
}
//func (q *Query) Args(args ...any) *Query {
// return q
//}
func (q *Query) Exec() error {
if q.stmt != nil {
defer q.stmt.Finalize()
}
if q.err != nil {
return q.err
}
rowReturned, err := q.stmt.Step()
if err != nil {
return err
}
if rowReturned {
return fmt.Errorf("row returned unexpectedly")
}
return err
}
func (q *Query) ScanSingle(results ...any) error {
if q.stmt != nil {
defer q.stmt.Finalize()
}
if q.err != nil {
return q.err
}
rowReturned, err := q.stmt.Step()
if err != nil {
return err
}
if !rowReturned {
return fmt.Errorf("did not return any rows")
}
for i, arg := range results {
if asString, ok := arg.(*string); ok {
*asString = q.stmt.ColumnText(i)
} else if asInt, ok := arg.(*int); ok {
*asInt = q.stmt.ColumnInt(i)
} else if asBool, ok := arg.(*bool); ok {
*asBool = q.stmt.ColumnBool(i)
} else {
return fmt.Errorf("unsupported column type at index %d", i)
}
}
return nil
}

34
transaction.go Normal file
View File

@ -0,0 +1,34 @@
package mysqlite
import "log"
type Tx struct {
db *Db
}
func (d *Db) Begin() (*Tx, error) {
err := d.QuerySingle("BEGIN")
if err != nil {
return nil, err
}
return &Tx{db: d}, nil
}
func (tx *Tx) Commit() error {
return tx.Query("COMMIT").Exec()
}
func (tx *Tx) Rollback() error {
return tx.Query("ROLLBACK").Exec()
}
func (tx *Tx) MustRollback() {
err := tx.Rollback()
if err != nil {
log.Panicf("error doing rollback: %w", err)
}
}
func (tx *Tx) Query(query string) *Query {
return tx.db.Query(query)
}