chore: release v0.26.4

This commit is contained in:
2026-08-22 08:35:49 +02:00
parent d48420485f
commit abfed4afa2
16 changed files with 864 additions and 23 deletions
+27 -6
View File
@@ -147,7 +147,17 @@ func NowISO() string { return time.Now().UTC().Format("2006-01-02T15:04:05.000Z"
// Open opens (and creates if needed) the SQLite database at the given path,
// enables performance PRAGMAs, and applies embedded migrations.
func Open(path string) (*sql.DB, error) {
//
// This is the SETTINGS database, which gets the full schema — see roles.go for
// why. Use OpenLogbook for a database that holds contacts only.
func Open(path string) (*sql.DB, error) { return open(path, RoleAll) }
// OpenLogbook opens a SQLite logbook: the qso table and nothing from the
// settings side, and any unused settings tables an earlier version left in it
// are dropped if they are empty.
func OpenLogbook(path string) (*sql.DB, error) { return open(path, RoleLogbook) }
func open(path string, role Role) (*sql.DB, error) {
// Escape only the two characters a path could contain that the DSN would
// otherwise read as its query/fragment delimiters. Windows separators
// (\\ and the drive ':') are left intact — url.PathEscape would mangle them.
@@ -162,10 +172,12 @@ func Open(path string) (*sql.DB, error) {
return nil, fmt.Errorf("ping sqlite: %w", err)
}
Dialect = "sqlite"
if err := migrate(conn, nil, path, filepath.Base(path)); err != nil {
label := filepath.Base(path)
if err := migrate(conn, nil, path, label, role); err != nil {
_ = conn.Close()
return nil, err
}
pruneForeignTables(conn, role, label)
return conn, nil
}
@@ -236,7 +248,7 @@ func backupBeforeRewrite(conn *sql.DB, dbPath, migration string) {
// interleaved migration runs in one log were indistinguishable — an operator
// reported "migrations are very slow" and the lines gave no way to tell one
// database migrated three times from three databases migrated once.
func migrate(conn *sql.DB, translate func(string) string, dbPath, label string) error {
func migrate(conn *sql.DB, translate func(string) string, dbPath, label string, role Role) error {
// A non-nil translator means this is the MySQL connection (use the
// per-statement, FK-aware path); nil means a SQLite connection. This is
// determined by the caller's argument, NOT the global Dialect, so the
@@ -331,9 +343,18 @@ func migrate(conn *sql.DB, translate func(string) string, dbPath, label string)
if err != nil {
return fmt.Errorf("begin tx for %s: %w", name, err)
}
if _, err := tx.Exec(sqlText); err != nil {
_ = tx.Rollback()
return fmt.Errorf("apply migration %s: %w", name, err)
// Statement by statement rather than one Exec of the whole file: the role
// filter works per statement, and a settings-table statement must not
// reach a logbook database (see roles.go). Still one transaction, so the
// file remains atomic.
for _, stmt := range splitStatements(sqlText) {
if !keepForRole(stmt, role) {
continue
}
if _, err := tx.Exec(stmt); err != nil {
_ = tx.Rollback()
return fmt.Errorf("apply migration %s: %w", name, err)
}
}
if _, err := tx.Exec(`INSERT INTO schema_migrations(name) VALUES(?)`, name); err != nil {
_ = tx.Rollback()