This commit is contained in:
jdl
2024-11-11 06:36:55 +01:00
parent d0587cc585
commit c5419d662e
102 changed files with 4181 additions and 0 deletions

45
sqliteutil/README.md Normal file
View 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
View 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
View 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
View 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
})
}

View 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")
}
}

View 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));

View File

@@ -0,0 +1 @@
INSERT INTO users(UserID, Email) VALUES (1, 'a@b.com'), (2, 'c@d.com');

View File

@@ -0,0 +1 @@
DELETE FROM users WHERE UserID=1;

28
sqliteutil/tx.go Normal file
View 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
}