Skip to content

Commit 2768547

Browse files
author
Hein
committed
feat(dbmanager): ✨ add support for existing SQL connections
* Introduced NewConnectionFromDB function to create connections from existing *sql.DB instances. * Added ExistingDBProvider to wrap existing database connections for dbmanager features. * Implemented tests for NewConnectionFromDB and ExistingDBProvider functionalities.
1 parent cf6a81e commit 2768547

File tree

4 files changed

+531
-0
lines changed

4 files changed

+531
-0
lines changed

pkg/dbmanager/factory.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dbmanager
22

33
import (
4+
"database/sql"
45
"fmt"
56

67
"github.com/bitechdev/ResolveSpec/pkg/dbmanager/providers"
@@ -49,3 +50,18 @@ func createProvider(dbType DatabaseType) (Provider, error) {
4950
// Provider is an alias to the providers.Provider interface
5051
// This allows dbmanager package consumers to use Provider without importing providers
5152
type Provider = providers.Provider
53+
54+
// NewConnectionFromDB creates a new Connection from an existing *sql.DB
55+
// This allows you to use dbmanager features (ORM wrappers, health checks, etc.)
56+
// with a database connection that was opened outside of dbmanager
57+
//
58+
// Parameters:
59+
// - name: A unique name for this connection
60+
// - dbType: The database type (DatabaseTypePostgreSQL, DatabaseTypeSQLite, or DatabaseTypeMSSQL)
61+
// - db: An existing *sql.DB connection
62+
//
63+
// Returns a Connection that wraps the existing *sql.DB
64+
func NewConnectionFromDB(name string, dbType DatabaseType, db *sql.DB) Connection {
65+
provider := providers.NewExistingDBProvider(db, name)
66+
return newSQLConnection(name, dbType, ConnectionConfig{Name: name, Type: dbType}, provider)
67+
}

pkg/dbmanager/factory_test.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package dbmanager
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"testing"
7+
8+
_ "github.com/mattn/go-sqlite3"
9+
)
10+
11+
func TestNewConnectionFromDB(t *testing.T) {
12+
// Open a SQLite in-memory database
13+
db, err := sql.Open("sqlite3", ":memory:")
14+
if err != nil {
15+
t.Fatalf("Failed to open database: %v", err)
16+
}
17+
defer db.Close()
18+
19+
// Create a connection from the existing database
20+
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
21+
if conn == nil {
22+
t.Fatal("Expected connection to be created")
23+
}
24+
25+
// Verify connection properties
26+
if conn.Name() != "test-connection" {
27+
t.Errorf("Expected name 'test-connection', got '%s'", conn.Name())
28+
}
29+
30+
if conn.Type() != DatabaseTypeSQLite {
31+
t.Errorf("Expected type DatabaseTypeSQLite, got '%s'", conn.Type())
32+
}
33+
}
34+
35+
func TestNewConnectionFromDB_Connect(t *testing.T) {
36+
db, err := sql.Open("sqlite3", ":memory:")
37+
if err != nil {
38+
t.Fatalf("Failed to open database: %v", err)
39+
}
40+
defer db.Close()
41+
42+
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
43+
ctx := context.Background()
44+
45+
// Connect should verify the existing connection works
46+
err = conn.Connect(ctx)
47+
if err != nil {
48+
t.Errorf("Expected Connect to succeed, got error: %v", err)
49+
}
50+
51+
// Cleanup
52+
conn.Close()
53+
}
54+
55+
func TestNewConnectionFromDB_Native(t *testing.T) {
56+
db, err := sql.Open("sqlite3", ":memory:")
57+
if err != nil {
58+
t.Fatalf("Failed to open database: %v", err)
59+
}
60+
defer db.Close()
61+
62+
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
63+
ctx := context.Background()
64+
65+
err = conn.Connect(ctx)
66+
if err != nil {
67+
t.Fatalf("Failed to connect: %v", err)
68+
}
69+
defer conn.Close()
70+
71+
// Get native DB
72+
nativeDB, err := conn.Native()
73+
if err != nil {
74+
t.Errorf("Expected Native to succeed, got error: %v", err)
75+
}
76+
77+
if nativeDB != db {
78+
t.Error("Expected Native to return the same database instance")
79+
}
80+
}
81+
82+
func TestNewConnectionFromDB_Bun(t *testing.T) {
83+
db, err := sql.Open("sqlite3", ":memory:")
84+
if err != nil {
85+
t.Fatalf("Failed to open database: %v", err)
86+
}
87+
defer db.Close()
88+
89+
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
90+
ctx := context.Background()
91+
92+
err = conn.Connect(ctx)
93+
if err != nil {
94+
t.Fatalf("Failed to connect: %v", err)
95+
}
96+
defer conn.Close()
97+
98+
// Get Bun ORM
99+
bunDB, err := conn.Bun()
100+
if err != nil {
101+
t.Errorf("Expected Bun to succeed, got error: %v", err)
102+
}
103+
104+
if bunDB == nil {
105+
t.Error("Expected Bun to return a non-nil instance")
106+
}
107+
}
108+
109+
func TestNewConnectionFromDB_GORM(t *testing.T) {
110+
db, err := sql.Open("sqlite3", ":memory:")
111+
if err != nil {
112+
t.Fatalf("Failed to open database: %v", err)
113+
}
114+
defer db.Close()
115+
116+
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
117+
ctx := context.Background()
118+
119+
err = conn.Connect(ctx)
120+
if err != nil {
121+
t.Fatalf("Failed to connect: %v", err)
122+
}
123+
defer conn.Close()
124+
125+
// Get GORM
126+
gormDB, err := conn.GORM()
127+
if err != nil {
128+
t.Errorf("Expected GORM to succeed, got error: %v", err)
129+
}
130+
131+
if gormDB == nil {
132+
t.Error("Expected GORM to return a non-nil instance")
133+
}
134+
}
135+
136+
func TestNewConnectionFromDB_HealthCheck(t *testing.T) {
137+
db, err := sql.Open("sqlite3", ":memory:")
138+
if err != nil {
139+
t.Fatalf("Failed to open database: %v", err)
140+
}
141+
defer db.Close()
142+
143+
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
144+
ctx := context.Background()
145+
146+
err = conn.Connect(ctx)
147+
if err != nil {
148+
t.Fatalf("Failed to connect: %v", err)
149+
}
150+
defer conn.Close()
151+
152+
// Health check should succeed
153+
err = conn.HealthCheck(ctx)
154+
if err != nil {
155+
t.Errorf("Expected HealthCheck to succeed, got error: %v", err)
156+
}
157+
}
158+
159+
func TestNewConnectionFromDB_Stats(t *testing.T) {
160+
db, err := sql.Open("sqlite3", ":memory:")
161+
if err != nil {
162+
t.Fatalf("Failed to open database: %v", err)
163+
}
164+
defer db.Close()
165+
166+
conn := NewConnectionFromDB("test-connection", DatabaseTypeSQLite, db)
167+
ctx := context.Background()
168+
169+
err = conn.Connect(ctx)
170+
if err != nil {
171+
t.Fatalf("Failed to connect: %v", err)
172+
}
173+
defer conn.Close()
174+
175+
stats := conn.Stats()
176+
if stats == nil {
177+
t.Fatal("Expected stats to be returned")
178+
}
179+
180+
if stats.Name != "test-connection" {
181+
t.Errorf("Expected stats.Name to be 'test-connection', got '%s'", stats.Name)
182+
}
183+
184+
if stats.Type != DatabaseTypeSQLite {
185+
t.Errorf("Expected stats.Type to be DatabaseTypeSQLite, got '%s'", stats.Type)
186+
}
187+
188+
if !stats.Connected {
189+
t.Error("Expected stats.Connected to be true")
190+
}
191+
}
192+
193+
func TestNewConnectionFromDB_PostgreSQL(t *testing.T) {
194+
// This test just verifies the factory works with PostgreSQL type
195+
// It won't actually connect since we're using SQLite
196+
db, err := sql.Open("sqlite3", ":memory:")
197+
if err != nil {
198+
t.Fatalf("Failed to open database: %v", err)
199+
}
200+
defer db.Close()
201+
202+
conn := NewConnectionFromDB("test-pg", DatabaseTypePostgreSQL, db)
203+
if conn == nil {
204+
t.Fatal("Expected connection to be created")
205+
}
206+
207+
if conn.Type() != DatabaseTypePostgreSQL {
208+
t.Errorf("Expected type DatabaseTypePostgreSQL, got '%s'", conn.Type())
209+
}
210+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package providers
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"sync"
8+
9+
"go.mongodb.org/mongo-driver/mongo"
10+
)
11+
12+
// ExistingDBProvider wraps an existing *sql.DB connection
13+
// This allows using dbmanager features with a database connection
14+
// that was opened outside of the dbmanager package
15+
type ExistingDBProvider struct {
16+
db *sql.DB
17+
name string
18+
mu sync.RWMutex
19+
}
20+
21+
// NewExistingDBProvider creates a new provider wrapping an existing *sql.DB
22+
func NewExistingDBProvider(db *sql.DB, name string) *ExistingDBProvider {
23+
return &ExistingDBProvider{
24+
db: db,
25+
name: name,
26+
}
27+
}
28+
29+
// Connect verifies the existing database connection is valid
30+
// It does NOT create a new connection, but ensures the existing one works
31+
func (p *ExistingDBProvider) Connect(ctx context.Context, cfg ConnectionConfig) error {
32+
p.mu.Lock()
33+
defer p.mu.Unlock()
34+
35+
if p.db == nil {
36+
return fmt.Errorf("database connection is nil")
37+
}
38+
39+
// Verify the connection works
40+
if err := p.db.PingContext(ctx); err != nil {
41+
return fmt.Errorf("failed to ping existing database: %w", err)
42+
}
43+
44+
return nil
45+
}
46+
47+
// Close closes the underlying database connection
48+
func (p *ExistingDBProvider) Close() error {
49+
p.mu.Lock()
50+
defer p.mu.Unlock()
51+
52+
if p.db == nil {
53+
return nil
54+
}
55+
56+
return p.db.Close()
57+
}
58+
59+
// HealthCheck verifies the connection is alive
60+
func (p *ExistingDBProvider) HealthCheck(ctx context.Context) error {
61+
p.mu.RLock()
62+
defer p.mu.RUnlock()
63+
64+
if p.db == nil {
65+
return fmt.Errorf("database connection is nil")
66+
}
67+
68+
return p.db.PingContext(ctx)
69+
}
70+
71+
// GetNative returns the wrapped *sql.DB
72+
func (p *ExistingDBProvider) GetNative() (*sql.DB, error) {
73+
p.mu.RLock()
74+
defer p.mu.RUnlock()
75+
76+
if p.db == nil {
77+
return nil, fmt.Errorf("database connection is nil")
78+
}
79+
80+
return p.db, nil
81+
}
82+
83+
// GetMongo returns an error since this is a SQL database
84+
func (p *ExistingDBProvider) GetMongo() (*mongo.Client, error) {
85+
return nil, ErrNotMongoDB
86+
}
87+
88+
// Stats returns connection statistics
89+
func (p *ExistingDBProvider) Stats() *ConnectionStats {
90+
p.mu.RLock()
91+
defer p.mu.RUnlock()
92+
93+
stats := &ConnectionStats{
94+
Name: p.name,
95+
Type: "sql", // Generic since we don't know the specific type
96+
Connected: p.db != nil,
97+
}
98+
99+
if p.db != nil {
100+
dbStats := p.db.Stats()
101+
stats.OpenConnections = dbStats.OpenConnections
102+
stats.InUse = dbStats.InUse
103+
stats.Idle = dbStats.Idle
104+
stats.WaitCount = dbStats.WaitCount
105+
stats.WaitDuration = dbStats.WaitDuration
106+
stats.MaxIdleClosed = dbStats.MaxIdleClosed
107+
stats.MaxLifetimeClosed = dbStats.MaxLifetimeClosed
108+
}
109+
110+
return stats
111+
}

0 commit comments

Comments
 (0)