wip
This commit is contained in:
45
sqliteutil/README.md
Normal file
45
sqliteutil/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# sqliteutil
|
||||
|
||||
## Transactions
|
||||
|
||||
Simplify postgres transactions using `WithTx` for serializable transactions,
|
||||
or `WithTxDefault` for the default isolation level. Use the `SerialTxRunner`
|
||||
type to get automatic retries of serialization errors.
|
||||
|
||||
## Migrations
|
||||
|
||||
Put your migrations into a directory, for example `migrations`, ordered by name
|
||||
(YYYY-MM-DD prefix, for example). Embed the directory and pass it to the
|
||||
`Migrate` function:
|
||||
|
||||
```Go
|
||||
//go:embed migrations
|
||||
var migrations embed.FS
|
||||
|
||||
func init() {
|
||||
Migrate(db, migrations) // Check the error, of course.
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
In order to test this packge, we need to create a test user and database:
|
||||
|
||||
```
|
||||
sudo su postgres
|
||||
psql
|
||||
|
||||
CREATE DATABASE test;
|
||||
CREATE USER test WITH ENCRYPTED PASSWORD 'test';
|
||||
GRANT ALL PRIVILEGES ON DATABASE test TO test;
|
||||
|
||||
use test
|
||||
|
||||
GRANT ALL ON SCHEMA public TO test;
|
||||
```
|
||||
|
||||
Check that you can connect via the command line:
|
||||
|
||||
```
|
||||
psql -h 127.0.0.1 -U test --password test
|
||||
```
|
||||
5
sqliteutil/go.mod
Normal file
5
sqliteutil/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module git.crumpington.com/lib/sqliteutil
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
2
sqliteutil/go.sum
Normal file
2
sqliteutil/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
82
sqliteutil/migrate.go
Normal file
82
sqliteutil/migrate.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package sqliteutil
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const initMigrationTableQuery = `
|
||||
CREATE TABLE IF NOT EXISTS migrations(filename TEXT NOT NULL PRIMARY KEY);`
|
||||
|
||||
const insertMigrationQuery = `INSERT INTO migrations(filename) VALUES($1)`
|
||||
|
||||
const checkMigrationAppliedQuery = `SELECT EXISTS(SELECT 1 FROM migrations WHERE filename=$1)`
|
||||
|
||||
func Migrate(db *sql.DB, migrationFS embed.FS) error {
|
||||
return WithTx(db, func(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(initMigrationTableQuery); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dirs, err := migrationFS.ReadDir(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(dirs) != 1 {
|
||||
return errors.New("expected a single migrations directory")
|
||||
}
|
||||
|
||||
if !dirs[0].IsDir() {
|
||||
return fmt.Errorf("unexpected non-directory in migration FS: %s", dirs[0].Name())
|
||||
}
|
||||
|
||||
dirName := dirs[0].Name()
|
||||
files, err := migrationFS.ReadDir(dirName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sort sql files by name.
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].Name() < files[j].Name()
|
||||
})
|
||||
|
||||
for _, dirEnt := range files {
|
||||
if !dirEnt.Type().IsRegular() {
|
||||
return fmt.Errorf("unexpected non-regular file in migration fs: %s", dirEnt.Name())
|
||||
}
|
||||
|
||||
var (
|
||||
name = dirEnt.Name()
|
||||
exists bool
|
||||
)
|
||||
|
||||
err := tx.QueryRow(checkMigrationAppliedQuery, name).Scan(&exists)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
continue
|
||||
}
|
||||
|
||||
migration, err := migrationFS.ReadFile(filepath.Join(dirName, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(string(migration)); err != nil {
|
||||
return fmt.Errorf("migration %s failed: %v", name, err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(insertMigrationQuery, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
44
sqliteutil/migrate_test.go
Normal file
44
sqliteutil/migrate_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package sqliteutil
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//go:embed test-migrations
|
||||
var testMigrationFS embed.FS
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := Migrate(db, testMigrationFS); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Shouldn't have any effect.
|
||||
if err := Migrate(db, testMigrationFS); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
query := `SELECT EXISTS(SELECT 1 FROM users WHERE UserID=$1)`
|
||||
var exists bool
|
||||
|
||||
if err = db.QueryRow(query, 1).Scan(&exists); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exists {
|
||||
t.Fatal("1 shouldn't exist")
|
||||
}
|
||||
|
||||
if err = db.QueryRow(query, 2).Scan(&exists); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !exists {
|
||||
t.Fatal("2 should exist")
|
||||
}
|
||||
|
||||
}
|
||||
9
sqliteutil/test-migrations/000.sql
Normal file
9
sqliteutil/test-migrations/000.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE users(
|
||||
UserID BIGINT NOT NULL PRIMARY KEY,
|
||||
Email TEXT NOT NULL UNIQUE);
|
||||
|
||||
CREATE TABLE user_notes(
|
||||
UserID BIGINT NOT NULL REFERENCES users(UserID),
|
||||
NoteID BIGINT NOT NULL,
|
||||
Note Text NOT NULL,
|
||||
PRIMARY KEY(UserID,NoteID));
|
||||
1
sqliteutil/test-migrations/001.sql
Normal file
1
sqliteutil/test-migrations/001.sql
Normal file
@@ -0,0 +1 @@
|
||||
INSERT INTO users(UserID, Email) VALUES (1, 'a@b.com'), (2, 'c@d.com');
|
||||
1
sqliteutil/test-migrations/002.sql
Normal file
1
sqliteutil/test-migrations/002.sql
Normal file
@@ -0,0 +1 @@
|
||||
DELETE FROM users WHERE UserID=1;
|
||||
28
sqliteutil/tx.go
Normal file
28
sqliteutil/tx.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package sqliteutil
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// This is a convenience function to run a function within a transaction.
|
||||
func WithTx(db *sql.DB, fn func(*sql.Tx) error) error {
|
||||
// Start a transaction.
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = fn(tx)
|
||||
|
||||
if err == nil {
|
||||
err = tx.Commit()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user