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
19 changes: 0 additions & 19 deletions database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package database

import (
"context"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 9 additions & 1 deletion database/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions database/go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down
146 changes: 146 additions & 0 deletions database/integration_test.go
Original file line number Diff line number Diff line change
@@ -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
}