Skip to content

Commit cd4b457

Browse files
feat: enhance DuckDBStorage with extension loading options and validation improvements
Signed-off-by: Alex Lovell-Troy <[email protected]>
1 parent 22596f6 commit cd4b457

File tree

4 files changed

+1110
-42
lines changed

4 files changed

+1110
-42
lines changed

README.md

Lines changed: 274 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,309 @@
11
# QuackQuack
22

3-
QuackQuack is a Go library for managing DuckDB databases with support for periodic snapshots and restoration.
3+
QuackQuack is a resilient Go library for managing DuckDB databases with support for periodic snapshots, restoration, and comprehensive health monitoring. It provides robust error handling, graceful degradation, and detailed status reporting for production environments.
44

5-
## Installation
5+
## Features
6+
7+
- 🔄 **Automatic Snapshots**: Periodic database snapshots in Parquet format
8+
- 🔧 **Resilient Extension Loading**: Multiple fallback strategies for DuckDB extensions
9+
- 🏥 **Health Monitoring**: Comprehensive health checks and status reporting
10+
-**Graceful Degradation**: Continues operation even when optional features fail
11+
- 🛡️ **Input Validation**: Early detection of configuration issues
12+
- 📊 **Detailed Logging**: Rich logging and error reporting
13+
- 🔍 **Extension Status Tracking**: Monitor which extensions loaded successfully
614

7-
To install DuckDBStorage, use `go get`:
15+
## Installation
816

917
```sh
1018
go get github.com/OpenCHAMI/quack/quack
1119
```
1220

13-
## Usage
21+
## Quick Start
1422

15-
Here's a basic example of how to use it:
23+
### Basic Usage
1624

1725
```go
1826
package main
1927

2028
import (
29+
"context"
2130
"log"
2231
"time"
2332

24-
"github.com/OpenCHAMI/quack"
33+
"github.com/OpenCHAMI/quack/quack"
2534
)
2635

2736
func main() {
28-
storage, err := quack.NewDuckDBStorage("path/to/db", quack.WithSnapshotFrequency(10*time.Minute))
37+
// Create database with automatic snapshots
38+
storage, err := quack.NewDuckDBStorage("myapp.db",
39+
quack.WithSnapshotFrequency(10*time.Minute),
40+
quack.WithSnapshotPath("backups/"),
41+
quack.WithCreateSnapshotDir(true))
42+
2943
if err != nil {
30-
log.Fatalf("Failed to initialize DuckDBStorage: %v", err)
44+
log.Fatalf("Failed to initialize database: %v", err)
3145
}
32-
defer storage.Close()
46+
defer storage.Shutdown(context.Background())
3347

34-
// Your code here
48+
// Check health status
49+
if !storage.IsHealthy() {
50+
health := storage.HealthCheck()
51+
for _, rec := range health.Recommendations {
52+
log.Printf("Recommendation: %s", rec)
53+
}
54+
}
55+
56+
// Use the database
57+
db := storage.DB()
58+
_, err = db.Exec("CREATE TABLE users (id INTEGER, name TEXT)")
59+
if err != nil {
60+
log.Printf("Error creating table: %v", err)
61+
}
3562
}
3663
```
3764

