diff --git a/database/database_test.go b/database/database_test.go index b97707d8..4790c8e9 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -5,7 +5,6 @@ package database import ( "context" - "strings" "testing" "time" @@ -124,24 +123,6 @@ func TestHealthCheck_ClosedDB(t *testing.T) { } } -// TestNew_DriverNotImported verifies a missing driver import surfaces as an -// error rather than a panic. The MySQL driver is not imported by this test -// binary. -func TestNew_DriverNotImported(t *testing.T) { - cfg := Config{ - Driver: MySQL, - MySQL: &MySQLConfig{Host: "127.0.0.1", User: "user", Password: "password", Name: "testdb"}, - } - - _, err := New(context.Background(), cfg) - if err == nil { - t.Fatal("expected an error for an unregistered driver, got nil") - } - if !strings.Contains(err.Error(), `unknown driver "mysql"`) { - t.Errorf("got %q, want it to mention the unknown driver", err.Error()) - } -} - func TestPoolConfig_apply(t *testing.T) { db, err := New(context.Background(), Config{Driver: SQLite, SQLite: &SQLiteConfig{Path: ":memory:"}}) if err != nil { diff --git a/database/go.mod b/database/go.mod index 93795036..49c4d37b 100644 --- a/database/go.mod +++ b/database/go.mod @@ -2,15 +2,23 @@ module github.com/OpenNSW/core/database go 1.26.0 -require modernc.org/sqlite v1.60.1 +require ( + github.com/go-sql-driver/mysql v1.10.1 + github.com/jackc/pgx/v5 v5.10.0 + modernc.org/sqlite v1.60.1 +) require ( + filippo.io/edwards25519 v1.2.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.29.0 // indirect modernc.org/libc v1.77.1 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.12.1 // indirect diff --git a/database/go.sum b/database/go.sum index 8b50aa66..1525ce13 100644 --- a/database/go.sum +++ b/database/go.sum @@ -1,25 +1,50 @@ +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/davecgh/go-spew v1.0.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/go-sql-driver/mysql v1.10.1 h1:arlSnNLq6a5yxGxV7qg9lF4j0C+KwD6NbQyKr9QL6ME= +github.com/go-sql-driver/mysql v1.10.1/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= 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/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg= modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE= diff --git a/database/integration_test.go b/database/integration_test.go new file mode 100644 index 00000000..b8825e0c --- /dev/null +++ b/database/integration_test.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2026 Lanka Software Foundation + +package database + +import ( + "context" + "database/sql" + "os" + "strconv" + "testing" + + _ "github.com/go-sql-driver/mysql" + _ "github.com/jackc/pgx/v5/stdlib" +) + +const ( + integrationMaxOpen = 4 + integrationMaxIdle = 1 + integrationLifetimeSeconds = 60 +) + +func TestPostgresIntegration(t *testing.T) { + host := os.Getenv("DATABASE_TEST_POSTGRES_HOST") + if host == "" { + t.Skip("DATABASE_TEST_POSTGRES_HOST is not set") + } + + cfg := Config{ + Driver: Postgres, + Postgres: &PostgresConfig{ + Host: host, + Port: integrationPort(t, "DATABASE_TEST_POSTGRES_PORT", 5432), + User: integrationRequired(t, "DATABASE_TEST_POSTGRES_USER"), + Password: integrationRequired(t, "DATABASE_TEST_POSTGRES_PASSWORD"), + Name: integrationRequired(t, "DATABASE_TEST_POSTGRES_NAME"), + SSLMode: "disable", + Pool: PoolConfig{ + MaxOpenConns: integrationMaxOpen, + MaxIdleConns: integrationMaxIdle, + MaxConnLifetimeSeconds: integrationLifetimeSeconds, + }, + }, + } + + testLiveDatabase(t, cfg) +} + +func TestMySQLIntegration(t *testing.T) { + host := os.Getenv("DATABASE_TEST_MYSQL_HOST") + if host == "" { + t.Skip("DATABASE_TEST_MYSQL_HOST is not set") + } + + cfg := Config{ + Driver: MySQL, + MySQL: &MySQLConfig{ + Host: host, + Port: integrationPort(t, "DATABASE_TEST_MYSQL_PORT", 3306), + User: integrationRequired(t, "DATABASE_TEST_MYSQL_USER"), + Password: integrationRequired(t, "DATABASE_TEST_MYSQL_PASSWORD"), + Name: integrationRequired(t, "DATABASE_TEST_MYSQL_NAME"), + Pool: PoolConfig{ + MaxOpenConns: integrationMaxOpen, + MaxIdleConns: integrationMaxIdle, + MaxConnLifetimeSeconds: integrationLifetimeSeconds, + }, + }, + } + + testLiveDatabase(t, cfg) +} + +func testLiveDatabase(t *testing.T, cfg Config) { + t.Helper() + + ctx := context.Background() + db, err := New(ctx, cfg) + if err != nil { + t.Fatalf("New returned unexpected error: %v", err) + } + t.Cleanup(func() { _ = db.Close() }) + + stats := db.Stats() + if stats.MaxOpenConnections != integrationMaxOpen { + t.Errorf("MaxOpenConnections = %d, want %d", stats.MaxOpenConnections, integrationMaxOpen) + } + + conns := make([]*sql.Conn, 0, integrationMaxIdle+1) + for i := 0; i < integrationMaxIdle+1; i++ { + conn, err := db.Conn(ctx) + if err != nil { + t.Fatalf("db.Conn() failed for connection %d: %v", i+1, err) + } + if err := conn.PingContext(ctx); err != nil { + _ = conn.Close() + t.Fatalf("connection %d ping failed: %v", i+1, err) + } + conns = append(conns, conn) + } + for _, conn := range conns { + if err := conn.Close(); err != nil { + t.Errorf("closing pooled connection failed: %v", err) + } + } + + stats = db.Stats() + if stats.Idle > integrationMaxIdle { + t.Errorf("Idle connections = %d, want at most %d", stats.Idle, integrationMaxIdle) + } + + if err := HealthCheck(ctx, db); err != nil { + t.Fatalf("HealthCheck returned unexpected error: %v", err) + } + + var one int + if err := db.QueryRowContext(ctx, "SELECT 1").Scan(&one); err != nil { + t.Fatalf("SELECT 1 failed: %v", err) + } + if one != 1 { + t.Fatalf("SELECT 1 returned %d, want 1", one) + } +} + +func integrationRequired(t *testing.T, key string) string { + t.Helper() + value := os.Getenv(key) + if value == "" { + t.Fatalf("%s must be set when the integration host is configured", key) + } + return value +} + +func integrationPort(t *testing.T, key string, defaultPort int) int { + t.Helper() + value := os.Getenv(key) + if value == "" { + return defaultPort + } + + port, err := strconv.Atoi(value) + if err != nil || port < 1 || port > 65535 { + t.Fatalf("%s must be a valid TCP port, got %q", key, value) + } + return port +}