Skip to content

Commit f30ba50

Browse files
committed
fix: implement GetDBConn() interface for db.DB() method access
- Fixed critical issue where db.DB() method failed due to missing GetDBConnector interface - Implemented GetDBConn() method in connection wrapper to provide proper *sql.DB access - Maintains compatibility with existing time pointer conversion functionality - Follows same pattern used by official GORM drivers (PostgreSQL, MySQL, SQLite) - Enables downstream projects to access underlying *sql.DB for connection pooling and health checks - Added standalone array utilities (ArrayLiteral, SimpleArrayScanner) for optional use - Updated documentation and addressed array/vector support issue report Fixes: db.DB() method compatibility Version: 0.2.1
1 parent d687cd3 commit f30ba50

File tree

9 files changed

+418
-70
lines changed

9 files changed

+418
-70
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,5 @@ Thumbs.db
4040
*.log
4141

4242
next.md
43+
RELEASE.md
44+
bugs/

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to the GORM DuckDB driver will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.2.1] - 2025-06-25
9+
10+
### Fixed
11+
12+
- **Database Connection Access**: Fixed critical issue where `db.DB()` method failed due to missing `GetDBConnector` interface implementation
13+
- **Connection Wrapper**: Implemented `GetDBConn() (*sql.DB, error)` method in connection wrapper to provide proper access to underlying `*sql.DB`
14+
- **GORM Compatibility**: Ensures compatibility with applications requiring direct database access for connection pooling, health checks, and advanced operations
15+
- **Time Pointer Conversion**: Maintained existing time pointer conversion functionality while fixing `db.DB()` access
16+
17+
#### Technical Notes
18+
19+
- Connection wrapper now properly implements GORM's `GetDBConnector` interface
20+
- Follows same pattern used by official GORM drivers (PostgreSQL, MySQL, SQLite)
21+
- Enables downstream projects to call `db.DB()` for `*sql.DB` access while preserving DuckDB-specific features
22+
- All existing functionality (CRUD operations, extensions, time handling) remains unchanged
23+
824
## [0.2.0] - 2025-06-23
925

1026
### Added

