replace manual SQL migration with goose (v3); embed migrations in binary; remove schema.sql / SCHEMA_PATH
This commit is contained in:
@@ -2,7 +2,6 @@ BOT_TOKEN=YOUR_TELEGRAM_BOT_TOKEN
|
||||
HTTP_PROXY=
|
||||
HTTPS_PROXY=
|
||||
DB_PATH=db.sqlite3
|
||||
SCHEMA_PATH=
|
||||
BOT_WEBHOOK_URL=
|
||||
BOT_LISTEN=:8080
|
||||
BOT_TLS_CERT=
|
||||
|
||||
@@ -19,7 +19,6 @@ RUN apk add --no-cache ca-certificates tzdata
|
||||
WORKDIR /data
|
||||
|
||||
COPY --from=builder /usr/local/bin/worktime-bot /usr/local/bin/worktime-bot
|
||||
COPY db/schema.sql /app/db/schema.sql
|
||||
VOLUME ["/data"]
|
||||
|
||||
CMD ["worktime-bot"]
|
||||
|
||||
29
README.md
29
README.md
@@ -12,18 +12,17 @@ docker compose up -d
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
| ------------------- | -------------------- | ----------------------------------------------------- |
|
||||
| `BOT_TOKEN` | — | Telegram Bot API token **(required)** |
|
||||
| `BOT_ALLOWED_USERS` | (all) | Comma-separated Telegram user IDs (empty = allow all) |
|
||||
| `HTTP_PROXY` | — | Outbound HTTP proxy for Telegram API |
|
||||
| `HTTPS_PROXY` | — | Outbound HTTPS proxy for Telegram API |
|
||||
| `DB_PATH` | `db.sqlite3` | Path to SQLite database file |
|
||||
| `SCHEMA_PATH` | `/app/db/schema.sql` | Path to SQL schema file |
|
||||
| `BOT_WEBHOOK_URL` | — | Set for webhook mode (omit for long-polling) |
|
||||
| `BOT_LISTEN` | `:8080` | Listen address for webhook mode |
|
||||
| `BOT_TLS_CERT` | — | TLS certificate path (webhook HTTPS) |
|
||||
| `BOT_TLS_KEY` | — | TLS key path (webhook HTTPS) |
|
||||
| Variable | Default | Purpose |
|
||||
| ------------------- | ------------ | ----------------------------------------------------- |
|
||||
| `BOT_TOKEN` | — | Telegram Bot API token **(required)** |
|
||||
| `BOT_ALLOWED_USERS` | (all) | Comma-separated Telegram user IDs (empty = allow all) |
|
||||
| `HTTP_PROXY` | — | Outbound HTTP proxy for Telegram API |
|
||||
| `HTTPS_PROXY` | — | Outbound HTTPS proxy for Telegram API |
|
||||
| `DB_PATH` | `db.sqlite3` | Path to SQLite database file |
|
||||
| `BOT_WEBHOOK_URL` | — | Set for webhook mode (omit for long-polling) |
|
||||
| `BOT_LISTEN` | `:8080` | Listen address for webhook mode |
|
||||
| `BOT_TLS_CERT` | — | TLS certificate path (webhook HTTPS) |
|
||||
| `BOT_TLS_KEY` | — | TLS key path (webhook HTTPS) |
|
||||
|
||||
## Calendar Support
|
||||
|
||||
@@ -101,9 +100,9 @@ Four color themes are available in settings: **Ocean**, **Beach**, **Rose**, **C
|
||||
│ │ ├── export.go Excel (.xlsx) report generation
|
||||
│ │ └── dateutil.go Gregorian/Jalali/Hijri conversions & calendars
|
||||
│ └── db/
|
||||
│ └── store.go SQLite store with auto-migration
|
||||
├── db/
|
||||
│ └── schema.sql Database schema
|
||||
│ ├── store.go SQLite store
|
||||
│ └── migrations/ Goose-managed SQL migrations
|
||||
│ └── 001_init.sql Initial schema
|
||||
├── .gitea/workflows/
|
||||
│ └── ci.yml Gitea Actions CI
|
||||
├── Dockerfile
|
||||
|
||||
@@ -12,7 +12,6 @@ services:
|
||||
- HTTP_PROXY=${HTTP_PROXY-}
|
||||
- HTTPS_PROXY=${HTTPS_PROXY-}
|
||||
- DB_PATH=/data/db.sqlite3
|
||||
- SCHEMA_PATH=${SCHEMA_PATH:-/app/db/schema.sql}
|
||||
- TZ=Asia/Tehran
|
||||
volumes:
|
||||
- worktime_data:/data
|
||||
|
||||
13
go.mod
13
go.mod
@@ -12,18 +12,23 @@ require (
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pressly/goose/v3 v3.27.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/richardlehane/mscfb v1.0.6 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.6 // indirect
|
||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.7.2 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
modernc.org/libc v1.73.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@@ -14,16 +14,24 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pressly/goose/v3 v3.27.1 h1:6uEvcprBybDmW4hcz3gYujhARhye+GoWKhEWyzD5sh4=
|
||||
github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76csUV7+7KYoM=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
|
||||
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
|
||||
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||
@@ -34,14 +42,20 @@ github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzx
|
||||
github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -49,6 +63,8 @@ golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
59
internal/db/migrations/001_init.sql
Normal file
59
internal/db/migrations/001_init.sql
Normal file
@@ -0,0 +1,59 @@
|
||||
-- +goose Up
|
||||
CREATE TABLE IF NOT EXISTS work_types (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO work_types (id, name) VALUES (1, 'onsite');
|
||||
INSERT OR IGNORE INTO work_types (id, name) VALUES (2, 'remote');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL UNIQUE,
|
||||
timezone TEXT NOT NULL DEFAULT 'Asia/Tehran',
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
report_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
report_time TEXT NOT NULL DEFAULT '23:00',
|
||||
last_report_date TEXT DEFAULT NULL,
|
||||
export_accent TEXT NOT NULL DEFAULT 'ocean'
|
||||
CHECK (export_accent IN ('ocean', 'beach', 'rose', 'catppuccin')),
|
||||
calendar TEXT NOT NULL DEFAULT 'gregorian'
|
||||
CHECK (calendar IN ('gregorian', 'jalali', 'hijri')),
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS days (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
date TEXT NOT NULL CHECK (date = strftime('%Y-%m-%d', date)),
|
||||
is_day_off INTEGER NOT NULL DEFAULT 0,
|
||||
day_off_reason TEXT NOT NULL DEFAULT '',
|
||||
current_work_type_id INTEGER NOT NULL DEFAULT 1 REFERENCES work_types(id),
|
||||
min_break_threshold INTEGER NOT NULL DEFAULT 300,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
UNIQUE(user_id, date)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
day_id INTEGER NOT NULL REFERENCES days(id),
|
||||
event_type TEXT NOT NULL CHECK (event_type IN ('in', 'out')),
|
||||
work_type_id INTEGER REFERENCES work_types(id),
|
||||
occurred_at INTEGER NOT NULL CHECK (occurred_at > 0),
|
||||
note TEXT NOT NULL DEFAULT '',
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_events_user_day ON events(user_id, day_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_user_occurred ON events(user_id, occurred_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_days_user_date ON days(user_id, date);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE IF EXISTS events;
|
||||
DROP TABLE IF EXISTS days;
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS work_types;
|
||||
@@ -3,23 +3,18 @@ package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const driverName = "sqlite"
|
||||
|
||||
var schemaPath string
|
||||
|
||||
func init() {
|
||||
schemaPath = os.Getenv("SCHEMA_PATH")
|
||||
if schemaPath == "" {
|
||||
schemaPath = "/app/db/schema.sql"
|
||||
}
|
||||
}
|
||||
//go:embed migrations/*.sql
|
||||
var migrationFS embed.FS
|
||||
|
||||
// Store wraps a SQLite database connection and provides methods for all data access.
|
||||
type Store struct {
|
||||
@@ -83,10 +78,7 @@ func NewStore(path string) (*Store, error) {
|
||||
return nil, err
|
||||
}
|
||||
db.Exec("PRAGMA journal_mode=WAL")
|
||||
if err := migrate(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := execSchema(db, schemaPath); err != nil {
|
||||
if err := runMigrations(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Store{db: db}, nil
|
||||
@@ -102,22 +94,49 @@ func (s *Store) DB() *sql.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
func execSchema(db *sql.DB, path string) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(string(data))
|
||||
return err
|
||||
}
|
||||
|
||||
func hasTable(db *sql.DB, name string) bool {
|
||||
var exists int
|
||||
db.QueryRow("SELECT COUNT(1) FROM sqlite_master WHERE type='table' AND name=?", name).Scan(&exists)
|
||||
return exists > 0
|
||||
}
|
||||
|
||||
func hasColumn(db *sql.DB, table, column string) bool {
|
||||
// runMigrations handles all database migrations using goose.
|
||||
func runMigrations(db *sql.DB) error {
|
||||
// Step 1: Handle the ancient single-table schema (pre-goose era).
|
||||
// Rename old tables so goose can create the current schema cleanly.
|
||||
needsOldMigrate := hasTable(db, "events_old") || (hasTable(db, "events") && !hasCol(db, "events", "user_id"))
|
||||
if needsOldMigrate {
|
||||
slog.Info("migrating from old schema to new schema")
|
||||
for _, t := range []string{"events", "days_off", "settings"} {
|
||||
if hasTable(db, t) {
|
||||
if _, err := db.Exec("ALTER TABLE " + t + " RENAME TO " + t + "_old"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Run goose (creates goose_db_version table if needed,
|
||||
// applies pending migrations).
|
||||
goose.SetBaseFS(migrationFS)
|
||||
if err := goose.SetDialect("sqlite3"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := goose.Up(db, "migrations"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Step 3: Copy old data into the freshly created tables.
|
||||
if needsOldMigrate {
|
||||
if err := migrateFromOldSchemaData(db); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasCol(db *sql.DB, table, column string) bool {
|
||||
rows, err := db.Query("PRAGMA table_info(" + table + ")")
|
||||
if err != nil {
|
||||
return false
|
||||
@@ -138,37 +157,6 @@ func hasColumn(db *sql.DB, table, column string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func migrate(db *sql.DB) error {
|
||||
if hasTable(db, "users") {
|
||||
if !hasColumn(db, "users", "calendar") {
|
||||
slog.Info("migrating: adding calendar column to users")
|
||||
db.Exec("ALTER TABLE users ADD COLUMN calendar TEXT NOT NULL DEFAULT 'gregorian' CHECK (calendar IN ('gregorian', 'jalali', 'hijri'))")
|
||||
}
|
||||
}
|
||||
if hasTable(db, "events_old") || hasTable(db, "events") && !hasColumn(db, "events", "user_id") {
|
||||
slog.Info("migrating from old schema to new schema")
|
||||
if err := migrateFromOldSchema(db); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateFromOldSchema(db *sql.DB) error {
|
||||
// Rename old tables so CREATE TABLE IF NOT EXISTS can create new ones
|
||||
for _, t := range []string{"events", "days_off", "settings"} {
|
||||
if hasTable(db, t) {
|
||||
db.Exec("ALTER TABLE " + t + " RENAME TO " + t + "_old")
|
||||
}
|
||||
}
|
||||
|
||||
if err := execSchema(db, schemaPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return migrateFromOldSchemaData(db)
|
||||
}
|
||||
|
||||
func migrateFromOldSchemaData(db *sql.DB) error {
|
||||
migrateUsers(db)
|
||||
migrateDaysOff(db)
|
||||
|
||||
Reference in New Issue
Block a user