commit 42dbd4e1faed8b3e1841918322dc9f51186b1a4c Author: Sebastiaan de Schaetzen Date: Tue Feb 18 04:46:58 2025 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/database.go b/database.go new file mode 100644 index 0000000..2fdf969 --- /dev/null +++ b/database.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..65b8d4d --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..11433c6 --- /dev/null +++ b/go.sum @@ -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= diff --git a/migrator.go b/migrator.go new file mode 100644 index 0000000..b4e62d8 --- /dev/null +++ b/migrator.go @@ -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", ¤tVersion) + 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) + } +} diff --git a/query.go b/query.go new file mode 100644 index 0000000..8e114bc --- /dev/null +++ b/query.go @@ -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 +} diff --git a/transaction.go b/transaction.go new file mode 100644 index 0000000..9bebf23 --- /dev/null +++ b/transaction.go @@ -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) +}