38-
For a more detailed example, check out the [example/](example/) directory.
65+
### Production Example with Monitoring
66+
67+
```go
68+
package main
69+
70+
import (
71+
"context"
72+
"log"
73+
"time"
74+
75+
"github.com/OpenCHAMI/quack/quack"
76+
)
77+
78+
func main() {
79+
// Production configuration
80+
storage, err := quack.NewDuckDBStorage("production.db",
81+
quack.WithSnapshotFrequency(1*time.Hour),
82+
quack.WithSnapshotPath("/var/backups/myapp/"),
83+
quack.WithCreateSnapshotDir(true))
84+
85+
if err != nil {
86+
// Get detailed error information
87+
log.Fatalf("Database initialization failed: %v", err)
88+
}
89+
defer storage.Shutdown(context.Background())
90+
91+
// Check for any initialization issues
92+
if initErrors := storage.GetInitializationErrors(); len(initErrors) > 0 {
93+
for _, err := range initErrors {
94+
log.Printf("Initialization warning: %v", err)
95+
}
96+
}
3997

40-
## Configuration
98+
// Monitor extension status
99+
extStatus := storage.GetExtensionStatus()
100+
log.Printf("Extensions loaded: %v", extStatus.Loaded)
101+
if len(extStatus.Failed) > 0 {
102+
log.Printf("Extensions failed: %v", extStatus.Failed)
103+
}
41104

42-
DuckDBStorage supports several configuration options through functional options:
43-
* WithSnapshotFrequency(duration time.Duration): Sets the frequency of database snapshots.
44-
* WithSnapshotPath(path string): Sets the path where snapshots will be stored.
45-
* WithRestoreFirst(restore bool): If set to true, the database will be restored from the latest snapshot on initialization.
46-
Example:
105+
// Periodic health monitoring
106+
go func() {
107+
ticker := time.NewTicker(5 * time.Minute)
108+
defer ticker.Stop()
109+
110+
for range ticker.C {
111+
health := storage.HealthCheck()
112+
if !health.Healthy {
113+
log.Printf("Health check failed: database_ok=%v", health.DatabaseOK)
114+
for _, rec := range health.Recommendations {
115+
log.Printf("Recommendation: %s", rec)
116+
}
117+
} else {
118+
log.Printf("Health check passed")
119+
}
120+
}
121+
}()
122+
123+
// Your application logic here
124+
db := storage.DB()
125+
// ... use database
126+
}
127+
```
128+
129+
## Configuration Options
130+
131+
All configuration is done through functional options:
132+
133+
### Core Options
134+
135+
| Option | Description | Example |
136+
|--------|-------------|---------|
137+
| `WithSnapshotFrequency(duration)` | Enable automatic snapshots at specified interval | `WithSnapshotFrequency(1*time.Hour)` |
138+
| `WithSnapshotPath(path)` | Set snapshot storage directory | `WithSnapshotPath("/backups/")` |
139+
| `WithCreateSnapshotDir(bool)` | Auto-create snapshot directory if missing | `WithCreateSnapshotDir(true)` |
140+
| `WithRestore(path)` | Restore from snapshot on startup | `WithRestore("/backups/latest/")` |
141+
142+
### Extension Management
143+
144+
| Option | Description | Example |
145+
|--------|-------------|---------|
146+
| `WithSkipExtensions(bool)` | Skip all extension loading | `WithSkipExtensions(true)` |
147+
148+
You can also use the environment variable `DUCKDB_SKIP_EXTENSIONS=true` to disable extensions.
149+
150+
### Advanced Example
47151

48152
```go
49-
storage, err := quack.NewDuckDBStorage(
50-
"path/to/db",
51-
quack.WithSnapshotFrequency(10*time.Minute),
52-
quack.WithSnapshotPath("path/to/snapshots"),
53-
quack.WithRestoreFirst(true),
153+
storage, err := quack.NewDuckDBStorage("app.db",
154+
// Snapshot configuration
155+
quack.WithSnapshotFrequency(30*time.Minute),
156+
quack.WithSnapshotPath("./backups/"),
157+
quack.WithCreateSnapshotDir(true),
158+
159+
// Extension configuration (useful in containerized environments)
160+
quack.WithSkipExtensions(false), // Default: try to load extensions
54161
)
55162
```
56163

