Added migration mgmt with goose.

This commit is contained in:
2025-06-04 02:36:09 -05:00
parent 9e59d05efe
commit fa5be206cb
13 changed files with 297 additions and 66 deletions

View File

@ -12,3 +12,14 @@ docker-push:
-t $(IMAGE_REPO):latest \
-t $(IMAGE_REPO):$(DATE_TAG) \
--push .
# Database migration targets
.PHONY: sqlc-generate
sqlc-generate:
cd internal/db && sqlc generate
# Build the application
.PHONY: build
build:
go build -o member-console .

View File

@ -2,28 +2,33 @@
Member console application for users to create, acccess, and manage their accounts associated with the Wiki Cafe MSC (multi-stakeholder co-operative).
## Development notes:
## Database Management
- [ ] Integrate a database migration library. `internal/db/database.go`.
- [ ] Make sure viper's 'env' key will work correctly in production
- [ ] Should session-secret and csrf-secret be generated on startup instead of in the config file? They should be persisted nonetheless. Do they need to be rotated?
- [ ] Add remove trailing slash middleware if we start using more custom handlers that don't end with a slash
- [ ] Add tests
- [ ] CSRF
- [ ] Logging
- [ ] compression
- [ ] recovery
- [ ] request ID
- [ ] timeout
- [ ] secure headers and CORS
- [ ] Auth setup sanity check. Review code.
- [ ] Remove keycloak specific code
- [ ] Implement backchannel logout: When a user logs out of the application, the application should notify the identity provider to log the user out of the identity provider as well.
- [ ] Auth session timeout should match security policy
- [ ] Rate limiting on login attempts
- [ ] Subresource Integrity (SRI) for CDN assets
- [ ] Serve HTMX assets not from CDN
- [ ] Find out if timeout middleware is actually needed or if net/http handles it
This project uses [pressly/goose](https://github.com/pressly/goose) for database migrations and [sqlc](https://github.com/sqlc-dev/sqlc) for type-safe SQL code generation.
### Database Migrations
Migrations are embedded in the binary and run automatically on application startup. The CLI also provides migration management commands (`migrate up`, `migrate down`, `migrate status`).
### Creating New Migrations
```bash
# Install goose CLI tool
go install github.com/pressly/goose/v3/cmd/goose@latest
# Create a new migration
cd internal/db/migrations
goose create your_migration_name sql
```
### sqlc Code Generation
sqlc generates type-safe Go code from SQL queries and migration files. Database models and query methods are automatically generated from the migration schema and SQL files in `internal/db/queries/`.
```bash
# Regenerate sqlc code after schema or query changes
cd internal/db && sqlc generate
```
## Building and publishing container image
@ -68,4 +73,26 @@ Example output:
e157b42a5b608882179cb4ac69c12f84
```
Ensure these secrets are securely stored and persisted for application use.
Ensure these secrets are securely stored and persisted for application use.
## Development notes:
- [ ] Make sure viper's 'env' key will work correctly in production
- [ ] Should session-secret and csrf-secret be generated on startup instead of in the config file? They should be persisted nonetheless. Do they need to be rotated?
- [ ] Add remove trailing slash middleware if we start using more custom handlers that don't end with a slash
- [ ] Add tests
- [ ] CSRF
- [ ] Logging
- [ ] compression
- [ ] recovery
- [ ] request ID
- [ ] timeout
- [ ] secure headers and CORS
- [ ] Auth setup sanity check. Review code.
- [ ] Remove keycloak specific code
- [ ] Implement backchannel logout: When a user logs out of the application, the application should notify the identity provider to log the user out of the identity provider as well.
- [ ] Auth session timeout should match security policy
- [ ] Rate limiting on login attempts
- [ ] Subresource Integrity (SRI) for CDN assets
- [ ] Serve HTMX assets not from CDN
- [ ] Find out if timeout middleware is actually needed or if net/http handles it

110
cmd/migrate.go Normal file
View File

@ -0,0 +1,110 @@
package cmd
import (
"context"
"log/slog"
"git.coopcloud.tech/wiki-cafe/member-console/internal/db"
"git.coopcloud.tech/wiki-cafe/member-console/internal/logging"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var migrateCmd = &cobra.Command{
Use: "migrate",
Short: "Database migration commands",
Long: "Run database migrations up, down, or check status",
}
var migrateUpCmd = &cobra.Command{
Use: "up",
Short: "Run all pending migrations",
Long: "Apply all pending database migrations",
RunE: func(cmd *cobra.Command, args []string) error {
logger := slog.Default()
if logging.AppLogger != nil {
logger = logging.AppLogger
}
dbDSN := viper.GetString("db-dsn")
config := db.DefaultDBConfig(dbDSN)
database, err := db.Connect(context.Background(), logger, config)
if err != nil {
logger.Error("failed to connect to database", slog.Any("error", err))
return err
}
defer database.Close()
if err := db.RunMigrations(database); err != nil {
logger.Error("failed to run migrations", slog.Any("error", err))
return err
}
logger.Info("migrations completed successfully")
return nil
},
}
var migrateDownCmd = &cobra.Command{
Use: "down",
Short: "Rollback the last migration",
Long: "Rollback the most recently applied migration",
RunE: func(cmd *cobra.Command, args []string) error {
logger := slog.Default()
if logging.AppLogger != nil {
logger = logging.AppLogger
}
dbDSN := viper.GetString("db-dsn")
config := db.DefaultDBConfig(dbDSN)
database, err := db.Connect(context.Background(), logger, config)
if err != nil {
logger.Error("failed to connect to database", slog.Any("error", err))
return err
}
defer database.Close()
if err := db.RollbackMigration(database); err != nil {
logger.Error("failed to rollback migration", slog.Any("error", err))
return err
}
logger.Info("migration rollback completed successfully")
return nil
},
}
var migrateStatusCmd = &cobra.Command{
Use: "status",
Short: "Show migration status",
Long: "Display the status of all migrations",
RunE: func(cmd *cobra.Command, args []string) error {
logger := slog.Default()
if logging.AppLogger != nil {
logger = logging.AppLogger
}
dbDSN := viper.GetString("db-dsn")
config := db.DefaultDBConfig(dbDSN)
database, err := db.Connect(context.Background(), logger, config)
if err != nil {
logger.Error("failed to connect to database", slog.Any("error", err))
return err
}
defer database.Close()
if err := db.MigrationStatus(database); err != nil {
logger.Error("failed to get migration status", slog.Any("error", err))
return err
}
return nil
},
}
func init() {
migrateCmd.AddCommand(migrateUpCmd)
migrateCmd.AddCommand(migrateDownCmd)
migrateCmd.AddCommand(migrateStatusCmd)
rootCmd.AddCommand(migrateCmd)
}

View File

@ -35,7 +35,7 @@ var startCmd = &cobra.Command{
// Database Setup
dbDSN := viper.GetString("db-dsn")
dbConfig := db.DefaultDBConfig(dbDSN)
database, err := db.NewDB(ctx, logger, dbConfig)
database, err := db.ConnectAndMigrate(ctx, logger, dbConfig)
if err != nil {
logger.Error("failed to initialize database", slog.Any("error", err))
os.Exit(1)

18
go.mod
View File

@ -12,8 +12,12 @@ require (
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/klauspost/compress v1.17.2 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/pressly/goose/v3 v3.24.3 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
golang.org/x/sync v0.14.0 // indirect
)
require (
@ -36,12 +40,12 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/oauth2 v0.26.0
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

26
go.sum
View File

@ -2,6 +2,8 @@ github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWG
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/coreos/go-oidc/v3 v3.12.0 h1:sJk+8G2qq94rDI6ehZ71Bol3oUHy63qNYmkiSjrc/Jo=
github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@ -36,6 +38,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@ -45,17 +49,23 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM=
github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -63,6 +73,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
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/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
@ -85,25 +97,39 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -3,7 +3,6 @@ package db
import (
"context"
"database/sql"
_ "embed"
"errors"
"fmt"
"log/slog"
@ -14,9 +13,6 @@ import (
"github.com/mattn/go-sqlite3"
)
//go:embed schema.sql
var ddl string
// DBConfig holds database configuration.
type DBConfig struct {
DSN string // Data Source Name for SQLite
@ -126,8 +122,9 @@ func openAndConfigureDB(config *DBConfig) (*sql.DB, error) {
return db, nil
}
// NewDB initializes and returns a new database connection pool and runs migrations.
func NewDB(ctx context.Context, logger *slog.Logger, config *DBConfig) (*sql.DB, error) {
// Connect initializes and returns a new database connection pool.
// This is the basic connection function without any automatic operations.
func Connect(ctx context.Context, logger *slog.Logger, config *DBConfig) (*sql.DB, error) {
db, err := openAndConfigureDB(config)
if err != nil {
return nil, err
@ -147,16 +144,26 @@ func NewDB(ctx context.Context, logger *slog.Logger, config *DBConfig) (*sql.DB,
slog.Int("max_open_conns", config.MaxOpenConns),
slog.Int("max_idle_conns", config.MaxIdleConns))
// Execute schema with retry logic
err = retryOperation(ctx, logger, config, "schema execution", func() error {
_, err := db.ExecContext(ctx, ddl)
return err
return db, nil
}
// ConnectAndMigrate initializes a database connection and automatically runs migrations.
// This is the main function for application startup.
func ConnectAndMigrate(ctx context.Context, logger *slog.Logger, config *DBConfig) (*sql.DB, error) {
db, err := Connect(ctx, logger, config)
if err != nil {
return nil, err
}
// Run migrations with retry logic
err = retryOperation(ctx, logger, config, "migrations", func() error {
return RunMigrations(db)
})
if err != nil {
db.Close()
return nil, err
}
logger.Info("database schema applied")
logger.Info("database migrations applied")
return db, nil
}

56
internal/db/migrations.go Normal file
View File

@ -0,0 +1,56 @@
package db
import (
"database/sql"
"embed"
"github.com/pressly/goose/v3"
)
//go:embed migrations/*.sql
var embedMigrations embed.FS
// RunMigrations runs all pending migrations
func RunMigrations(db *sql.DB) error {
goose.SetBaseFS(embedMigrations)
if err := goose.SetDialect("sqlite3"); err != nil {
return err
}
if err := goose.Up(db, "migrations"); err != nil {
return err
}
return nil
}
// RollbackMigration rolls back the last migration
func RollbackMigration(db *sql.DB) error {
goose.SetBaseFS(embedMigrations)
if err := goose.SetDialect("sqlite3"); err != nil {
return err
}
if err := goose.Down(db, "migrations"); err != nil {
return err
}
return nil
}
// MigrationStatus shows the status of all migrations
func MigrationStatus(db *sql.DB) error {
goose.SetBaseFS(embedMigrations)
if err := goose.SetDialect("sqlite3"); err != nil {
return err
}
if err := goose.Status(db, "migrations"); err != nil {
return err
}
return nil
}

View File

@ -1,5 +1,4 @@
-- internal/db/schema.sql
-- +goose Up
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
oidc_subject TEXT UNIQUE NOT NULL,
@ -31,14 +30,12 @@ CREATE TABLE payments (
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Create indexes for foreign keys and frequently queried columns
CREATE INDEX idx_sites_user_id ON sites(user_id);
CREATE INDEX idx_payments_user_id ON payments(user_id);
CREATE INDEX idx_users_oidc_subject ON users(oidc_subject);
CREATE INDEX idx_sites_domain ON sites(domain);
CREATE INDEX idx_payments_payment_processor_id ON payments(payment_processor_id);
-- Triggers to update 'updated_at' timestamps
CREATE TRIGGER trigger_users_updated_at
AFTER UPDATE ON users
FOR EACH ROW
@ -59,3 +56,16 @@ FOR EACH ROW
BEGIN
UPDATE payments SET updated_at = CURRENT_TIMESTAMP WHERE id = OLD.id;
END;
-- +goose Down
DROP TRIGGER IF EXISTS trigger_payments_updated_at;
DROP TRIGGER IF EXISTS trigger_sites_updated_at;
DROP TRIGGER IF EXISTS trigger_users_updated_at;
DROP INDEX IF EXISTS idx_payments_payment_processor_id;
DROP INDEX IF EXISTS idx_sites_domain;
DROP INDEX IF EXISTS idx_users_oidc_subject;
DROP INDEX IF EXISTS idx_payments_user_id;
DROP INDEX IF EXISTS idx_sites_user_id;
DROP TABLE IF EXISTS payments;
DROP TABLE IF EXISTS sites;
DROP TABLE IF EXISTS users;

View File

@ -12,12 +12,6 @@ type Querier interface {
CreatePayment(ctx context.Context, arg CreatePaymentParams) (Payment, error)
CreateSite(ctx context.Context, arg CreateSiteParams) (Site, error)
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
// THIS IS NOT USED YET
// -- name: UpdateSiteDomain :one
// UPDATE sites
// SET domain = ?
// WHERE id = ?
// RETURNING *;
DeleteSite(ctx context.Context, id int64) error
DeleteUser(ctx context.Context, id int64) error
GetPaymentByID(ctx context.Context, id int64) (Payment, error)

View File

@ -16,13 +16,6 @@ ORDER BY created_at DESC;
SELECT * FROM sites
WHERE domain = ?;
-- THIS IS NOT USED YET
-- -- name: UpdateSiteDomain :one
-- UPDATE sites
-- SET domain = ?
-- WHERE id = ?
-- RETURNING *;
-- name: DeleteSite :exec
DELETE FROM sites
WHERE id = ?;

View File

@ -34,17 +34,10 @@ func (q *Queries) CreateSite(ctx context.Context, arg CreateSiteParams) (Site, e
}
const deleteSite = `-- name: DeleteSite :exec
DELETE FROM sites
WHERE id = ?
`
// THIS IS NOT USED YET
// -- name: UpdateSiteDomain :one
// UPDATE sites
// SET domain = ?
// WHERE id = ?
// RETURNING *;
func (q *Queries) DeleteSite(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteSite, id)
return err

View File

@ -2,7 +2,7 @@
version: "2"
sql:
- engine: "sqlite"
schema: "schema.sql"
schema: "migrations/"
queries: "queries/"
gen:
go: