Refactor db code. Add migration support

This commit is contained in:
Jan Eitzinger 2025-05-25 20:27:55 +02:00
parent dacdd3b826
commit 706744d657
9 changed files with 300 additions and 268 deletions

25
go.mod
View File

@ -3,24 +3,25 @@ module git.clustercockpit.org/moebiusband/go-http-skeleton
go 1.24
require (
golang.org/x/sync v0.7.0
google.golang.org/protobuf v1.34.2
zombiezen.com/go/sqlite v1.3.0
github.com/golang-migrate/migrate/v4 v4.18.3
github.com/joho/godotenv v1.5.1
golang.org/x/sync v0.14.0
modernc.org/sqlite v1.37.1
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/tools v0.22.0 // indirect
modernc.org/libc v1.54.2 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.30.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/sys v0.33.0 // indirect
modernc.org/libc v1.65.7 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)

95
go.sum
View File

@ -1,51 +1,70 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs=
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
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-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.0 h1:f9K5VdC0nVhHKTFMvhjtZ8TbRgFQbASvE5yO1zs8eC0=
modernc.org/ccgo/v4 v4.19.0/go.mod h1:CfpAl+673iXNwMG/aqcQn+vDcu4Es/YLya7+9RHjTa4=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.54.2 h1:9ymAodb+3v85YfBIZqn62BGgO4L9zF2Hx4LNb6dSU/Q=
modernc.org/libc v1.54.2/go.mod h1:B0D6klDmSmnq26T1iocn9kzyX6NtbzjuI3+oX/xfvng=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk=
modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
zombiezen.com/go/sqlite v1.3.0 h1:98g1gnCm+CNz6AuQHu0gqyw7gR2WU3O3PJufDOStpUs=
zombiezen.com/go/sqlite v1.3.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY=

View File

