Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Configuration for PostgreSQL connection and pooling.
| `STATICREG_DB_PASSWORD` | string | *(optional)* | PostgreSQL password. Alternative to `STATICREG_DB_URL`. |
| `STATICREG_DB_NAME` | string | *(required)* | PostgreSQL database name. Alternative to `STATICREG_DB_URL`. Required if using individual variables. |
| `STATICREG_DB_SSLMODE` | string | *(optional)* | PostgreSQL SSL mode. Options: `disable`, `require`, `verify-ca`, `verify-full`. Alternative to `STATICREG_DB_URL`. |
| `STATICREG_DB_SCHEMA` | string | `staticreg` | PostgreSQL schema where StaticReg owns its tables. The schema is created on startup if missing and is set as the connection `search_path`. Must match `^[a-z_][a-z0-9_]*$` (lowercase letters, digits, underscores; max 63 chars). Use a per-deployment value (e.g. `staticreg_prod`) when multiple StaticReg instances share a database. |


### Registry Configuration
Expand Down Expand Up @@ -105,6 +106,7 @@ export STATICREG_DB_USER="staticreg"
export STATICREG_DB_PASSWORD="password"
export STATICREG_DB_NAME="staticreg"
export STATICREG_DB_SSLMODE="require"
export STATICREG_DB_SCHEMA="staticreg"