57-
License
164+
## Health Monitoring
165+
166+
QuackQuack provides comprehensive health monitoring:
167+
168+
```go
169+
// Quick health check
170+
if storage.IsHealthy() {
171+
log.Println("Database is healthy")
172+
}
173+
174+
// Detailed health information
175+
health := storage.HealthCheck()
176+
fmt.Printf("Database OK: %v\n", health.DatabaseOK)
177+
fmt.Printf("Extensions Loaded: %v\n", health.ExtensionStatus.Loaded)
178+
fmt.Printf("Extensions Failed: %v\n", health.ExtensionStatus.Failed)
179+
180+
// Get actionable recommendations
181+
for _, rec := range health.Recommendations {
182+
fmt.Printf("💡 %s\n", rec)
183+
}
184+
```
185+
186+
### Health Status Structure
187+
188+
```go
189+
type HealthStatus struct {
190+
Healthy bool `json:"healthy"`
191+
DatabaseOK bool `json:"database_ok"`
192+
ExtensionStatus ExtensionStatus `json:"extension_status"`
193+
SnapshotEnabled bool `json:"snapshot_enabled"`
194+
InitErrors []string `json:"init_errors,omitempty"`
195+
LastHealthCheck time.Time `json:"last_health_check"`
196+
Recommendations []string `json:"recommendations,omitempty"`
197+
}
198+
```
199+
200+
## Error Handling
201+
202+
QuackQuack provides detailed error information and validation:
203+
204+
```go
205+
storage, err := quack.NewDuckDBStorage("invalid\x00path.db")
206+
if err != nil {
207+
// Will get: "validation error for path='invalid\x00path.db': database path contains null bytes"
208+
log.Printf("Validation failed: %v", err)
209+
}
210+
211+
// Check for non-fatal initialization issues
212+
if initErrors := storage.GetInitializationErrors(); len(initErrors) > 0 {
213+
for _, err := range initErrors {
214+
log.Printf("Warning: %v", err)
215+
}
216+
}
217+
```
218+
219+
## Extension Management
220+
221+
QuackQuack uses a multi-strategy approach for loading DuckDB extensions:
222+
223+
1. **Local Loading**: Try to load pre-installed extensions
224+
2. **Auto-Install**: Download and install extensions automatically
225+
3. **Basic Fallback**: Minimal extension loading
226+
227+
```go
228+
// Check extension status
229+
extStatus := storage.GetExtensionStatus()
230+
fmt.Printf("Strategy used: %d\n", extStatus.Strategy)
231+
fmt.Printf("Loaded: %v\n", extStatus.Loaded)
232+
fmt.Printf("Failed: %v\n", extStatus.Failed)
233+
fmt.Printf("Skipped: %v\n", extStatus.Skipped)
234+
235+
// Disable extensions in problematic environments
236+
storage, err := quack.NewDuckDBStorage("app.db",
237+
quack.WithSkipExtensions(true))
238+
239+
// Or via environment variable
240+
os.Setenv("DUCKDB_SKIP_EXTENSIONS", "true")
241+
```
242+
243+
## Environment Variables
244+
245+
| Variable | Description | Default |
246+
|----------|-------------|---------|
247+
| `DUCKDB_HOME` | Directory for extension storage | `$HOME` |
248+
| `DUCKDB_SKIP_EXTENSIONS` | Skip extension loading (`true`/`false`) | `false` |
249+
250+
## Snapshots and Restoration
251+
252+
### Manual Snapshots
253+
254+
```go
255+
ctx := context.Background()
256+
err := storage.SnapshotParquet(ctx, "./manual-backup/")
257+
if err != nil {
258+
log.Printf("Snapshot failed: %v", err)
259+
}
260+
```
261+
262+
### Restoration
263+
264+
```go
265+
// Restore from specific snapshot
266+
storage, err := quack.NewDuckDBStorage("restored.db",
267+
quack.WithRestore("./backups/2023-01-01T12-00-00/"))
268+
```
269+
270+
## Troubleshooting
271+
272+
### Common Issues
273+
274+
1. **Extension Loading Failures**:
275+
```
276+
Set DUCKDB_SKIP_EXTENSIONS=true if extensions aren't needed
277+
```
278+
279+
2. **Permission Errors**:
280+
```
281+
Ensure the database directory is writable
282+
Check DUCKDB_HOME permissions
283+
```
284+
285+
3. **Snapshot Directory Issues**:
286+
```
287+
Use WithCreateSnapshotDir(true) to auto-create directories
288+
```
289+
290+
### Getting Help
291+
292+
Check the health status for actionable recommendations:
293+
294+
```go
295+
health := storage.HealthCheck()
296+
for _, rec := range health.Recommendations {
297+
fmt.Printf("💡 %s\n", rec)
298+
}
299+
```
300+
}
301+
```
302+
303+
## Examples
304+
305+
For a complete working example, check out the [example/](example/) directory.
306+
307+
## License
308+
58309
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

0 commit comments

Comments
 (0)