feat(db): button up migrations
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -46,53 +46,60 @@ func NewMgr(c *config.Config) *DBManager {
|
||||
|
||||
// Init manager
|
||||
func (dbm *DBManager) init() error {
|
||||
if dbm.cfg.DBType == "sqlite" || dbm.cfg.DBType == "memory" {
|
||||
var dbLocation string = ":memory:"
|
||||
if dbm.cfg.DBType == "sqlite" {
|
||||
dbLocation = filepath.Join(dbm.cfg.ConfigPath, fmt.Sprintf("%s.db", dbm.cfg.DBName))
|
||||
}
|
||||
|
||||
var err error
|
||||
dbm.DB, err = sql.Open("sqlite", dbLocation)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to open DB: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Single Open Connection
|
||||
dbm.DB.SetMaxOpenConns(1)
|
||||
|
||||
// Execute DDL
|
||||
if _, err := dbm.DB.Exec(ddl, nil); err != nil {
|
||||
log.Errorf("Error executing schema: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Perform Migrations
|
||||
err = dbm.performMigrations()
|
||||
if err != nil && err != goose.ErrNoMigrationFiles {
|
||||
log.Errorf("Error running DB migrations: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set SQLite Settings (After Migrations)
|
||||
pragmaQuery := `
|
||||
PRAGMA foreign_keys = ON;
|
||||
PRAGMA journal_mode = WAL;
|
||||
`
|
||||
if _, err := dbm.DB.Exec(pragmaQuery, nil); err != nil {
|
||||
log.Errorf("Error executing pragma: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache Tables
|
||||
dbm.CacheTempTables()
|
||||
} else {
|
||||
// Build DB Location
|
||||
var dbLocation string
|
||||
switch dbm.cfg.DBType {
|
||||
case "sqlite":
|
||||
dbLocation = filepath.Join(dbm.cfg.ConfigPath, fmt.Sprintf("%s.db", dbm.cfg.DBName))
|
||||
case "memory":
|
||||
dbLocation = ":memory:"
|
||||
default:
|
||||
return fmt.Errorf("unsupported database")
|
||||
}
|
||||
|
||||
var err error
|
||||
dbm.DB, err = sql.Open("sqlite", dbLocation)
|
||||
if err != nil {
|
||||
log.Panicf("Unable to open DB: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Single open connection
|
||||
dbm.DB.SetMaxOpenConns(1)
|
||||
|
||||
// Check if DB is new
|
||||
isNew, err := isEmpty(dbm.DB)
|
||||
if err != nil {
|
||||
log.Panicf("Unable to determine db info: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Init SQLc
|
||||
dbm.Queries = New(dbm.DB)
|
||||
|
||||
// Execute schema
|
||||
if _, err := dbm.DB.Exec(ddl, nil); err != nil {
|
||||
log.Panicf("Error executing schema: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Perform migrations
|
||||
err = dbm.performMigrations(isNew)
|
||||
if err != nil && err != goose.ErrNoMigrationFiles {
|
||||
log.Panicf("Error running DB migrations: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update settings
|
||||
err = dbm.updateSettings()
|
||||
if err != nil {
|
||||
log.Panicf("Error running DB settings update: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache tables
|
||||
go dbm.CacheTempTables()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -136,12 +143,50 @@ func (dbm *DBManager) CacheTempTables() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbm *DBManager) performMigrations() error {
|
||||
// Set DB Migration
|
||||
func (dbm *DBManager) updateSettings() error {
|
||||
// Set SQLite PRAGMA Settings
|
||||
pragmaQuery := `
|
||||
PRAGMA foreign_keys = ON;
|
||||
PRAGMA journal_mode = WAL;
|
||||
`
|
||||
if _, err := dbm.DB.Exec(pragmaQuery, nil); err != nil {
|
||||
log.Errorf("Error executing pragma: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update Antholume Version in DB
|
||||
if _, err := dbm.Queries.UpdateSettings(dbm.Ctx, UpdateSettingsParams{
|
||||
Name: "version",
|
||||
Value: dbm.cfg.Version,
|
||||
}); err != nil {
|
||||
log.Errorf("Error updating DB settings: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbm *DBManager) performMigrations(isNew bool) error {
|
||||
// Create context
|
||||
ctx := context.WithValue(context.Background(), "isNew", isNew)
|
||||
|
||||
// Set DB migration
|
||||
goose.SetBaseFS(migrations)
|
||||
|
||||
// Run Migrations
|
||||
// Run migrations
|
||||
goose.SetLogger(log.StandardLogger())
|
||||
goose.SetDialect("sqlite")
|
||||
return goose.Up(dbm.DB, "migrations")
|
||||
if err := goose.SetDialect("sqlite"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return goose.UpContext(ctx, dbm.DB, "migrations")
|
||||
}
|
||||
|
||||
func isEmpty(db *sql.DB) (bool, error) {
|
||||
var tableCount int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table';").Scan(&tableCount)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return tableCount == 0, nil
|
||||
}
|
||||
|
||||
@@ -14,16 +14,14 @@ func init() {
|
||||
}
|
||||
|
||||
func upUserAuthHash(ctx context.Context, tx *sql.Tx) error {
|
||||
// Validate column doesn't already exist
|
||||
hasCol, err := hasColumn(tx, "users", "auth_hash")
|
||||
if err != nil {
|
||||
return err
|
||||
} else if hasCol {
|
||||
// Determine if we have a new DB or not
|
||||
isNew := ctx.Value("isNew").(bool)
|
||||
if isNew {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy table & create column
|
||||
_, err = tx.Exec(`
|
||||
_, err := tx.Exec(`
|
||||
-- Create Copy Table
|
||||
CREATE TABLE temp_users AS SELECT * FROM users;
|
||||
ALTER TABLE temp_users ADD COLUMN auth_hash TEXT;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# DB Migrations
|
||||
|
||||
```bash
|
||||
# SQL migration
|
||||
goose create migration_name sql
|
||||
|
||||
# Go migration
|
||||
goose create migration_name
|
||||
```
|
||||
|
||||
## Note
|
||||
|
||||
Since we update both the `schema.sql`, as well as the migration files, when we create a new DB it will inherently be up-to-date. We don't want to run the migrations if it's already up-to-date. Instead each migration checks if we have a new DB (via a value passed into the context), and if we do we simply return.
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type columnInfo struct {
|
||||
CID int
|
||||
Name string
|
||||
Type string
|
||||
NotNull int
|
||||
DefaultVal sql.NullString
|
||||
PK int
|
||||
}
|
||||
|
||||
func hasColumn(tx *sql.Tx, table string, column string) (bool, error) {
|
||||
rows, err := tx.Query(fmt.Sprintf("PRAGMA table_info(%s)", table))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
colExists := false
|
||||
for rows.Next() {
|
||||
var col columnInfo
|
||||
if err := rows.Scan(&col.CID, &col.Name, &col.Type, &col.NotNull, &col.DefaultVal, &col.PK); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if col.Name == column {
|
||||
colExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return colExists, nil
|
||||
}
|
||||
@@ -93,6 +93,13 @@ type Metadatum struct {
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type Setting struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Pass *string `json:"-"`
|
||||
|
||||
@@ -373,6 +373,15 @@ SET
|
||||
WHERE id = $user_id
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateSettings :one
|
||||
INSERT INTO settings (name, value)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT DO UPDATE
|
||||
SET
|
||||
name = COALESCE(excluded.name, name),
|
||||
value = COALESCE(excluded.value, value)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpsertDevice :one
|
||||
INSERT INTO devices (id, user_id, last_synced, device_name)
|
||||
VALUES (?, ?, ?, ?)
|
||||
|
||||
@@ -1213,6 +1213,33 @@ func (q *Queries) UpdateProgress(ctx context.Context, arg UpdateProgressParams)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateSettings = `-- name: UpdateSettings :one
|
||||
INSERT INTO settings (name, value)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT DO UPDATE
|
||||
SET
|
||||
name = COALESCE(excluded.name, name),
|
||||
value = COALESCE(excluded.value, value)
|
||||
RETURNING id, name, value, created_at
|
||||
`
|
||||
|
||||
type UpdateSettingsParams struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateSettings(ctx context.Context, arg UpdateSettingsParams) (Setting, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateSettings, arg.Name, arg.Value)
|
||||
var i Setting
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Value,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateUser = `-- name: UpdateUser :one
|
||||
UPDATE users
|
||||
SET
|
||||
|
||||
@@ -44,7 +44,6 @@ CREATE TABLE IF NOT EXISTS documents (
|
||||
-- Metadata
|
||||
CREATE TABLE IF NOT EXISTS metadata (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
document_id TEXT NOT NULL,
|
||||
|
||||
title TEXT,
|
||||
@@ -108,6 +107,16 @@ CREATE TABLE IF NOT EXISTS activity (
|
||||
FOREIGN KEY (device_id) REFERENCES devices (id)
|
||||
);
|
||||
|
||||
-- Settings
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
name TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
|
||||
created_at DATETIME NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%SZ', 'now'))
|
||||
);
|
||||
|
||||
---------------------------------------------------------------
|
||||
----------------------- Temporary Tables ----------------------
|
||||
---------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user