Keep foreign keys on for every database connection (closes #207)
check / check (push) Waiting to run
check / check (push) Waiting to run
SQLite keeps the foreign keys setting per connection, and the server turned it on once with a PRAGMA. A query cancelled while it runs makes the pool close its only connection and open a new one, which then ran without foreign keys until a restart. connect now adds `_pragma=foreign_keys(1)` to the database URL. The driver runs every `_pragma` parameter on each connection it opens, so this also holds for a `DBURL` without `_pragma` parameters. The new test cancels a query on a file database, then checks that the tables are still there and that foreign keys are on. Model: opus-5-5
This commit is contained in:
+11
-7
@@ -90,6 +90,17 @@ func (database *Database) connect(ctx context.Context) error {
|
||||
dbURL = config.DefaultDBURL
|
||||
}
|
||||
|
||||
// SQLite keeps the foreign keys setting per connection. The driver
|
||||
// runs each _pragma parameter on every connection it opens. A
|
||||
// PRAGMA run once would miss the connection the pool opens after a
|
||||
// cancelled query breaks the old one.
|
||||
separator := "?"
|
||||
if strings.Contains(dbURL, "?") {
|
||||
separator = "&"
|
||||
}
|
||||
|
||||
dbURL += separator + "_pragma=foreign_keys(1)"
|
||||
|
||||
database.log.Info(
|
||||
"connecting to database", "url", dbURL,
|
||||
)
|
||||
@@ -109,13 +120,6 @@ func (database *Database) connect(ctx context.Context) error {
|
||||
database.conn = conn
|
||||
database.log.Info("database connected")
|
||||
|
||||
_, err = database.conn.ExecContext(
|
||||
ctx, "PRAGMA foreign_keys = ON",
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("enable foreign keys: %w", err)
|
||||
}
|
||||
|
||||
return database.runMigrations(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/fx/fxtest"
|
||||
"sneak.berlin/go/neoirc/internal/config"
|
||||
@@ -14,6 +17,16 @@ import (
|
||||
|
||||
const wantBusyTimeoutMS = 5000
|
||||
|
||||
const (
|
||||
// endlessQuery counts the rows of a recursive query that never
|
||||
// stops, so it runs until its context ends.
|
||||
endlessQuery = `WITH RECURSIVE n(x) AS
|
||||
(SELECT 1 UNION ALL SELECT x + 1 FROM n)
|
||||
SELECT COUNT(*) FROM n`
|
||||
|
||||
endlessQueryTimeout = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// TestDefaultDBURLSetsWALAndBusyTimeout opens a file database
|
||||
// with the default URL's parameters the way the server does,
|
||||
// and checks the journal mode and busy timeout it ends up with.
|
||||
@@ -73,3 +86,84 @@ func TestDefaultDBURLSetsWALAndBusyTimeout(t *testing.T) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TestForeignKeysOnAfterCancelledQuery cancels a query while it
|
||||
// runs, which makes the pool close its only connection and open a
|
||||
// new one. It checks that the file database keeps its tables and
|
||||
// that the new connection has foreign keys on.
|
||||
func TestForeignKeysOnAfterCancelledQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, defaultParams, _ := strings.Cut(config.DefaultDBURL, "?")
|
||||
|
||||
urlParams := map[string]string{
|
||||
"default parameters": "?" + defaultParams,
|
||||
"no parameters": "",
|
||||
}
|
||||
|
||||
for name, params := range urlParams {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbURL := "file://" +
|
||||
filepath.Join(t.TempDir(), "state.db") + params
|
||||
|
||||
lifecycle := fxtest.NewLifecycle(t)
|
||||
|
||||
log, err := logger.New(lifecycle, logger.Params{
|
||||
Globals: &globals.Globals{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
database, err := db.New(lifecycle, db.Params{
|
||||
Logger: log,
|
||||
Config: &config.Config{DBURL: dbURL},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lifecycle.RequireStart()
|
||||
t.Cleanup(lifecycle.RequireStop)
|
||||
|
||||
ctx, cancel := context.WithTimeout(
|
||||
t.Context(), endlessQueryTimeout,
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
_, err = database.GetDB().ExecContext(ctx, endlessQuery)
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("endless query: err = %v", err)
|
||||
}
|
||||
|
||||
var migrations int
|
||||
|
||||
err = database.GetDB().QueryRowContext(
|
||||
t.Context(),
|
||||
"SELECT COUNT(*) FROM schema_migrations",
|
||||
).Scan(&migrations)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if migrations == 0 {
|
||||
t.Error("schema_migrations is empty")
|
||||
}
|
||||
|
||||
var foreignKeys int
|
||||
|
||||
err = database.GetDB().QueryRowContext(
|
||||
t.Context(), "PRAGMA foreign_keys",
|
||||
).Scan(&foreignKeys)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if foreignKeys != 1 {
|
||||
t.Errorf("foreign_keys = %d, want 1", foreignKeys)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user