@ -1,231 +1,41 @@
package repository
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitemigration"
"zombiezen.com/go/sqlite/sqlitex"
"database/sql"
"log/slog"
"sync"
)
type Database struct {
filename string
migrations []string
writePool *sqlitex.Pool
readPool *sqlitex.Pool
}
type TxFn func(tx *sqlite.Conn) error
func NewDatabase(ctx context.Context, dbFilename string, migrations []string) (*Database, error) {
if dbFilename == "" {
return nil, fmt.Errorf("database filename is required")
}
db := &Database{
filename: dbFilename,
migrations: migrations,
}
if err := db.Reset(ctx, false); err != nil {
return nil, fmt.Errorf("failed to reset database: %w", err)
}
return db, nil
}
func (db *Database) WriteWithoutTx(ctx context.Context, fn TxFn) error {
conn, err := db.writePool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to take write connection: %w", err)
}
if conn == nil {
return fmt.Errorf("could not get write connection from pool")
}
defer db.writePool.Put(conn)
if err := fn(conn); err != nil {
return fmt.Errorf("could not execute write transaction: %w", err)
}
return nil
}
func (db *Database) Reset(ctx context.Context, shouldClear bool) (err error) {
if err := db.Close(); err != nil {
return fmt.Errorf("could not close database: %w", err)
}
if shouldClear {
if err := os.RemoveAll(db.filename + "*"); err != nil {
return fmt.Errorf("could not remove database file: %w", err)
}
}
if err := os.MkdirAll(filepath.Dir(db.filename), 0755); err != nil {
return fmt.Errorf("could not create database directory: %w", err)
}
uri := fmt.Sprintf("file:%s?_journal_mode=WAL&_synchronous=NORMAL", db.filename)
db.writePool, err = sqlitex.NewPool(uri, sqlitex.PoolOptions{
PoolSize: 1,
})
if err != nil {
return fmt.Errorf("could not open write pool: %w", err)
}
db.readPool, err = sqlitex.NewPool(uri, sqlitex.PoolOptions{
PoolSize: runtime.NumCPU(),
})
if err := db.WriteTX(ctx, func(tx *sqlite.Conn) error {
foreignKeysStmt := tx.Prep("PRAGMA foreign_keys = ON")
defer foreignKeysStmt.Finalize()
if _, err := foreignKeysStmt.Step(); err != nil {
return fmt.Errorf("failed to enable foreign keys: %w", err)
}
return nil
}); err != nil {
return fmt.Errorf("failed to initialize database: %w", err)
}
schema := sqlitemigration.Schema{Migrations: db.migrations}
conn, err := db.writePool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to take write connection: %w", err)
}
defer db.writePool.Put(conn)
if err := sqlitemigration.Migrate(ctx, conn, schema); err != nil {
db.writePool.Put(conn)
return fmt.Errorf("failed to migrate database: %w", err)
}
return nil
}
func (db *Database) Close() error {
errs := []error{}
if db.writePool != nil {
errs = append(errs, db.writePool.Close())
}
if db.readPool != nil {
errs = append(errs, db.readPool.Close())
}
return errors.Join(errs...)
}
func (db *Database) WriteTX(ctx context.Context, fn TxFn) (err error) {
conn, err := db.writePool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to take write connection: %w", err)
}
if conn == nil {
return fmt.Errorf("could not get write connection from pool")
}
defer db.writePool.Put(conn)
endFn, err := sqlitex.ImmediateTransaction(conn)
if err != nil {
return fmt.Errorf("could not start transaction: %w", err)
}
defer endFn(&err)
if err := fn(conn); err != nil {
return fmt.Errorf("could not execute write transaction: %w", err)
}
return nil
}
func (db *Database) ReadTX(ctx context.Context, fn TxFn) (err error) {
conn, err := db.readPool.Take(ctx)
if err != nil {
return fmt.Errorf("failed to take read connection: %w", err)
}
if conn == nil {
return fmt.Errorf("could not get read connection from pool")
}
defer db.readPool.Put(conn)
endFn := sqlitex.Transaction(conn)
defer endFn(&err)
if err := fn(conn); err != nil {
return fmt.Errorf("could not execute read transaction: %w", err)
}
return nil
}
const (
secondsInADay = 86400
UnixEpochJulianDay = 2440587.5
var (
dbConnOnce sync.Once
dbConn *sql.DB
repo *Queries
)
var JulianZeroTime = JulianDayToTime(0)
func Connect(dsnURI string) {
dbConnOnce.Do(func() {
var err error
dbConn, err = sql.Open("sqlite", dsnURI)
if err != nil {
slog.Error("Fatal error")
}
// TimeToJulianDay converts a time.Time into a Julian day.
func TimeToJulianDay(t time.Time) float64 {
return float64(t.UTC().Unix())/secondsInADay + UnixEpochJulianDay
repo = New(dbConn)
})
}
// JulianDayToTime converts a Julian day into a time.Time.
func JulianDayToTime(d float64) time.Time {
return time.Unix(int64((d-UnixEpochJulianDay)*secondsInADay), 0).UTC()
}
func JulianNow() float64 {
return TimeToJulianDay(time.Now())
}
func TimestampJulian(ts *timestamppb.Timestamp) float64 {
return TimeToJulianDay(ts.AsTime())
}
func JulianDayToTimestamp(f float64) *timestamppb.Timestamp {
t := JulianDayToTime(f)
return timestamppb.New(t)
}
func StmtJulianToTimestamp(stmt *sqlite.Stmt, colName string) *timestamppb.Timestamp {
julianDays := stmt.GetFloat(colName)
return JulianDayToTimestamp(julianDays)
}
func StmtJulianToTime(stmt *sqlite.Stmt, colName string) time.Time {
julianDays := stmt.GetFloat(colName)
return JulianDayToTime(julianDays)
}
func DurationToMilliseconds(d time.Duration) int64 {
return int64(d / time.Millisecond)
}
func MillisecondsToDuration(ms int64) time.Duration {
return time.Duration(ms) * time.Millisecond
}
func StmtBytes(stmt *sqlite.Stmt, colName string) []byte {
bl := stmt.GetLen(colName)
if bl == 0 {
return nil
func GetConnection() (*sql.DB, error) {
if dbConn == nil {
slog.Error("Database connection not initialized!")
}
buf := make([]byte, bl)
if writtent := stmt.GetBytes(colName, buf); writtent != bl {
return nil
return dbConn, nil
}
func GetRepository() (*Queries, error) {
if repo == nil {
slog.Error("Database connection not initialized!")
}
return buf
return repo, nil
}

View File

@ -0,0 +1,137 @@
package repository
import (
"database/sql"
"embed"
"fmt"
"log/slog"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source/iofs"
)
const Version uint = 1
//go:embed migrations/*
var migrationFiles embed.FS
func checkDBVersion(db *sql.DB) error {
var m *migrate.Migrate
driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
if err != nil {
return err
}
d, err := iofs.New(migrationFiles, "migrations")
if err != nil {
return err
}
m, err = migrate.NewWithInstance("iofs", d, "sqlite3", driver)
if err != nil {
return err
}
v, dirty, err := m.Version()
if err != nil {
if err == migrate.ErrNilVersion {
slog.Warn("Legacy database without version or missing database file!")
} else {
return err
}
}
if v < Version {
return fmt.Errorf("unsupported database version %d, need %d.\nPlease backup your database file and run cc-backend -migrate-db", v, Version)
} else if v > Version {
return fmt.Errorf("unsupported database version %d, need %d.\nPlease refer to documentation how to downgrade db with external migrate tool", v, Version)
}
if dirty {
return fmt.Errorf("last migration to version %d has failed, please fix the db manually and force version with -force-db flag", Version)
}
return nil
}
func getMigrateInstance(dsnURI string) (m *migrate.Migrate, err error) {
d, err := iofs.New(migrationFiles, "migrations")
if err != nil {
slog.Error("failed to get instance", "Error", err)
}
m, err = migrate.NewWithSourceInstance("iofs", d, dsnURI)
if err != nil {
return m, err
}
return m, nil
}
func MigrateDB(db string) error {
m, err := getMigrateInstance(db)
if err != nil {
return err
}
v, dirty, err := m.Version()
if err != nil {
if err == migrate.ErrNilVersion {
slog.Warn("Legacy database without version or missing database file!")
} else {
return err
}
}
if v < Version {
slog.Info("unsupported database version %d, need %d.\nPlease backup your database file and run cc-backend -migrate-db", v, Version)
}
if dirty {
return fmt.Errorf("last migration to version %d has failed, please fix the db manually and force version with -force-db flag", Version)
}
if err := m.Up(); err != nil {
if err == migrate.ErrNoChange {
slog.Info("DB already up to date!")
} else {
return err
}
}
m.Close()
return nil
}
func RevertDB(db string) error {
m, err := getMigrateInstance(db)
if err != nil {
return err
}
if err := m.Migrate(Version - 1); err != nil {
if err == migrate.ErrNoChange {
slog.Info("DB already up to date!")
} else {
return err
}
}
m.Close()
return nil
}
func ForceDB(db string) error {
m, err := getMigrateInstance(db)
if err != nil {
return err
}
if err := m.Force(int(Version)); err != nil {
return err
}
m.Close()
return nil
}

View File

@ -0,0 +1,11 @@
CREATE TABLE news (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
bio TEXT
);
CREATE TABLE retailer (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
bio TEXT
);

50
main.go
View File

@ -2,6 +2,7 @@ package main
import (
"embed"
"flag"
"fmt"
"io/fs"
"log/slog"
@ -11,6 +12,10 @@ import (
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/handlers"
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/middleware"
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
"github.com/joho/godotenv"
_ "modernc.org/sqlite"
)
//go:embed web/static/*
@ -42,6 +47,51 @@ func init() {
}
func main() {
var flagMigrateDB, flagRevertDB, flagForceDB bool
flag.BoolVar(&flagMigrateDB, "migrate-db", false, "Migrate database to supported version and exit")
flag.BoolVar(&flagRevertDB, "revert-db", false, "Migrate database to previous version and exit")
flag.BoolVar(&flagForceDB, "force-db", false, "Force database version, clear dirty flag and exit")
err := godotenv.Load()
if err != nil {
slog.Error("Could not parse existing .env file at location './.env'. Application startup failed, exited.\nError: %s\n", "Error", err.Error())
}
dbURL := os.Getenv("DB")
if dbURL == "" {
dbURL = ""
}
if flagMigrateDB {
err := repository.MigrateDB(dbURL)
if err != nil {
slog.Error("MigrateDB Failed: Could not migrate database at location.", "version", repository.Version, "error", err)
os.Exit(1)
}
slog.Info("MigrateDB Success: Migrated database at location.\n", "version", repository.Version)
}
if flagRevertDB {
err := repository.RevertDB(dbURL)
if err != nil {
slog.Error("RevertDB Failed: Could not revert database at location", "version", (repository.Version - 1), "error", err)
os.Exit(1)
}
slog.Info("RevertDB Success: Reverted database", "version", (repository.Version - 1))
}
if flagForceDB {
err := repository.ForceDB(dbURL)
if err != nil {
slog.Error("ForceDB Failed: Could not force database version", "version", repository.Version, "error", err)
os.Exit(1)
}
slog.Error("ForceDB Success: Forced database version", "version", repository.Version)
}
repository.Connect(dbURL)
port := os.Getenv("PORT")
if port == "" {
port = "8080"

View File

@ -1,9 +1,13 @@
version: "2"
sql:
- engine: "sqlite"
queries: "db/query.sql"
schema: "db/schema.sql"
queries: "internal/repository/sql/"
schema: "internal/repository/migrations/"
gen:
go:
package: "repository"
out: "internal/repository"
emit_db_tags: true
emit_json_tags: true
json_tags_case_style: "camel"
emit_pointers_for_null_types: true