database: create shared database package
Extract common database functionality from api/ntpdb and monitor/ntpdb into shared common/database package: - Dynamic connector pattern with configuration loading - Configurable connection pool management (API: 25/10, Monitor: 10/5) - Optional Prometheus metrics integration - Generic transaction helpers with proper error handling - Unified interfaces compatible with SQLC-generated code Foundation for migration to eliminate ~200 lines of duplicate code.
This commit is contained in:
parent
96afb77844
commit
a1a5a6b8be
81
database/config_test.go
Normal file
81
database/config_test.go
Normal file
@ -0,0 +1,81 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func TestDefaultConfigOptions(t *testing.T) {
|
||||
opts := DefaultConfigOptions()
|
||||
|
||||
// Verify expected defaults for API package
|
||||
if opts.MaxOpenConns != 25 {
|
||||
t.Errorf("Expected MaxOpenConns=25, got %d", opts.MaxOpenConns)
|
||||
}
|
||||
if opts.MaxIdleConns != 10 {
|
||||
t.Errorf("Expected MaxIdleConns=10, got %d", opts.MaxIdleConns)
|
||||
}
|
||||
if opts.ConnMaxLifetime != 3*time.Minute {
|
||||
t.Errorf("Expected ConnMaxLifetime=3m, got %v", opts.ConnMaxLifetime)
|
||||
}
|
||||
if !opts.EnablePoolMonitoring {
|
||||
t.Error("Expected EnablePoolMonitoring=true")
|
||||
}
|
||||
if opts.PrometheusRegisterer != prometheus.DefaultRegisterer {
|
||||
t.Error("Expected PrometheusRegisterer to be DefaultRegisterer")
|
||||
}
|
||||
if len(opts.ConfigFiles) == 0 {
|
||||
t.Error("Expected ConfigFiles to be non-empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitorConfigOptions(t *testing.T) {
|
||||
opts := MonitorConfigOptions()
|
||||
|
||||
// Verify expected defaults for Monitor package
|
||||
if opts.MaxOpenConns != 10 {
|
||||
t.Errorf("Expected MaxOpenConns=10, got %d", opts.MaxOpenConns)
|
||||
}
|
||||
if opts.MaxIdleConns != 5 {
|
||||
t.Errorf("Expected MaxIdleConns=5, got %d", opts.MaxIdleConns)
|
||||
}
|
||||
if opts.ConnMaxLifetime != 3*time.Minute {
|
||||
t.Errorf("Expected ConnMaxLifetime=3m, got %v", opts.ConnMaxLifetime)
|
||||
}
|
||||
if opts.EnablePoolMonitoring {
|
||||
t.Error("Expected EnablePoolMonitoring=false")
|
||||
}
|
||||
if opts.PrometheusRegisterer != nil {
|
||||
t.Error("Expected PrometheusRegisterer to be nil")
|
||||
}
|
||||
if len(opts.ConfigFiles) == 0 {
|
||||
t.Error("Expected ConfigFiles to be non-empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigStructures(t *testing.T) {
|
||||
// Test that configuration structures can be created and populated
|
||||
config := Config{
|
||||
MySQL: DBConfig{
|
||||
DSN: "user:pass@tcp(localhost:3306)/dbname",
|
||||
User: "testuser",
|
||||
Pass: "testpass",
|
||||
DBName: "testdb",
|
||||
},
|
||||
}
|
||||
|
||||
if config.MySQL.DSN == "" {
|
||||
t.Error("Expected DSN to be set")
|
||||
}
|
||||
if config.MySQL.User != "testuser" {
|
||||
t.Errorf("Expected User='testuser', got '%s'", config.MySQL.User)
|
||||
}
|
||||
if config.MySQL.Pass != "testpass" {
|
||||
t.Errorf("Expected Pass='testpass', got '%s'", config.MySQL.Pass)
|
||||
}
|
||||
if config.MySQL.DBName != "testdb" {
|
||||
t.Errorf("Expected DBName='testdb', got '%s'", config.MySQL.DBName)
|
||||
}
|
||||
}
|
117
database/integration_test.go
Normal file
117
database/integration_test.go
Normal file
@ -0,0 +1,117 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Mock types for testing SQLC integration patterns
|
||||
type mockQueries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
type mockQueriesTx struct {
|
||||
*mockQueries
|
||||
tx *sql.Tx
|
||||
}
|
||||
|
||||
// Mock the Begin method pattern that SQLC generates
|
||||
func (q *mockQueries) Begin(ctx context.Context) (*mockQueriesTx, error) {
|
||||
// This would normally be: tx, err := q.db.(*sql.DB).BeginTx(ctx, nil)
|
||||
// For our test, we return a mock
|
||||
return &mockQueriesTx{mockQueries: q, tx: nil}, nil
|
||||
}
|
||||
|
||||
func (qtx *mockQueriesTx) Commit(ctx context.Context) error {
|
||||
return nil // Mock implementation
|
||||
}
|
||||
|
||||
func (qtx *mockQueriesTx) Rollback(ctx context.Context) error {
|
||||
return nil // Mock implementation
|
||||
}
|
||||
|
||||
// This test verifies that our common database interfaces are compatible with SQLC-generated code
|
||||
func TestSQLCIntegration(t *testing.T) {
|
||||
// Test that SQLC's DBTX interface matches our DBTX interface
|
||||
t.Run("DBTX Interface Compatibility", func(t *testing.T) {
|
||||
// Test interface compatibility by assignment without execution
|
||||
var ourDBTX DBTX
|
||||
|
||||
// Test with sql.DB (should implement DBTX)
|
||||
var db *sql.DB
|
||||
ourDBTX = db // This will compile only if interfaces are compatible
|
||||
_ = ourDBTX // Use the variable to avoid "unused" warning
|
||||
|
||||
// Test with sql.Tx (should implement DBTX)
|
||||
var tx *sql.Tx
|
||||
ourDBTX = tx // This will compile only if interfaces are compatible
|
||||
_ = ourDBTX // Use the variable to avoid "unused" warning
|
||||
|
||||
// If we reach here, interfaces are compatible
|
||||
t.Log("DBTX interface is compatible with sql.DB and sql.Tx")
|
||||
})
|
||||
|
||||
t.Run("Transaction Interface Compatibility", func(t *testing.T) {
|
||||
// This test verifies our transaction interfaces work with SQLC patterns
|
||||
// We can't define methods inside a function, so we test interface compatibility
|
||||
|
||||
// Verify our DB interface is compatible with what SQLC expects
|
||||
var dbInterface DB[*mockQueriesTx]
|
||||
var mockDB *mockQueries = &mockQueries{}
|
||||
dbInterface = mockDB
|
||||
|
||||
// Test that our transaction helper can work with this pattern
|
||||
err := WithTransaction(context.Background(), dbInterface, func(ctx context.Context, qtx *mockQueriesTx) error {
|
||||
// This would be where you'd call SQLC-generated query methods
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Transaction helper failed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test that demonstrates how the common package would be used with real SQLC patterns
|
||||
func TestRealWorldUsagePattern(t *testing.T) {
|
||||
// This test shows how a package would typically use our common database code
|
||||
|
||||
t.Run("Database Opening Pattern", func(t *testing.T) {
|
||||
// Test that our configuration options work as expected
|
||||
opts := DefaultConfigOptions()
|
||||
|
||||
// Modify for test environment (no actual database connection)
|
||||
opts.ConfigFiles = []string{} // No config files for unit test
|
||||
opts.PrometheusRegisterer = nil // No metrics for unit test
|
||||
|
||||
// This would normally open a database: db, err := OpenDB(ctx, opts)
|
||||
// For our unit test, we just verify the options are reasonable
|
||||
if opts.MaxOpenConns <= 0 {
|
||||
t.Error("MaxOpenConns should be positive")
|
||||
}
|
||||
if opts.MaxIdleConns <= 0 {
|
||||
t.Error("MaxIdleConns should be positive")
|
||||
}
|
||||
if opts.ConnMaxLifetime <= 0 {
|
||||
t.Error("ConnMaxLifetime should be positive")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Monitor Package Configuration", func(t *testing.T) {
|
||||
opts := MonitorConfigOptions()
|
||||
|
||||
// Verify monitor-specific settings
|
||||
if opts.EnablePoolMonitoring {
|
||||
t.Error("Monitor package should not enable pool monitoring")
|
||||
}
|
||||
if opts.PrometheusRegisterer != nil {
|
||||
t.Error("Monitor package should not have Prometheus registerer")
|
||||
}
|
||||
if opts.MaxOpenConns != 10 {
|
||||
t.Errorf("Expected MaxOpenConns=10 for monitor, got %d", opts.MaxOpenConns)
|
||||
}
|
||||
if opts.MaxIdleConns != 5 {
|
||||
t.Errorf("Expected MaxIdleConns=5 for monitor, got %d", opts.MaxIdleConns)
|
||||
}
|
||||
})
|
||||
}
|
@ -41,11 +41,12 @@ func WithTransaction[Q TX](ctx context.Context, db DB[Q], fn func(ctx context.Co
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
err = tx.Commit(ctx)
|
||||
committed = true // Mark as committed regardless of commit success/failure
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to commit transaction: %w", err)
|
||||
}
|
||||
|
||||
committed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
|
157
database/transaction_test.go
Normal file
157
database/transaction_test.go
Normal file
@ -0,0 +1,157 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Mock implementations for testing
|
||||
type mockDB struct {
|
||||
beginError error
|
||||
txMock *mockTX
|
||||
}
|
||||
|
||||
func (m *mockDB) Begin(ctx context.Context) (*mockTX, error) {
|
||||
if m.beginError != nil {
|
||||
return nil, m.beginError
|
||||
}
|
||||
return m.txMock, nil
|
||||
}
|
||||
|
||||
type mockTX struct {
|
||||
commitError error
|
||||
rollbackError error
|
||||
commitCalled bool
|
||||
rollbackCalled bool
|
||||
}
|
||||
|
||||
func (m *mockTX) Commit(ctx context.Context) error {
|
||||
m.commitCalled = true
|
||||
return m.commitError
|
||||
}
|
||||
|
||||
func (m *mockTX) Rollback(ctx context.Context) error {
|
||||
m.rollbackCalled = true
|
||||
return m.rollbackError
|
||||
}
|
||||
|
||||
func TestWithTransaction_Success(t *testing.T) {
|
||||
tx := &mockTX{}
|
||||
db := &mockDB{txMock: tx}
|
||||
|
||||
var functionCalled bool
|
||||
err := WithTransaction(context.Background(), db, func(ctx context.Context, q *mockTX) error {
|
||||
functionCalled = true
|
||||
if q != tx {
|
||||
t.Error("Expected transaction to be passed to function")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if !functionCalled {
|
||||
t.Error("Expected function to be called")
|
||||
}
|
||||
if !tx.commitCalled {
|
||||
t.Error("Expected commit to be called")
|
||||
}
|
||||
if tx.rollbackCalled {
|
||||
t.Error("Expected rollback NOT to be called on success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithTransaction_FunctionError(t *testing.T) {
|
||||
tx := &mockTX{}
|
||||
db := &mockDB{txMock: tx}
|
||||
|
||||
expectedError := errors.New("function error")
|
||||
err := WithTransaction(context.Background(), db, func(ctx context.Context, q *mockTX) error {
|
||||
return expectedError
|
||||
})
|
||||
|
||||
if err != expectedError {
|
||||
t.Errorf("Expected error %v, got %v", expectedError, err)
|
||||
}
|
||||
if tx.commitCalled {
|
||||
t.Error("Expected commit NOT to be called on function error")
|
||||
}
|
||||
if !tx.rollbackCalled {
|
||||
t.Error("Expected rollback to be called on function error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithTransaction_BeginError(t *testing.T) {
|
||||
expectedError := errors.New("begin error")
|
||||
db := &mockDB{beginError: expectedError}
|
||||
|
||||
err := WithTransaction(context.Background(), db, func(ctx context.Context, q *mockTX) error {
|
||||
t.Error("Function should not be called when Begin fails")
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil || !errors.Is(err, expectedError) {
|
||||
t.Errorf("Expected wrapped begin error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithTransaction_CommitError(t *testing.T) {
|
||||
commitError := errors.New("commit error")
|
||||
tx := &mockTX{commitError: commitError}
|
||||
db := &mockDB{txMock: tx}
|
||||
|
||||
err := WithTransaction(context.Background(), db, func(ctx context.Context, q *mockTX) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil || !errors.Is(err, commitError) {
|
||||
t.Errorf("Expected wrapped commit error, got %v", err)
|
||||
}
|
||||
if !tx.commitCalled {
|
||||
t.Error("Expected commit to be called")
|
||||
}
|
||||
if tx.rollbackCalled {
|
||||
t.Error("Expected rollback NOT to be called when commit fails")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithReadOnlyTransaction_Success(t *testing.T) {
|
||||
tx := &mockTX{}
|
||||
db := &mockDB{txMock: tx}
|
||||
|
||||
var functionCalled bool
|
||||
err := WithReadOnlyTransaction(context.Background(), db, func(ctx context.Context, q *mockTX) error {
|
||||
functionCalled = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if !functionCalled {
|
||||
t.Error("Expected function to be called")
|
||||
}
|
||||
if tx.commitCalled {
|
||||
t.Error("Expected commit NOT to be called in read-only transaction")
|
||||
}
|
||||
if !tx.rollbackCalled {
|
||||
t.Error("Expected rollback to be called in read-only transaction")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithReadOnlyTransaction_FunctionError(t *testing.T) {
|
||||
tx := &mockTX{}
|
||||
db := &mockDB{txMock: tx}
|
||||
|
||||
expectedError := errors.New("function error")
|
||||
err := WithReadOnlyTransaction(context.Background(), db, func(ctx context.Context, q *mockTX) error {
|
||||
return expectedError
|
||||
})
|
||||
|
||||
if err != expectedError {
|
||||
t.Errorf("Expected error %v, got %v", expectedError, err)
|
||||
}
|
||||
if !tx.rollbackCalled {
|
||||
t.Error("Expected rollback to be called")
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user