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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.beads-credential-key +1 -0
  3. data/.beads/.gitignore +66 -0
  4. data/.beads/README.md +85 -0
  5. data/.beads/config.yaml +56 -0
  6. data/.beads/hooks/post-checkout +24 -0
  7. data/.beads/hooks/post-merge +24 -0
  8. data/.beads/hooks/pre-commit +24 -0
  9. data/.beads/hooks/pre-push +24 -0
  10. data/.beads/hooks/prepare-commit-msg +24 -0
  11. data/.beads/metadata.json +7 -0
  12. data/.kiro/specs/advanced-sql-features-implementation/design.md +3 -1
  13. data/.kiro/specs/advanced-sql-features-implementation/requirements.md +1 -1
  14. data/.kiro/specs/advanced-sql-features-implementation/tasks.md +5 -1
  15. data/.kiro/specs/duckdb-sql-syntax-compatibility/design.md +15 -1
  16. data/.kiro/specs/duckdb-sql-syntax-compatibility/requirements.md +1 -1
  17. data/.kiro/specs/duckdb-sql-syntax-compatibility/tasks.md +13 -0
  18. data/.kiro/specs/edge-cases-and-validation-fixes/requirements.md +1 -1
  19. data/.kiro/specs/integration-test-database-setup/requirements.md +1 -1
  20. data/.kiro/specs/sequel-duckdb-adapter/design.md +8 -1
  21. data/.kiro/specs/sequel-duckdb-adapter/requirements.md +10 -10
  22. data/.kiro/specs/sequel-duckdb-adapter/tasks.md +48 -3
  23. data/.kiro/specs/sql-expression-handling-fix/design.md +34 -1
  24. data/.kiro/specs/sql-expression-handling-fix/requirements.md +1 -1
  25. data/.kiro/specs/sql-expression-handling-fix/tasks.md +3 -0
  26. data/.kiro/specs/test-infrastructure-improvements/requirements.md +1 -1
  27. data/.kiro/steering/product.md +5 -1
  28. data/.kiro/steering/structure.md +1 -1
  29. data/.kiro/steering/tech.md +14 -1
  30. data/.kiro/steering/testing.md +22 -1
  31. data/.mdformat.toml +2 -0
  32. data/.release-please-manifest.json +3 -0
  33. data/.rubocop.yml +116 -58
  34. data/.rubocop_todo.yml +323 -0
  35. data/AGENTS.md +154 -0
  36. data/API_DOCUMENTATION.md +73 -49
  37. data/CHANGELOG.md +46 -10
  38. data/FINAL_STATUS.md +99 -0
  39. data/LICENSE +1 -1
  40. data/MIGRATION_EXAMPLES.md +1 -1
  41. data/PERFORMANCE_OPTIMIZATIONS.md +4 -1
  42. data/README.md +90 -1
  43. data/REFACTORING_SUMMARY.md +264 -0
  44. data/Rakefile +21 -5
  45. data/TASK_10.2_IMPLEMENTATION_SUMMARY.md +19 -1
  46. data/docs/DUCKDB_SQL_PATTERNS.md +39 -1
  47. data/docs/TASK_12_VERIFICATION_SUMMARY.md +14 -1
  48. data/justfile +52 -0
  49. data/lib/sequel/adapters/duckdb.rb +137 -108
  50. data/lib/sequel/adapters/shared/duckdb.rb +292 -1490
  51. data/lib/sequel/duckdb/helpers/copier.rb +50 -0
  52. data/lib/sequel/duckdb/helpers/pathifier.rb +141 -0
  53. data/lib/sequel/duckdb/version.rb +5 -2
  54. data/plans/date_arithmetic.md +420 -0
  55. data/plans/engineering/Sequel.md +471 -0
  56. data/plans/engineering/duckdb.md +712 -0
  57. data/plans/engineering/sqlite.md +453 -0
  58. data/plans/mock_connection_bug.md +333 -0
  59. data/plans/mock_without_driver_gem.md +371 -0
  60. data/plans/over_engineering_analysis.md +122 -0
  61. data/plans/schema_management.md +383 -0
  62. data/release-please-config.json +14 -0
  63. metadata +49 -27
@@ -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