README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,95 @@ type Config struct {
239239
- The driver follows DuckDB's SQL dialect and capabilities
240240
- For production use, consider DuckDB's performance characteristics for your specific use case
241241

242+
## Known Limitations
243+
244+
While this driver provides full GORM compatibility, there are some DuckDB-specific limitations to be aware of:
245+
246+
### Migration Schema Validation
247+
248+
**Issue:** DuckDB's `PRAGMA table_info()` returns slightly different column metadata format than PostgreSQL/MySQL.
249+
250+
**Symptoms:**
251+
252+
- GORM AutoMigrate occasionally reports false schema differences
253+
- Unnecessary migration attempts on startup
254+
- Warnings in logs about column type mismatches
255+
256+
**Example Warning:**
257+
258+
```text
259+
[WARN] column type mismatch: expected 'VARCHAR', got 'STRING'
260+
```
261+
262+
**Workaround:**
263+
264+
```go
265+
// Disable automatic migration validation for specific cases
266+
db.AutoMigrate(&YourModel{})
267+
// Add manual validation if needed
268+
```
269+
270+
**Impact:** Low - Cosmetic warnings, doesn't affect functionality
271+
272+
### Transaction Isolation Levels
273+
274+
**Issue:** DuckDB has limited transaction isolation level support compared to traditional databases.
275+
276+
**Symptoms:**
277+
278+
- `db.Begin().Isolation()` methods have limited options
279+
- Some GORM transaction patterns may not work as expected
280+
- Read phenomena behavior differs from PostgreSQL
281+
282+
**Workaround:**
283+
284+
```go
285+
// Use simpler transaction patterns
286+
tx := db.Begin()
287+
defer func() {
288+
if r := recover(); r != nil {
289+
tx.Rollback()
290+
}
291+
}()
292+
293+
// Perform operations...
294+
if err := tx.Commit().Error; err != nil {
295+
return err
296+
}
297+
```
298+
299+
**Impact:** Low - Simple transactions work fine, complex isolation scenarios need adjustment
300+
301+
### Time Pointer Conversion
302+
303+
**Issue:** Current implementation has limitations with `*time.Time` pointer conversion in some edge cases.
304+
305+
**Symptoms:**
306+
307+
- Potential issues when working with nullable time fields
308+
- Some time pointer operations may not behave identically to other GORM drivers
309+
310+
**Workaround:**
311+
312+
```go
313+
// Use time.Time instead of *time.Time when possible
314+
type Model struct {
315+
ID uint `gorm:"primarykey"`
316+
CreatedAt time.Time // Preferred
317+
UpdatedAt time.Time // Preferred
318+
DeletedAt gorm.DeletedAt `gorm:"index"` // This works fine
319+
}
320+
```
321+
322+
**Impact:** Low - Standard GORM time handling works correctly
323+
324+
## Performance Considerations
325+
326+
- DuckDB is optimized for analytical workloads (OLAP) rather than transactional workloads (OLTP)
327+
- For high-frequency write operations, consider batching or using traditional OLTP databases
328+
- DuckDB excels at complex queries, aggregations, and read-heavy workloads
329+
- For production use, consider DuckDB's performance characteristics for your specific use case
330+
242331
## Contributing
243332

244333
This DuckDB driver aims to become an official GORM driver. Contributions are welcome!

array_minimal.go

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package duckdb
2+
3+
import (
4+
"database/sql/driver"
5+
"fmt"
6+
"reflect"
7+
"strings"
8+
)
9+
10+
// isSliceType checks if a type is a supported slice type for DuckDB arrays
11+
func isSliceType(t reflect.Type) bool {
12+
if t.Kind() != reflect.Slice {
13+
return false
14+
}
15+
16+
elem := t.Elem()
17+
switch elem.Kind() {
18+
case reflect.Float32, reflect.Float64:
19+
return true
20+
case reflect.String:
21+
return true
22+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
23+
return true
24+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
25+
return true
26+
case reflect.Bool:
27+
return true
28+
default:
29+
return false
30+
}
31+
}
32+
33+
// formatSliceForDuckDB converts a Go slice to DuckDB array literal syntax
34+
func formatSliceForDuckDB(value interface{}) (string, error) {
35+
v := reflect.ValueOf(value)
36+
if v.Kind() != reflect.Slice {
37+
return "", fmt.Errorf("expected slice, got %T", value)
38+
}
39+
40+
if v.Len() == 0 {
41+
return "[]", nil
42+
}
43+
44+
var elements []string
45+
for i := 0; i < v.Len(); i++ {
46+
elem := v.Index(i)
47+
switch elem.Kind() {
48+
case reflect.Float32, reflect.Float64:
49+
elements = append(elements, fmt.Sprintf("%g", elem.Float()))
50+
case reflect.String:
51+
// Escape single quotes in strings
52+
str := strings.ReplaceAll(elem.String(), "'", "''")
53+
elements = append(elements, fmt.Sprintf("'%s'", str))
54+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
55+
elements = append(elements, fmt.Sprintf("%d", elem.Int()))
56+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
57+
elements = append(elements, fmt.Sprintf("%d", elem.Uint()))
58+
case reflect.Bool:
59+
if elem.Bool() {
60+
elements = append(elements, "true")
61+
} else {
62+
elements = append(elements, "false")
63+
}
64+
default:
65+
return "", fmt.Errorf("unsupported slice element type: %v", elem.Kind())
66+
}
67+
}
68+
69+
return "[" + strings.Join(elements, ", ") + "]", nil
70+
}
71+
72+
// ArrayLiteral wraps a Go slice to be formatted as a DuckDB array literal
73+
type ArrayLiteral struct {
74+
Data interface{}
75+
}
76+
77+
// Value implements driver.Valuer for DuckDB array literals
78+
func (al ArrayLiteral) Value() (driver.Value, error) {
79+
if al.Data == nil {
80+
return nil, nil
81+
}
82+
83+
return formatSliceForDuckDB(al.Data)
84+
}
85+
86+
// SimpleArrayScanner provides basic array scanning functionality
87+
type SimpleArrayScanner struct {
88+
Target interface{} // Pointer to slice
89+
}
90+
91+
// Scan implements sql.Scanner for basic array types
92+
func (sas *SimpleArrayScanner) Scan(value interface{}) error {
93+
if value == nil {
94+
return nil
95+
}
96+
97+
// Handle Go slice types directly (DuckDB returns []interface{})
98+
if slice, ok := value.([]interface{}); ok {
99+
targetValue := reflect.ValueOf(sas.Target)
100+
if targetValue.Kind() != reflect.Ptr || targetValue.Elem().Kind() != reflect.Slice {
101+
return fmt.Errorf("target must be pointer to slice")
102+
}
103+
104+
sliceType := targetValue.Elem().Type()
105+
elemType := sliceType.Elem()
106+
result := reflect.MakeSlice(sliceType, len(slice), len(slice))
107+
108+
for i, elem := range slice {
109+
elemValue := result.Index(i)
110+
111+
switch elemType.Kind() {
112+
case reflect.Float64:
113+
// Handle both float32 and float64 from DuckDB
114+
switch f := elem.(type) {
115+
case float64:
116+
elemValue.SetFloat(f)
117+
case float32:
118+
elemValue.SetFloat(float64(f))
119+
default:
120+
return fmt.Errorf("expected float32/float64, got %T at index %d", elem, i)
121+
}
122+
case reflect.String:
123+
if s, ok := elem.(string); ok {
124+
elemValue.SetString(s)
125+
} else {
126+
return fmt.Errorf("expected string, got %T at index %d", elem, i)
127+
}
128+
case reflect.Int64:
129+
// Handle various integer types from DuckDB
130+
switch i := elem.(type) {
131+
case int64:
132+
elemValue.SetInt(i)
133+
case int32:
134+
elemValue.SetInt(int64(i))
135+
case int:
136+
elemValue.SetInt(int64(i))
137+
default:
138+
return fmt.Errorf("expected integer type, got %T at index %d", elem, i)
139+
}
140+
case reflect.Bool:
141+
if b, ok := elem.(bool); ok {
142+
elemValue.SetBool(b)
143+
} else {
144+
return fmt.Errorf("expected bool, got %T at index %d", elem, i)
145+
}
146+
default:
147+
return fmt.Errorf("unsupported target element type: %v", elemType.Kind())
148+
}
149+
}
150+
151+
targetValue.Elem().Set(result)
152+
return nil
153+
}
154+
155+
// Fallback: Handle string representations of arrays
156+
var arrayStr string
157+
switch v := value.(type) {
158+
case string:
159+
arrayStr = v
160+
case []byte:
161+
arrayStr = string(v)
162+
default:
163+
return fmt.Errorf("cannot scan %T into SimpleArrayScanner", value)
164+
}
165+
166+
// Parse DuckDB array format: [1.0, 2.0, 3.0] or [item1, item2, item3]
167+
arrayStr = strings.TrimSpace(arrayStr)
168+
if !strings.HasPrefix(arrayStr, "[") || !strings.HasSuffix(arrayStr, "]") {
169+
return fmt.Errorf("invalid array format: %s", arrayStr)
170+
}
171+
172+
// Remove brackets
173+
content := arrayStr[1 : len(arrayStr)-1]
174+
content = strings.TrimSpace(content)
175+
176+
if content == "" {
177+
// Empty array
178+
targetValue := reflect.ValueOf(sas.Target)
179+
if targetValue.Kind() != reflect.Ptr || targetValue.Elem().Kind() != reflect.Slice {
180+
return fmt.Errorf("target must be pointer to slice")
181+
}
182+
targetValue.Elem().Set(reflect.MakeSlice(targetValue.Elem().Type(), 0, 0))
183+
return nil
184+
}
185+
186+
// Split elements and parse based on target type
187+
elements := strings.Split(content, ",")
188+
targetValue := reflect.ValueOf(sas.Target)
189+
if targetValue.Kind() != reflect.Ptr || targetValue.Elem().Kind() != reflect.Slice {
190+
return fmt.Errorf("target must be pointer to slice")
191+
}
192+
193+
sliceType := targetValue.Elem().Type()
194+
elemType := sliceType.Elem()
195+
result := reflect.MakeSlice(sliceType, len(elements), len(elements))
196+
197+
for i, elemStr := range elements {
198+
elemStr = strings.TrimSpace(elemStr)
199+
elemValue := result.Index(i)
200+
201+
switch elemType.Kind() {
202+
case reflect.Float64:
203+
var f float64
204+
if _, err := fmt.Sscanf(elemStr, "%f", &f); err != nil {
205+
return fmt.Errorf("failed to parse float: %s", elemStr)
206+
}
207+
elemValue.SetFloat(f)
208+
case reflect.String:
209+
// Remove quotes if present
210+
if strings.HasPrefix(elemStr, "'") && strings.HasSuffix(elemStr, "'") {
211+
elemStr = elemStr[1 : len(elemStr)-1]
212+
elemStr = strings.ReplaceAll(elemStr, "''", "'") // Unescape quotes
213+
}
214+
elemValue.SetString(elemStr)
215+
case reflect.Int64:
216+
var i int64
217+
if _, err := fmt.Sscanf(elemStr, "%d", &i); err != nil {
218+
return fmt.Errorf("failed to parse int: %s", elemStr)
219+
}
220+
elemValue.SetInt(i)
221+
case reflect.Bool:
222+
var b bool
223+
if _, err := fmt.Sscanf(elemStr, "%t", &b); err != nil {
224+
return fmt.Errorf("failed to parse bool: %s", elemStr)
225+
}
226+
elemValue.SetBool(b)
227+
default:
228+
return fmt.Errorf("unsupported target element type: %v", elemType.Kind())
229+
}
230+
}
231+
232+
targetValue.Elem().Set(result)
233+
return nil
234+
}

0 commit comments

Comments
 (0)