sequel-duckdb 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.beads/.beads-credential-key +1 -0
- data/.beads/.gitignore +66 -0
- data/.beads/README.md +85 -0
- data/.beads/config.yaml +56 -0
- data/.beads/hooks/post-checkout +24 -0
- data/.beads/hooks/post-merge +24 -0
- data/.beads/hooks/pre-commit +24 -0
- data/.beads/hooks/pre-push +24 -0
- data/.beads/hooks/prepare-commit-msg +24 -0
- data/.beads/metadata.json +7 -0
- data/.kiro/specs/advanced-sql-features-implementation/design.md +3 -1
- data/.kiro/specs/advanced-sql-features-implementation/requirements.md +1 -1
- data/.kiro/specs/advanced-sql-features-implementation/tasks.md +5 -1
- data/.kiro/specs/duckdb-sql-syntax-compatibility/design.md +15 -1
- data/.kiro/specs/duckdb-sql-syntax-compatibility/requirements.md +1 -1
- data/.kiro/specs/duckdb-sql-syntax-compatibility/tasks.md +13 -0
- data/.kiro/specs/edge-cases-and-validation-fixes/requirements.md +1 -1
- data/.kiro/specs/integration-test-database-setup/requirements.md +1 -1
- data/.kiro/specs/sequel-duckdb-adapter/design.md +8 -1
- data/.kiro/specs/sequel-duckdb-adapter/requirements.md +10 -10
- data/.kiro/specs/sequel-duckdb-adapter/tasks.md +48 -3
- data/.kiro/specs/sql-expression-handling-fix/design.md +34 -1
- data/.kiro/specs/sql-expression-handling-fix/requirements.md +1 -1
- data/.kiro/specs/sql-expression-handling-fix/tasks.md +3 -0
- data/.kiro/specs/test-infrastructure-improvements/requirements.md +1 -1
- data/.kiro/steering/product.md +5 -1
- data/.kiro/steering/structure.md +1 -1
- data/.kiro/steering/tech.md +14 -1
- data/.kiro/steering/testing.md +22 -1
- data/.mdformat.toml +2 -0
- data/.release-please-manifest.json +3 -0
- data/.rubocop.yml +116 -58
- data/.rubocop_todo.yml +323 -0
- data/AGENTS.md +154 -0
- data/API_DOCUMENTATION.md +73 -49
- data/CHANGELOG.md +46 -10
- data/FINAL_STATUS.md +99 -0
- data/LICENSE +1 -1
- data/MIGRATION_EXAMPLES.md +1 -1
- data/PERFORMANCE_OPTIMIZATIONS.md +4 -1
- data/README.md +90 -1
- data/REFACTORING_SUMMARY.md +264 -0
- data/Rakefile +21 -5
- data/TASK_10.2_IMPLEMENTATION_SUMMARY.md +19 -1
- data/docs/DUCKDB_SQL_PATTERNS.md +39 -1
- data/docs/TASK_12_VERIFICATION_SUMMARY.md +14 -1
- data/justfile +52 -0
- data/lib/sequel/adapters/duckdb.rb +137 -108
- data/lib/sequel/adapters/shared/duckdb.rb +292 -1490
- data/lib/sequel/duckdb/helpers/copier.rb +50 -0
- data/lib/sequel/duckdb/helpers/pathifier.rb +141 -0
- data/lib/sequel/duckdb/version.rb +5 -2
- data/plans/date_arithmetic.md +420 -0
- data/plans/engineering/Sequel.md +471 -0
- data/plans/engineering/duckdb.md +712 -0
- data/plans/engineering/sqlite.md +453 -0
- data/plans/mock_connection_bug.md +333 -0
- data/plans/mock_without_driver_gem.md +371 -0
- data/plans/over_engineering_analysis.md +122 -0
- data/plans/schema_management.md +383 -0
- data/release-please-config.json +14 -0
- metadata +49 -27
data/docs/DUCKDB_SQL_PATTERNS.md
CHANGED
|
@@ -13,6 +13,7 @@ The sequel-duckdb adapter generates SQL that is optimized for DuckDB's analytica
|
|
|
13
13
|
The adapter generates clean LIKE clauses without unnecessary ESCAPE clauses, following DuckDB's simplified syntax requirements.
|
|
14
14
|
|
|
15
15
|
#### Standard LIKE Patterns
|
|
16
|
+
|
|
16
17
|
```ruby
|
|
17
18
|
# Sequel Code
|
|
18
19
|
dataset.where(Sequel.like(:name, "%John%"))
|
|
@@ -22,6 +23,7 @@ SELECT * FROM users WHERE (name LIKE '%John%')
|
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
#### NOT LIKE Patterns
|
|
26
|
+
|
|
25
27
|
```ruby
|
|
26
28
|
# Sequel Code
|
|
27
29
|
dataset.exclude(Sequel.like(:name, "%John%"))
|
|
@@ -31,6 +33,7 @@ SELECT * FROM users WHERE (name NOT LIKE '%John%')
|
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
#### Pattern Variations
|
|
36
|
+
|
|
34
37
|
```ruby
|
|
35
38
|
# Prefix matching
|
|
36
39
|
dataset.where(Sequel.like(:name, "John%"))
|
|
@@ -52,6 +55,7 @@ dataset.where(Sequel.like(:name, "%John%"))
|
|
|
52
55
|
Since DuckDB doesn't have native ILIKE support, the adapter converts ILIKE operations to UPPER() LIKE UPPER() patterns with proper parentheses.
|
|
53
56
|
|
|
54
57
|
#### ILIKE Conversion
|
|
58
|
+
|
|
55
59
|
```ruby
|
|
56
60
|
# Sequel Code
|
|
57
61
|
dataset.where(Sequel.ilike(:name, "%john%"))
|
|
@@ -61,6 +65,7 @@ SELECT * FROM users WHERE (UPPER(name) LIKE UPPER('%john%'))
|
|
|
61
65
|
```
|
|
62
66
|
|
|
63
67
|
#### NOT ILIKE Conversion
|
|
68
|
+
|
|
64
69
|
```ruby
|
|
65
70
|
# Sequel Code
|
|
66
71
|
dataset.exclude(Sequel.ilike(:name, "%john%"))
|
|
@@ -76,6 +81,7 @@ SELECT * FROM users WHERE (UPPER(name) NOT LIKE UPPER('%john%'))
|
|
|
76
81
|
The adapter uses DuckDB's `regexp_matches()` function for reliable regex operations, with proper parentheses for expression grouping.
|
|
77
82
|
|
|
78
83
|
#### Basic Regex Matching
|
|
84
|
+
|
|
79
85
|
```ruby
|
|
80
86
|
# Sequel Code
|
|
81
87
|
dataset.where(name: /^John/)
|
|
@@ -85,6 +91,7 @@ SELECT * FROM users WHERE (regexp_matches(name, '^John'))
|
|
|
85
91
|
```
|
|
86
92
|
|
|
87
93
|
#### Case-Insensitive Regex
|
|
94
|
+
|
|
88
95
|
```ruby
|
|
89
96
|
# Sequel Code
|
|
90
97
|
dataset.where(name: /john/i)
|
|
@@ -94,6 +101,7 @@ SELECT * FROM users WHERE (regexp_matches(name, 'john', 'i'))
|
|
|
94
101
|
```
|
|
95
102
|
|
|
96
103
|
#### Complex Regex Patterns
|
|
104
|
+
|
|
97
105
|
```ruby
|
|
98
106
|
# Sequel Code
|
|
99
107
|
dataset.where(name: /^John.*Doe$/)
|
|
@@ -109,6 +117,7 @@ SELECT * FROM users WHERE (regexp_matches(name, '^John.*Doe$'))
|
|
|
109
117
|
The adapter uses standard SQL dot notation for qualified column references, ensuring compatibility with SQL standards and DuckDB's expectations.
|
|
110
118
|
|
|
111
119
|
#### Table.Column Format
|
|
120
|
+
|
|
112
121
|
```ruby
|
|
113
122
|
# Sequel Code
|
|
114
123
|
dataset.join(:profiles, user_id: :id)
|
|
@@ -118,6 +127,7 @@ SELECT * FROM users INNER JOIN profiles ON (profiles.user_id = users.id)
|
|
|
118
127
|
```
|
|
119
128
|
|
|
120
129
|
#### Subquery Column References
|
|
130
|
+
|
|
121
131
|
```ruby
|
|
122
132
|
# Sequel Code
|
|
123
133
|
subquery = db[:orders].select(:count).where(user_id: :users__id)
|
|
@@ -134,6 +144,7 @@ SELECT name, (SELECT count FROM orders WHERE (user_id = users.id)) AS order_coun
|
|
|
134
144
|
The adapter supports all standard JOIN types with proper syntax generation for DuckDB.
|
|
135
145
|
|
|
136
146
|
#### INNER JOIN
|
|
147
|
+
|
|
137
148
|
```ruby
|
|
138
149
|
# Sequel Code
|
|
139
150
|
dataset.join(:profiles, user_id: :id)
|
|
@@ -143,6 +154,7 @@ SELECT * FROM users INNER JOIN profiles ON (profiles.user_id = users.id)
|
|
|
143
154
|
```
|
|
144
155
|
|
|
145
156
|
#### LEFT JOIN
|
|
157
|
+
|
|
146
158
|
```ruby
|
|
147
159
|
# Sequel Code
|
|
148
160
|
dataset.left_join(:profiles, user_id: :id)
|
|
@@ -152,6 +164,7 @@ SELECT * FROM users LEFT JOIN profiles ON (profiles.user_id = users.id)
|
|
|
152
164
|
```
|
|
153
165
|
|
|
154
166
|
#### JOIN USING Clause
|
|
167
|
+
|
|
155
168
|
```ruby
|
|
156
169
|
# Sequel Code (using internal JOIN USING clause)
|
|
157
170
|
join_clause = Sequel::SQL::JoinUsingClause.new([:user_id], :inner, :profiles)
|
|
@@ -162,6 +175,7 @@ SELECT * FROM users INNER JOIN profiles USING (user_id)
|
|
|
162
175
|
```
|
|
163
176
|
|
|
164
177
|
#### Multiple Column USING
|
|
178
|
+
|
|
165
179
|
```ruby
|
|
166
180
|
# Generated SQL for multiple columns
|
|
167
181
|
SELECT * FROM users INNER JOIN profiles USING (user_id, company_id)
|
|
@@ -174,6 +188,7 @@ SELECT * FROM users INNER JOIN profiles USING (user_id, company_id)
|
|
|
174
188
|
The adapter automatically detects recursive CTEs and generates appropriate WITH RECURSIVE syntax.
|
|
175
189
|
|
|
176
190
|
#### Regular CTE
|
|
191
|
+
|
|
177
192
|
```ruby
|
|
178
193
|
# Sequel Code
|
|
179
194
|
cte = db[:users].select(:id, :name).where(active: true)
|
|
@@ -184,6 +199,7 @@ WITH active_users AS (SELECT id, name FROM users WHERE (active IS TRUE)) SELECT
|
|
|
184
199
|
```
|
|
185
200
|
|
|
186
201
|
#### Recursive CTE (Auto-detected)
|
|
202
|
+
|
|
187
203
|
```ruby
|
|
188
204
|
# Sequel Code
|
|
189
205
|
base_case = db.select(Sequel.as(1, :n))
|
|
@@ -202,6 +218,7 @@ WITH RECURSIVE t AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM t WHERE (n < 10))
|
|
|
202
218
|
The adapter formats literals according to DuckDB's expectations for optimal type handling.
|
|
203
219
|
|
|
204
220
|
#### String Literals
|
|
221
|
+
|
|
205
222
|
```ruby
|
|
206
223
|
# Sequel Code
|
|
207
224
|
dataset.where(name: "John's Name")
|
|
@@ -211,6 +228,7 @@ SELECT * FROM users WHERE (name = 'John''s Name')
|
|
|
211
228
|
```
|
|
212
229
|
|
|
213
230
|
#### Date/Time Literals
|
|
231
|
+
|
|
214
232
|
```ruby
|
|
215
233
|
# Date literal
|
|
216
234
|
dataset.where(birth_date: Date.new(2023, 5, 15))
|
|
@@ -226,6 +244,7 @@ dataset.where(start_time: Time.local(1970, 1, 1, 9, 30, 0))
|
|
|
226
244
|
```
|
|
227
245
|
|
|
228
246
|
#### Boolean Literals
|
|
247
|
+
|
|
229
248
|
```ruby
|
|
230
249
|
# Sequel Code
|
|
231
250
|
dataset.where(active: true)
|
|
@@ -235,6 +254,7 @@ SELECT * FROM users WHERE (active IS TRUE)
|
|
|
235
254
|
```
|
|
236
255
|
|
|
237
256
|
#### NULL Literals
|
|
257
|
+
|
|
238
258
|
```ruby
|
|
239
259
|
# Sequel Code
|
|
240
260
|
dataset.where(deleted_at: nil)
|
|
@@ -250,6 +270,7 @@ SELECT * FROM users WHERE (deleted_at IS NULL)
|
|
|
250
270
|
The adapter ensures proper parentheses around complex expressions for correct operator precedence and readability.
|
|
251
271
|
|
|
252
272
|
#### Expression Grouping
|
|
273
|
+
|
|
253
274
|
```ruby
|
|
254
275
|
# All complex expressions are wrapped in parentheses
|
|
255
276
|
# LIKE: (name LIKE '%John%')
|
|
@@ -266,6 +287,7 @@ The adapter ensures proper parentheses around complex expressions for correct op
|
|
|
266
287
|
The adapter supports DuckDB's comprehensive window function capabilities.
|
|
267
288
|
|
|
268
289
|
#### Basic Window Function
|
|
290
|
+
|
|
269
291
|
```ruby
|
|
270
292
|
# Sequel Code
|
|
271
293
|
dataset.select(:name, Sequel.function(:row_number).over(order: :name))
|
|
@@ -275,6 +297,7 @@ SELECT name, row_number() OVER (ORDER BY name) FROM users
|
|
|
275
297
|
```
|
|
276
298
|
|
|
277
299
|
#### Partitioned Window Function
|
|
300
|
+
|
|
278
301
|
```ruby
|
|
279
302
|
# Sequel Code
|
|
280
303
|
dataset.select(
|
|
@@ -294,6 +317,7 @@ SELECT product_id, amount, rank() OVER (PARTITION BY category ORDER BY amount DE
|
|
|
294
317
|
The adapter generates standard aggregate function syntax optimized for DuckDB's columnar storage.
|
|
295
318
|
|
|
296
319
|
#### Standard Aggregates
|
|
320
|
+
|
|
297
321
|
```ruby
|
|
298
322
|
# Sequel Code
|
|
299
323
|
dataset.select(
|
|
@@ -311,12 +335,15 @@ SELECT count(*) AS total_count, avg(age) AS avg_age, max(age) AS max_age FROM us
|
|
|
311
335
|
## Performance Optimizations
|
|
312
336
|
|
|
313
337
|
### 1. Columnar Projection
|
|
338
|
+
|
|
314
339
|
The adapter optimizes SELECT statements for DuckDB's columnar storage by generating efficient column projections.
|
|
315
340
|
|
|
316
341
|
### 2. Parallel Execution Hints
|
|
342
|
+
|
|
317
343
|
For complex queries, the adapter can include hints for DuckDB's parallel execution engine.
|
|
318
344
|
|
|
319
345
|
### 3. Bulk Operations
|
|
346
|
+
|
|
320
347
|
The adapter uses DuckDB's efficient bulk loading capabilities for multi-insert operations.
|
|
321
348
|
|
|
322
349
|
## Error Handling Patterns
|
|
@@ -331,6 +358,7 @@ The adapter maps DuckDB errors to appropriate Sequel exception types:
|
|
|
331
358
|
## Best Practices for Developers
|
|
332
359
|
|
|
333
360
|
### 1. Use Appropriate Data Types
|
|
361
|
+
|
|
334
362
|
```ruby
|
|
335
363
|
# Prefer specific types for better performance
|
|
336
364
|
create_table :events do
|
|
@@ -344,6 +372,7 @@ end
|
|
|
344
372
|
```
|
|
345
373
|
|
|
346
374
|
### 2. Leverage DuckDB's Analytical Features
|
|
375
|
+
|
|
347
376
|
```ruby
|
|
348
377
|
# Use window functions for analytical queries
|
|
349
378
|
sales_with_rank = db[:sales]
|
|
@@ -364,6 +393,7 @@ result = db.with(:monthly, monthly_sales)
|
|
|
364
393
|
```
|
|
365
394
|
|
|
366
395
|
### 3. Optimize for Columnar Storage
|
|
396
|
+
|
|
367
397
|
```ruby
|
|
368
398
|
# Select only needed columns for better performance
|
|
369
399
|
db[:large_table].select(:id, :name, :amount).where(active: true)
|
|
@@ -379,32 +409,40 @@ db[:sales].group(:category).select(
|
|
|
379
409
|
## Troubleshooting Common Issues
|
|
380
410
|
|
|
381
411
|
### 1. LIKE Clause Issues
|
|
412
|
+
|
|
382
413
|
If LIKE clauses aren't working as expected, ensure you're not expecting ESCAPE clause behavior:
|
|
414
|
+
|
|
383
415
|
```ruby
|
|
384
416
|
# Correct - no ESCAPE needed
|
|
385
417
|
dataset.where(Sequel.like(:name, "%John%"))
|
|
386
418
|
```
|
|
387
419
|
|
|
388
420
|
### 2. Case-Insensitive Matching
|
|
421
|
+
|
|
389
422
|
Use ILIKE for case-insensitive matching:
|
|
423
|
+
|
|
390
424
|
```ruby
|
|
391
425
|
# Case-insensitive search
|
|
392
426
|
dataset.where(Sequel.ilike(:name, "%john%"))
|
|
393
427
|
```
|
|
394
428
|
|
|
395
429
|
### 3. Regular Expression Matching
|
|
430
|
+
|
|
396
431
|
Use Ruby regex syntax for pattern matching:
|
|
432
|
+
|
|
397
433
|
```ruby
|
|
398
434
|
# Regex matching
|
|
399
435
|
dataset.where(name: /^John.*Doe$/)
|
|
400
436
|
```
|
|
401
437
|
|
|
402
438
|
### 4. Qualified Column References
|
|
439
|
+
|
|
403
440
|
Use standard Sequel syntax for qualified columns:
|
|
441
|
+
|
|
404
442
|
```ruby
|
|
405
443
|
# Correct qualified reference
|
|
406
444
|
dataset.join(:profiles, user_id: :id)
|
|
407
445
|
# Generates: profiles.user_id = users.id
|
|
408
446
|
```
|
|
409
447
|
|
|
410
|
-
This documentation provides a comprehensive reference for understanding and working with the SQL patterns generated by the sequel-duckdb adapter. The patterns are designed to leverage DuckDB's strengths while maintaining compatibility with Sequel's conventions.
|
|
448
|
+
This documentation provides a comprehensive reference for understanding and working with the SQL patterns generated by the sequel-duckdb adapter. The patterns are designed to leverage DuckDB's strengths while maintaining compatibility with Sequel's conventions.
|
|
@@ -7,6 +7,7 @@ Task 12 has been successfully completed. All tests in the sequel-duckdb adapter
|
|
|
7
7
|
## Test Results Summary
|
|
8
8
|
|
|
9
9
|
### Complete Test Suite Results
|
|
10
|
+
|
|
10
11
|
- **Total Tests**: 547 runs
|
|
11
12
|
- **Total Assertions**: 42,451 assertions
|
|
12
13
|
- **Failures**: 0
|
|
@@ -17,6 +18,7 @@ Task 12 has been successfully completed. All tests in the sequel-duckdb adapter
|
|
|
17
18
|
### Key Test Categories Verified
|
|
18
19
|
|
|
19
20
|
#### 1. SQL Generation Tests (62 tests, 69 assertions)
|
|
21
|
+
|
|
20
22
|
- All SQL generation patterns produce consistent, standard SQL
|
|
21
23
|
- LIKE clauses generate clean SQL without unnecessary ESCAPE clauses
|
|
22
24
|
- Complex expressions are properly parenthesized
|
|
@@ -24,21 +26,25 @@ Task 12 has been successfully completed. All tests in the sequel-duckdb adapter
|
|
|
24
26
|
- All SQL syntax follows Sequel conventions
|
|
25
27
|
|
|
26
28
|
#### 2. Dataset Tests (50 tests, 201 assertions)
|
|
29
|
+
|
|
27
30
|
- Dataset operations work correctly with generated SQL
|
|
28
31
|
- Integration between SQL generation and actual database operations
|
|
29
32
|
- Proper handling of complex queries and data operations
|
|
30
33
|
|
|
31
34
|
#### 3. Core SQL Generation Tests (56 tests, 56 assertions)
|
|
35
|
+
|
|
32
36
|
- Basic SQL operations (SELECT, INSERT, UPDATE, DELETE) generate correct syntax
|
|
33
37
|
- Proper handling of data types, literals, and expressions
|
|
34
38
|
- Consistent identifier quoting and escaping
|
|
35
39
|
|
|
36
40
|
#### 4. Advanced SQL Generation Tests (70 tests, 70 assertions)
|
|
41
|
+
|
|
37
42
|
- Complex SQL features work correctly (CTEs, window functions, subqueries)
|
|
38
43
|
- JOIN operations including JOIN USING generate proper syntax
|
|
39
44
|
- Recursive CTEs include RECURSIVE keyword when needed
|
|
40
45
|
|
|
41
46
|
#### 5. Integration Tests (7 tests, 367 assertions)
|
|
47
|
+
|
|
42
48
|
- End-to-end functionality verification
|
|
43
49
|
- Real database operations work with generated SQL
|
|
44
50
|
- Performance and memory efficiency validation
|
|
@@ -48,30 +54,37 @@ Task 12 has been successfully completed. All tests in the sequel-duckdb adapter
|
|
|
48
54
|
All key SQL patterns that were addressed in previous tasks are working correctly:
|
|
49
55
|
|
|
50
56
|
### ✅ LIKE Clause Generation (Requirement 1.1)
|
|
57
|
+
|
|
51
58
|
- **Generated SQL**: `SELECT * FROM users WHERE (name LIKE '%John%')`
|
|
52
59
|
- **Status**: Clean generation without ESCAPE clause
|
|
53
60
|
|
|
54
61
|
### ✅ ILIKE Clause Generation (Requirement 1.3)
|
|
62
|
+
|
|
55
63
|
- **Generated SQL**: `SELECT * FROM users WHERE (UPPER(name) LIKE UPPER('%john%'))`
|
|
56
64
|
- **Status**: Proper parentheses and UPPER() conversion
|
|
57
65
|
|
|
58
66
|
### ✅ Regex Expression Generation (Requirement 2.2)
|
|
67
|
+
|
|
59
68
|
- **Generated SQL**: `SELECT * FROM users WHERE (name = '^John')`
|
|
60
69
|
- **Status**: Proper parentheses around expressions
|
|
61
70
|
|
|
62
71
|
### ✅ Qualified Column References (Requirement 3.1)
|
|
72
|
+
|
|
63
73
|
- **Generated SQL**: `SELECT * FROM users WHERE (users.id = 1)`
|
|
64
74
|
- **Status**: Standard dot notation for table.column references
|
|
65
75
|
|
|
66
76
|
### ✅ Subquery Column References (Requirement 5.1)
|
|
77
|
+
|
|
67
78
|
- **Generated SQL**: `SELECT * FROM users WHERE (id IN (SELECT user_id FROM posts WHERE (posts.active IS TRUE)))`
|
|
68
79
|
- **Status**: Proper dot notation in subqueries
|
|
69
80
|
|
|
70
81
|
### ✅ JOIN USING Generation (Requirement 4.1)
|
|
82
|
+
|
|
71
83
|
- **Generated SQL**: `SELECT * FROM users INNER JOIN posts USING (user_id)`
|
|
72
84
|
- **Status**: Correct USING clause syntax
|
|
73
85
|
|
|
74
86
|
### ✅ Recursive CTE Generation (Requirement 5.1)
|
|
87
|
+
|
|
75
88
|
- **Generated SQL**: `WITH RECURSIVE tree AS (...)`
|
|
76
89
|
- **Status**: RECURSIVE keyword properly included
|
|
77
90
|
|
|
@@ -119,4 +132,4 @@ The adapter is ready for production use with confidence in its SQL generation re
|
|
|
119
132
|
- Integration with actual DuckDB database instances
|
|
120
133
|
- Mock database SQL generation patterns
|
|
121
134
|
|
|
122
|
-
**Task 12 Status: ✅ COMPLETED SUCCESSFULLY**
|
|
135
|
+
**Task 12 Status: ✅ COMPLETED SUCCESSFULLY**
|
data/justfile
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
test:
|
|
2
|
+
bundle exec rake test
|
|
3
|
+
|
|
4
|
+
lint:
|
|
5
|
+
bundle exec rubocop
|
|
6
|
+
|
|
7
|
+
ci: fmt-check lint test hygiene
|
|
8
|
+
|
|
9
|
+
bundle-update *ARGS:
|
|
10
|
+
bundle update {{ ARGS }}
|
|
11
|
+
|
|
12
|
+
# CHANGELOG.md is generated by release-please in its own style and rewritten on
|
|
13
|
+
# every release, so the markdown formatter leaves it alone.
|
|
14
|
+
# Rewrite files to canonical format. Run deliberately; never from a hook.
|
|
15
|
+
fmt:
|
|
16
|
+
git ls-files "*.sh" | xargs -r shfmt -w
|
|
17
|
+
just --fmt --unstable
|
|
18
|
+
git ls-files "*.md" ":!:CHANGELOG.md" | xargs -r mdformat
|
|
19
|
+
|
|
20
|
+
# Report format drift without changing anything. This is what the hooks run —
|
|
21
|
+
# a formatter that rewrites files mid-commit changes what you already reviewed.
|
|
22
|
+
fmt-check:
|
|
23
|
+
git ls-files "*.sh" | xargs -r shfmt -d
|
|
24
|
+
just --fmt --check --unstable
|
|
25
|
+
git ls-files "*.md" ":!:CHANGELOG.md" | xargs -r mdformat --check
|
|
26
|
+
|
|
27
|
+
# What actually runs before a push. Defaults to the complete `ci`; point it at
|
|
28
|
+
# something smaller ONLY where running complete CI locally is impractical.
|
|
29
|
+
pre-push: ci
|
|
30
|
+
|
|
31
|
+
# Runs on every commit, so it must stay FAST — a sub-minute budget. Tests belong
|
|
32
|
+
# here when they fit; lint alone when they do not. fmt-check never rewrites.
|
|
33
|
+
pre-commit: fmt-check lint test hygiene
|
|
34
|
+
|
|
35
|
+
# Content checks inherited from overcommit when it was removed (2026-09-12):
|
|
36
|
+
# MergeConflicts, YamlSyntax, JsonSyntax. RuboCop and the test target were already
|
|
37
|
+
# covered by fmt-check/lint/test; HardTabs and TrailingWhitespace were dropped because
|
|
38
|
+
# they fight shfmt, .tsv, and generated files. See habituate/standards.md.
|
|
39
|
+
hygiene:
|
|
40
|
+
#!/usr/bin/env bash
|
|
41
|
+
set -uo pipefail
|
|
42
|
+
rc=0
|
|
43
|
+
bad=$(git ls-files | xargs -r grep -IlE '^(<{7}|={7}|>{7})( |$)' 2>/dev/null || true)
|
|
44
|
+
[ -n "$bad" ] && { echo "merge conflict markers:"; printf '%s\n' "$bad" | sed 's/^/ /'; rc=1; }
|
|
45
|
+
for f in $(git ls-files '*.yml' '*.yaml'); do
|
|
46
|
+
python3 -c 'import yaml,sys; yaml.safe_load(open(sys.argv[1]))' "$f" 2>/dev/null \
|
|
47
|
+
|| { echo "invalid YAML: $f"; rc=1; }
|
|
48
|
+
done
|
|
49
|
+
for f in $(git ls-files '*.json'); do
|
|
50
|
+
jq empty "$f" 2>/dev/null || { echo "invalid JSON: $f"; rc=1; }
|
|
51
|
+
done
|
|
52
|
+
exit $rc
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "sequel"
|
|
4
|
+
require "duckdb"
|
|
4
5
|
require_relative "shared/duckdb"
|
|
5
6
|
|
|
6
7
|
# Sequel is a database toolkit for Ruby that provides a powerful ORM and database abstraction layer.
|
|
@@ -52,6 +53,140 @@ module Sequel
|
|
|
52
53
|
#
|
|
53
54
|
# @since 0.1.0
|
|
54
55
|
module DuckDB
|
|
56
|
+
module DriverDatabaseMethods
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# Open the DuckDB::Database once during Sequel::Database initialization.
|
|
60
|
+
# DuckDB's ATTACH/schema state is per-Database, not per-Connection, so all
|
|
61
|
+
# pool connections must share one Database instance.
|
|
62
|
+
def adapter_initialize
|
|
63
|
+
database_path = opts[:database]
|
|
64
|
+
|
|
65
|
+
@duckdb_database = if database_path == ":memory:" || database_path.nil?
|
|
66
|
+
::DuckDB::Database.open(":memory:")
|
|
67
|
+
else
|
|
68
|
+
if database_path.match?(/^[a-zA-Z]/) && !database_path.start_with?(":")
|
|
69
|
+
database_path = "/#{database_path}"
|
|
70
|
+
end
|
|
71
|
+
::DuckDB::Database.open(database_path)
|
|
72
|
+
end
|
|
73
|
+
rescue ::DuckDB::Error => e
|
|
74
|
+
raise Sequel::DatabaseConnectionError, "Failed to connect to DuckDB database: #{e.message}"
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
raise Sequel::DatabaseConnectionError, "Unexpected error connecting to DuckDB: #{e.message}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
public
|
|
80
|
+
|
|
81
|
+
def connect(_server)
|
|
82
|
+
@duckdb_database.connect
|
|
83
|
+
rescue ::DuckDB::Error => e
|
|
84
|
+
raise Sequel::DatabaseConnectionError, "Failed to connect to DuckDB database: #{e.message}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def disconnect_connection(conn)
|
|
88
|
+
return unless conn
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
conn.close
|
|
92
|
+
rescue ::DuckDB::Error # rubocop:disable Lint/SuppressedException -- best-effort cleanup on disconnect
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def valid_connection?(conn)
|
|
97
|
+
return false unless conn
|
|
98
|
+
|
|
99
|
+
begin
|
|
100
|
+
conn.query("SELECT 1")
|
|
101
|
+
true
|
|
102
|
+
rescue ::DuckDB::Error
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def execute(sql, opts = OPTS, &)
|
|
108
|
+
_execute(:select, sql, opts, &)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def execute_dui(sql, opts = OPTS)
|
|
112
|
+
_execute(:update, sql, opts)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def execute_insert(sql, opts = OPTS)
|
|
116
|
+
_execute(:insert, sql, opts)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def dataset_class_default
|
|
120
|
+
Dataset
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def database_error_classes
|
|
126
|
+
[::DuckDB::Error]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def _execute(type, sql, opts, &block)
|
|
130
|
+
synchronize(opts[:server]) do |conn|
|
|
131
|
+
case type
|
|
132
|
+
when :select
|
|
133
|
+
execute_select(sql, conn, &block)
|
|
134
|
+
when :insert, :update
|
|
135
|
+
log_connection_yield(sql, conn) { conn.query(sql).rows_changed }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
rescue ::DuckDB::Error => e
|
|
139
|
+
raise_error(e, opts)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def execute_select(sql, conn, &block)
|
|
143
|
+
log_connection_yield(sql, conn) do
|
|
144
|
+
result = conn.query(sql)
|
|
145
|
+
yield_rows(result, &block) if block
|
|
146
|
+
result
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def yield_rows(result)
|
|
151
|
+
columns = result.columns
|
|
152
|
+
result.each do |row_array|
|
|
153
|
+
row_hash = {}
|
|
154
|
+
columns.each_with_index do |column, index|
|
|
155
|
+
column_name = column.respond_to?(:name) ? column.name : column.to_s
|
|
156
|
+
row_hash[column_name.to_sym] = row_array[index]
|
|
157
|
+
end
|
|
158
|
+
yield row_hash
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def result_column_names(result)
|
|
163
|
+
result.columns.map do |column|
|
|
164
|
+
column.respond_to?(:name) ? column.name.to_s : column.to_s
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
module DriverDatasetMethods
|
|
170
|
+
def fetch_rows(sql)
|
|
171
|
+
db.synchronize(opts[:server]) do |conn|
|
|
172
|
+
result = db.send(:log_connection_yield, sql, conn) { conn.query(sql) }
|
|
173
|
+
|
|
174
|
+
col_names = db.send(:result_column_names, result)
|
|
175
|
+
self.columns = col_names.map { |c| output_identifier(c) }
|
|
176
|
+
|
|
177
|
+
result.each do |row_array|
|
|
178
|
+
row_hash = {}
|
|
179
|
+
col_names.each_with_index do |col_name, i|
|
|
180
|
+
row_hash[output_identifier(col_name)] = row_array[i]
|
|
181
|
+
end
|
|
182
|
+
yield row_hash
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
rescue ::DuckDB::Error => e
|
|
186
|
+
raise Sequel::DatabaseError, e.message
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
55
190
|
# Database class for DuckDB adapter
|
|
56
191
|
#
|
|
57
192
|
# This class extends Sequel::Database to provide DuckDB-specific functionality.
|
|
@@ -86,107 +221,11 @@ module Sequel
|
|
|
86
221
|
# @since 0.1.0
|
|
87
222
|
class Database < Sequel::Database
|
|
88
223
|
include Sequel::DuckDB::DatabaseMethods
|
|
224
|
+
include Sequel::DuckDB::DriverDatabaseMethods
|
|
89
225
|
|
|
90
226
|
# Set the adapter scheme for DuckDB
|
|
91
227
|
# This allows Sequel.connect('duckdb://...') to work
|
|
92
228
|
set_adapter_scheme :duckdb
|
|
93
|
-
|
|
94
|
-
# Connect to a DuckDB database
|
|
95
|
-
#
|
|
96
|
-
# Creates a connection to either a file-based or in-memory DuckDB database.
|
|
97
|
-
# This method handles the low-level connection establishment and error handling.
|
|
98
|
-
#
|
|
99
|
-
# @param server [Hash] Server configuration options from Sequel
|
|
100
|
-
# @option server [String] :database Database path or ':memory:' for in-memory database
|
|
101
|
-
# @option server [Hash] :config DuckDB-specific configuration options
|
|
102
|
-
# @option server [Boolean] :readonly Whether to open database in read-only mode
|
|
103
|
-
#
|
|
104
|
-
# @return [::DuckDB::Connection] Active DuckDB database connection
|
|
105
|
-
#
|
|
106
|
-
# @raise [Sequel::DatabaseConnectionError] If connection fails due to:
|
|
107
|
-
# - Invalid database path
|
|
108
|
-
# - Insufficient permissions
|
|
109
|
-
# - DuckDB library errors
|
|
110
|
-
# - Configuration errors
|
|
111
|
-
#
|
|
112
|
-
# @example Connect to in-memory database
|
|
113
|
-
# conn = connect(database: ':memory:')
|
|
114
|
-
#
|
|
115
|
-
# @example Connect to file database
|
|
116
|
-
# conn = connect(database: '/path/to/database.duckdb')
|
|
117
|
-
#
|
|
118
|
-
# @example Connect with configuration
|
|
119
|
-
# conn = connect(
|
|
120
|
-
# database: '/path/to/database.duckdb',
|
|
121
|
-
# config: { memory_limit: '2GB', threads: 4 }
|
|
122
|
-
# )
|
|
123
|
-
#
|
|
124
|
-
# @see disconnect_connection
|
|
125
|
-
# @see valid_connection?
|
|
126
|
-
# @since 0.1.0
|
|
127
|
-
def connect(server) # rubocop:disable Metrics/MethodLength
|
|
128
|
-
opts = server_opts(server)
|
|
129
|
-
database_path = opts[:database]
|
|
130
|
-
|
|
131
|
-
begin
|
|
132
|
-
if database_path == ":memory:" || database_path.nil?
|
|
133
|
-
# Create in-memory database and return connection
|
|
134
|
-
db = ::DuckDB::Database.open(":memory:")
|
|
135
|
-
else
|
|
136
|
-
# Fix URI parsing issue - add leading slash if missing for absolute paths
|
|
137
|
-
database_path = "/#{database_path}" if database_path.match?(/^[a-zA-Z]/) && !database_path.start_with?(":")
|
|
138
|
-
|
|
139
|
-
# Create file-based database (will create file if it doesn't exist) and return connection
|
|
140
|
-
db = ::DuckDB::Database.open(database_path)
|
|
141
|
-
end
|
|
142
|
-
db.connect
|
|
143
|
-
rescue ::DuckDB::Error => e
|
|
144
|
-
raise Sequel::DatabaseConnectionError, "Failed to connect to DuckDB database: #{e.message}"
|
|
145
|
-
rescue StandardError => e
|
|
146
|
-
raise Sequel::DatabaseConnectionError, "Unexpected error connecting to DuckDB: #{e.message}"
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Disconnect from a DuckDB database connection
|
|
151
|
-
#
|
|
152
|
-
# @param conn [::DuckDB::Connection] The database connection to close
|
|
153
|
-
# @return [void]
|
|
154
|
-
def disconnect_connection(conn)
|
|
155
|
-
return unless conn
|
|
156
|
-
|
|
157
|
-
begin
|
|
158
|
-
conn.close
|
|
159
|
-
rescue ::DuckDB::Error
|
|
160
|
-
# Ignore errors during disconnect - connection may already be closed
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Check if a DuckDB connection is valid and open
|
|
165
|
-
#
|
|
166
|
-
# @param conn [::DuckDB::Connection] The database connection to check
|
|
167
|
-
# @return [Boolean] true if connection is valid and open, false otherwise
|
|
168
|
-
def valid_connection?(conn)
|
|
169
|
-
return false unless conn
|
|
170
|
-
|
|
171
|
-
begin
|
|
172
|
-
# Try a simple query to check if the connection is still valid
|
|
173
|
-
conn.query("SELECT 1")
|
|
174
|
-
true
|
|
175
|
-
rescue ::DuckDB::Error
|
|
176
|
-
false
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Return the default dataset class for this database
|
|
181
|
-
#
|
|
182
|
-
# This method is called by Sequel to determine which Dataset class
|
|
183
|
-
# to use when creating new datasets for this database connection.
|
|
184
|
-
#
|
|
185
|
-
# @return [Class] The Dataset class to use for this database (always DuckDB::Dataset)
|
|
186
|
-
# @see Dataset
|
|
187
|
-
def dataset_class_default
|
|
188
|
-
Dataset
|
|
189
|
-
end
|
|
190
229
|
end
|
|
191
230
|
|
|
192
231
|
# Dataset class for DuckDB adapter
|
|
@@ -240,17 +279,7 @@ module Sequel
|
|
|
240
279
|
# @since 0.1.0
|
|
241
280
|
class Dataset < Sequel::Dataset
|
|
242
281
|
include Sequel::DuckDB::DatasetMethods
|
|
282
|
+
include Sequel::DuckDB::DriverDatasetMethods
|
|
243
283
|
end
|
|
244
284
|
end
|
|
245
285
|
end
|
|
246
|
-
|
|
247
|
-
# Register the DuckDB adapter with Sequel
|
|
248
|
-
# This registration allows Sequel.connect("duckdb://...") to automatically
|
|
249
|
-
# use the DuckDB adapter and create DuckDB::Database instances.
|
|
250
|
-
#
|
|
251
|
-
# @example Connection string usage
|
|
252
|
-
# db = Sequel.connect('duckdb::memory:')
|
|
253
|
-
# db = Sequel.connect('duckdb:///path/to/database.duckdb')
|
|
254
|
-
#
|
|
255
|
-
# @see Sequel::DuckDB::Database
|
|
256
|
-
Sequel::Database.set_shared_adapter_scheme :duckdb, Sequel::DuckDB
|