# Webhook Batching
export STATICREG_METRICS_BATCH_SIZE=200
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/gin-gonic/gin v1.10.1
github.com/google/go-containerregistry v0.20.4
github.com/jackc/pgx/v5 v5.7.6
github.com/pressly/goose/v3 v3.24.3
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/samber/slog-gin v1.15.1
github.com/spf13/cobra v1.9.1
Expand Down Expand Up @@ -45,6 +46,7 @@ require (
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
Expand All @@ -53,13 +55,15 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/vbatts/tar-split v0.12.1 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.17.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
Expand Down
27 changes: 26 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
Expand Down Expand Up @@ -120,6 +122,8 @@ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjS
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
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/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/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand All @@ -128,6 +132,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
Expand All @@ -154,13 +160,20 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
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/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
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/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/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/slog-gin v1.15.1 h1:jsnfr+S5HQPlz9pFPA3tOmKW7wN/znyZiE6hncucrTM=
github.com/samber/slog-gin v1.15.1/go.mod h1:mPAEinK/g2jPLauuWO11m3Q0Ca7aG4k9XjXjXY8IhMQ=
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
Expand Down Expand Up @@ -194,13 +207,17 @@ go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKr
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
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/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
Expand Down Expand Up @@ -289,4 +306,12 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y=
modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs=
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.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
6 changes: 6 additions & 0 deletions manifests/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ spec:
name: staticreg-db-credentials
key: STATICREG_DB_SSLMODE
optional: true
- name: STATICREG_DB_SCHEMA
valueFrom:
configMapKeyRef:
name: staticreg-config
key: STATICREG_DB_SCHEMA
optional: true

# Webhook Batching Configuration
- name: STATICREG_METRICS_BATCH_SIZE
Expand Down
25 changes: 25 additions & 0 deletions pkg/db/dbtest/dbtest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Seqera

//go:build integration

// Package dbtest provides integration-test helpers shared across packages
// that exercise a real postgres connection. Compiled only with the
// `integration` build tag so it does not enter normal builds.
package dbtest

import "testing"

// SetEnv populates the STATICREG_DB_* environment variables to point at the
// docker-compose postgres container with the given dedicated schema. All
// values are restored automatically by t.Setenv on cleanup.
func SetEnv(t *testing.T, schema string) {
t.Helper()
t.Setenv("STATICREG_DB_HOST", "127.0.0.1")
t.Setenv("STATICREG_DB_PORT", "5432")
t.Setenv("STATICREG_DB_USER", "staticreg")
t.Setenv("STATICREG_DB_PASSWORD", "password")
t.Setenv("STATICREG_DB_NAME", "staticreg")
t.Setenv("STATICREG_DB_SSLMODE", "disable")
t.Setenv("STATICREG_DB_SCHEMA", schema)
}
134 changes: 77 additions & 57 deletions pkg/db/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,38 @@ import (
"fmt"
"log/slog"
"os"
"regexp"
"time"

"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
"github.com/seqeralabs/staticreg/pkg/observability/logger"
schemasql "github.com/seqeralabs/staticreg/pkg/sql"
)

// defaultSchema is the postgres schema used when STATICREG_DB_SCHEMA is unset.
const defaultSchema = "staticreg"

// schemaIdentRegexp validates that a schema name is a safe postgres identifier.
// Only lowercase letters, digits, and underscores; must not start with a digit.
// Identifier is interpolated into DDL (CREATE SCHEMA, search_path) since pgx
// parameters cannot bind identifiers — strict validation prevents injection.
var schemaIdentRegexp = regexp.MustCompile(`^[a-z_][a-z0-9_]*$`)

// resolveSchema returns the validated schema name from STATICREG_DB_SCHEMA or
// the default. Returns an error if the env var is set but invalid.
func resolveSchema() (string, error) {
schema := os.Getenv("STATICREG_DB_SCHEMA")
if schema == "" {
return defaultSchema, nil
}
if len(schema) > 63 || !schemaIdentRegexp.MatchString(schema) {
return "", fmt.Errorf("invalid STATICREG_DB_SCHEMA %q: must match %s and be <=63 chars", schema, schemaIdentRegexp)
}
return schema, nil
}

// buildConnectionString constructs a PostgreSQL connection string from environment variables.
// It prioritizes STATICREG_DB_URL if set, otherwise builds the connection string from individual
// STATICREG_DB_* environment variables. Returns empty string if no configuration is provided.
Expand Down Expand Up @@ -62,94 +87,89 @@ func InitPool() *pgxpool.Pool {
return nil
}

schema, err := resolveSchema()
if err != nil {
slog.Warn("Invalid database schema configuration. Database functions will be disabled.", logger.ErrAttr(err))
return nil
}

config, err := pgxpool.ParseConfig(connStr)
if err != nil {
slog.Warn("Unable to parse database configuration: %v. Database functions will be disabled.", logger.ErrAttr(err))
slog.Warn("Unable to parse database configuration. Database functions will be disabled.", logger.ErrAttr(err))
return nil
}

// Pin every pooled connection to the dedicated schema so unqualified
// identifiers in queries (and goose's bookkeeping table) resolve there.
if config.ConnConfig.RuntimeParams == nil {
config.ConnConfig.RuntimeParams = map[string]string{}
}
config.ConnConfig.RuntimeParams["search_path"] = schema

// Configure connection pool settings
config.MaxConns = 25

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Short timeout for pool construction + initial ping. A separate, longer
// budget is used for migrations below — running goose under the same
// 10s deadline can produce spurious "context deadline exceeded" failures
// at startup even when the database is healthy.
poolCtx, poolCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer poolCancel()

// Attempt to create the connection pool
pool, err := pgxpool.NewWithConfig(ctx, config)
pool, err := pgxpool.NewWithConfig(poolCtx, config)
if err != nil {
slog.Warn("Unable to create connection pool: %v. Database functions will be disabled.", logger.ErrAttr(err))
slog.Warn("Unable to create connection pool. Database functions will be disabled.", logger.ErrAttr(err))
return nil
}

// Attempt to ping the database
if err = pool.Ping(ctx); err != nil {
slog.Warn("Database connection failed to ping: %v. Database functions will be disabled.", logger.ErrAttr(err))
// Close the pool if ping failed
if err = pool.Ping(poolCtx); err != nil {
slog.Warn("Database connection failed to ping. Database functions will be disabled.", logger.ErrAttr(err))
pool.Close()
return nil
}

// Success
slog.Info("PostgreSQL connection pool successfully initialized.")
slog.Info("PostgreSQL connection pool successfully initialized.", slog.String("schema", schema))

// Initialize database schema
if err := initSchema(ctx, pool); err != nil {
slog.Error("Failed to initialize database schema: %v. Exiting application.", logger.ErrAttr(err))
migrationCtx, migrationCancel := context.WithTimeout(context.Background(), time.Minute)
defer migrationCancel()
if err := initSchema(migrationCtx, pool, schema); err != nil {
slog.Error("Failed to initialize database schema. Exiting application.", logger.ErrAttr(err))
pool.Close()
os.Exit(1)
}

return pool
}

// initSchema creates the database schema if it doesn't exist
func initSchema(ctx context.Context, pool *pgxpool.Pool) error {
// initSchema ensures the dedicated schema exists and runs all pending migrations.
// Schema name has already been validated by resolveSchema before this is called.
func initSchema(ctx context.Context, pool *pgxpool.Pool, schema string) error {
if pool == nil {
return nil
}

slog.Info("Initializing database schema...")
// Schema name was validated against schemaIdentRegexp, so direct interpolation is safe.
// Goose's bookkeeping table (goose_db_version) lands inside this schema because the
// pool's search_path was set above, before the first connection was acquired.
if _, err := pool.Exec(ctx, fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", schema)); err != nil {
return fmt.Errorf("failed to create schema %q: %w", schema, err)
}

// Execute the SQL schema directly (includes DROP TABLE IF EXISTS which is safe)
_, err := pool.Exec(ctx, schemasql.EventSchemaSQL)
if err != nil {
return fmt.Errorf("failed to execute schema: %w", err)
// stdlib.OpenDBFromPool returns a *sql.DB that wraps the pool; closing it
// does not close the pool (per pgx docs).
db := stdlib.OpenDBFromPool(pool)
defer db.Close()

goose.SetBaseFS(schemasql.Migrations)
if err := goose.SetDialect("postgres"); err != nil {
return fmt.Errorf("failed to set goose dialect: %w", err)
}
goose.SetLogger(goose.NopLogger())

// Validate that the table was created successfully
var tableExists bool
err = pool.QueryRow(ctx, `
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'container_pull_metrics'
)`).Scan(&tableExists)
if err != nil {
return fmt.Errorf("failed to verify table creation: %w", err)
}
if !tableExists {
return fmt.Errorf("table container_pull_metrics was not created")
}

// Validate that indexes were created successfully
expectedIndexes := []string{"idx_pull_date", "idx_repo_date", "idx_repo_arch_date"}
for _, indexName := range expectedIndexes {
var indexExists bool
err = pool.QueryRow(ctx, `
SELECT EXISTS (
SELECT FROM pg_indexes
WHERE schemaname = 'public'
AND tablename = 'container_pull_metrics'
AND indexname = $1
)`, indexName).Scan(&indexExists)
if err != nil {
return fmt.Errorf("failed to verify index %s: %w", indexName, err)
}
if !indexExists {
return fmt.Errorf("index %s was not created", indexName)
}
}

slog.Info("Database schema initialized successfully.")
if err := goose.UpContext(ctx, db, "migrations"); err != nil {
return fmt.Errorf("failed to apply migrations: %w", err)
}

slog.Info("Database schema ready.", slog.String("schema", schema))
return nil
}
Loading
Loading