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