sequel-duckdb 0.1.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 +7 -0
- data/.kiro/specs/advanced-sql-features-implementation/design.md +24 -0
- data/.kiro/specs/advanced-sql-features-implementation/requirements.md +43 -0
- data/.kiro/specs/advanced-sql-features-implementation/tasks.md +24 -0
- data/.kiro/specs/duckdb-sql-syntax-compatibility/design.md +258 -0
- data/.kiro/specs/duckdb-sql-syntax-compatibility/requirements.md +84 -0
- data/.kiro/specs/duckdb-sql-syntax-compatibility/tasks.md +94 -0
- data/.kiro/specs/edge-cases-and-validation-fixes/requirements.md +32 -0
- data/.kiro/specs/integration-test-database-setup/design.md +0 -0
- data/.kiro/specs/integration-test-database-setup/requirements.md +117 -0
- data/.kiro/specs/sequel-duckdb-adapter/design.md +542 -0
- data/.kiro/specs/sequel-duckdb-adapter/requirements.md +202 -0
- data/.kiro/specs/sequel-duckdb-adapter/tasks.md +247 -0
- data/.kiro/specs/sql-expression-handling-fix/design.md +298 -0
- data/.kiro/specs/sql-expression-handling-fix/requirements.md +86 -0
- data/.kiro/specs/sql-expression-handling-fix/tasks.md +22 -0
- data/.kiro/specs/test-infrastructure-improvements/requirements.md +106 -0
- data/.kiro/steering/product.md +22 -0
- data/.kiro/steering/structure.md +88 -0
- data/.kiro/steering/tech.md +124 -0
- data/.kiro/steering/testing.md +192 -0
- data/.rubocop.yml +103 -0
- data/.yardopts +8 -0
- data/API_DOCUMENTATION.md +919 -0
- data/CHANGELOG.md +131 -0
- data/LICENSE +21 -0
- data/MIGRATION_EXAMPLES.md +740 -0
- data/PERFORMANCE_OPTIMIZATIONS.md +723 -0
- data/README.md +692 -0
- data/Rakefile +27 -0
- data/TASK_10.2_IMPLEMENTATION_SUMMARY.md +164 -0
- data/docs/DUCKDB_SQL_PATTERNS.md +410 -0
- data/docs/TASK_12_VERIFICATION_SUMMARY.md +122 -0
- data/lib/sequel/adapters/duckdb.rb +256 -0
- data/lib/sequel/adapters/shared/duckdb.rb +2349 -0
- data/lib/sequel/duckdb/version.rb +16 -0
- data/lib/sequel/duckdb.rb +43 -0
- data/sig/sequel/duckdb.rbs +6 -0
- metadata +235 -0
data/README.md
ADDED
@@ -0,0 +1,692 @@
|
|
1
|
+
# Sequel::DuckDB
|
2
|
+
|
3
|
+
A Ruby database adapter that enables [Sequel](https://sequel.jeremyevans.net/) to work with [DuckDB](https://duckdb.org/) databases. This gem provides full integration between Sequel's powerful ORM and query building capabilities with DuckDB's high-performance analytical database engine.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **Complete Sequel Integration**: Full compatibility with Sequel's Database and Dataset APIs
|
8
|
+
- **Connection Management**: Support for both file-based and in-memory DuckDB databases
|
9
|
+
- **Schema Introspection**: Automatic discovery of tables, columns, indexes, and constraints
|
10
|
+
- **SQL Generation**: DuckDB-optimized SQL generation for all standard operations
|
11
|
+
- **Data Type Mapping**: Seamless conversion between Ruby and DuckDB data types
|
12
|
+
- **Transaction Support**: Full transaction handling with commit/rollback capabilities
|
13
|
+
- **Error Handling**: Comprehensive error mapping to appropriate Sequel exceptions
|
14
|
+
- **Performance Optimized**: Leverages DuckDB's columnar storage and parallel processing
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'sequel-duckdb'
|
22
|
+
```
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
bundle install
|
28
|
+
```
|
29
|
+
|
30
|
+
Or install it yourself as:
|
31
|
+
|
32
|
+
```bash
|
33
|
+
gem install sequel-duckdb
|
34
|
+
```
|
35
|
+
|
36
|
+
## Quick Start
|
37
|
+
|
38
|
+
### Basic Connection
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
require 'sequel'
|
42
|
+
|
43
|
+
# Connect to an in-memory database
|
44
|
+
db = Sequel.connect('duckdb::memory:')
|
45
|
+
|
46
|
+
# Connect to a file database
|
47
|
+
db = Sequel.connect('duckdb:///path/to/database.duckdb')
|
48
|
+
|
49
|
+
# Alternative connection syntax
|
50
|
+
db = Sequel.connect(
|
51
|
+
adapter: 'duckdb',
|
52
|
+
database: '/path/to/database.duckdb'
|
53
|
+
)
|
54
|
+
```
|
55
|
+
|
56
|
+
### Basic Usage
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
# Create a table
|
60
|
+
db.create_table :users do
|
61
|
+
primary_key :id
|
62
|
+
String :name, null: false
|
63
|
+
String :email
|
64
|
+
Integer :age
|
65
|
+
DateTime :created_at
|
66
|
+
end
|
67
|
+
|
68
|
+
# Insert data
|
69
|
+
users = db[:users]
|
70
|
+
users.insert(name: 'John Doe', email: 'john@example.com', age: 30, created_at: Time.now)
|
71
|
+
users.insert(name: 'Jane Smith', email: 'jane@example.com', age: 25, created_at: Time.now)
|
72
|
+
|
73
|
+
# Query data
|
74
|
+
puts users.where(age: 30).first
|
75
|
+
# => {:id=>1, :name=>"John Doe", :email=>"john@example.com", :age=>30, :created_at=>...}
|
76
|
+
|
77
|
+
puts users.where { age > 25 }.all
|
78
|
+
# => [{:id=>1, :name=>"John Doe", ...}, ...]
|
79
|
+
|
80
|
+
# Update data
|
81
|
+
users.where(name: 'John Doe').update(age: 31)
|
82
|
+
|
83
|
+
# Delete data
|
84
|
+
users.where(age: 25).delete
|
85
|
+
```
|
86
|
+
|
87
|
+
## Development
|
88
|
+
|
89
|
+
After checking out the repo, run `bin/setup` to install dependencies:
|
90
|
+
|
91
|
+
```bash
|
92
|
+
git clone https://github.com/aguynamedryan/sequel-duckdb.git
|
93
|
+
cd sequel-duckdb
|
94
|
+
bin/setup
|
95
|
+
```
|
96
|
+
|
97
|
+
### Running Tests
|
98
|
+
|
99
|
+
The test suite uses Minitest and includes both unit tests (using Sequel's mock database) and integration tests (using real DuckDB databases):
|
100
|
+
|
101
|
+
```bash
|
102
|
+
# Run all tests
|
103
|
+
bundle exec rake test
|
104
|
+
|
105
|
+
# Run specific test file
|
106
|
+
ruby test/database_test.rb
|
107
|
+
|
108
|
+
# Run tests with verbose output
|
109
|
+
ruby test/all.rb -v
|
110
|
+
```
|
111
|
+
|
112
|
+
### Development Console
|
113
|
+
|
114
|
+
You can run `bin/console` for an interactive prompt that will allow you to experiment:
|
115
|
+
|
116
|
+
```bash
|
117
|
+
bin/console
|
118
|
+
```
|
119
|
+
|
120
|
+
This will start an IRB session with the gem loaded and a test database available.
|
121
|
+
|
122
|
+
### Code Quality
|
123
|
+
|
124
|
+
The project uses RuboCop for code style enforcement:
|
125
|
+
|
126
|
+
```bash
|
127
|
+
# Check code style
|
128
|
+
bundle exec rubocop
|
129
|
+
|
130
|
+
# Auto-fix style issues
|
131
|
+
bundle exec rubocop -a
|
132
|
+
```
|
133
|
+
|
134
|
+
### Building and Installing
|
135
|
+
|
136
|
+
To install this gem onto your local machine:
|
137
|
+
|
138
|
+
```bash
|
139
|
+
bundle exec rake install
|
140
|
+
```
|
141
|
+
|
142
|
+
To build the gem:
|
143
|
+
|
144
|
+
```bash
|
145
|
+
bundle exec rake build
|
146
|
+
```
|
147
|
+
|
148
|
+
### Release Process
|
149
|
+
|
150
|
+
To release a new version:
|
151
|
+
|
152
|
+
1. Update the version number in `lib/sequel/duckdb/version.rb`
|
153
|
+
2. Update `CHANGELOG.md` with the new version details
|
154
|
+
3. Run the tests to ensure everything works
|
155
|
+
4. Commit the changes
|
156
|
+
5. Run `bundle exec rake release`
|
157
|
+
|
158
|
+
This will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
159
|
+
|
160
|
+
## Contributing
|
161
|
+
|
162
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/aguynamedryan/sequel-duckdb.
|
163
|
+
|
164
|
+
### Development Guidelines
|
165
|
+
|
166
|
+
1. **Follow TDD**: Write tests before implementing features
|
167
|
+
2. **Code Style**: Follow the existing RuboCop configuration
|
168
|
+
3. **Documentation**: Update README and code documentation for new features
|
169
|
+
4. **Compatibility**: Ensure compatibility with supported Ruby and Sequel versions
|
170
|
+
5. **Performance**: Consider performance implications, especially for analytical workloads
|
171
|
+
|
172
|
+
### Reporting Issues
|
173
|
+
|
174
|
+
When reporting issues, please include:
|
175
|
+
|
176
|
+
- Ruby version
|
177
|
+
- Sequel version
|
178
|
+
- DuckDB version
|
179
|
+
- Operating system
|
180
|
+
- Minimal code example that reproduces the issue
|
181
|
+
- Full error message and stack trace
|
182
|
+
|
183
|
+
## License
|
184
|
+
|
185
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
186
|
+
|
187
|
+
## Acknowledgments
|
188
|
+
|
189
|
+
- [Jeremy Evans](https://github.com/jeremyevans) for creating and maintaining Sequel
|
190
|
+
- The [DuckDB team](https://duckdb.org/docs/api/ruby) for the excellent database engine and Ruby client
|
191
|
+
- Contributors to [sequel-hexspace](https://github.com/hexspace/sequel-hexspace) and other Sequel adapters for implementation patterns
|
192
|
+
## Connection Options
|
193
|
+
|
194
|
+
### Connection Strings
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
# In-memory database (data lost when connection closes)
|
198
|
+
db = Sequel.connect('duckdb::memory:')
|
199
|
+
|
200
|
+
# File database (persistent storage)
|
201
|
+
db = Sequel.connect('duckdb:///absolute/path/to/database.duckdb')
|
202
|
+
db = Sequel.connect('duckdb://relative/path/to/database.duckdb')
|
203
|
+
|
204
|
+
# With connection options
|
205
|
+
db = Sequel.connect('duckdb:///path/to/database.duckdb?readonly=true')
|
206
|
+
```
|
207
|
+
|
208
|
+
### Connection Hash
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
db = Sequel.connect(
|
212
|
+
adapter: 'duckdb',
|
213
|
+
database: '/path/to/database.duckdb',
|
214
|
+
# DuckDB-specific options
|
215
|
+
readonly: false,
|
216
|
+
config: {
|
217
|
+
threads: 4,
|
218
|
+
memory_limit: '1GB'
|
219
|
+
}
|
220
|
+
)
|
221
|
+
```
|
222
|
+
|
223
|
+
## Schema Operations
|
224
|
+
|
225
|
+
### Table Management
|
226
|
+
|
227
|
+
```ruby
|
228
|
+
# Create table with various column types
|
229
|
+
db.create_table :products do
|
230
|
+
primary_key :id
|
231
|
+
String :name, size: 255, null: false
|
232
|
+
Text :description
|
233
|
+
Decimal :price, size: [10, 2]
|
234
|
+
Integer :stock_quantity
|
235
|
+
Boolean :active, default: true
|
236
|
+
Date :release_date
|
237
|
+
DateTime :created_at
|
238
|
+
Time :daily_update_time
|
239
|
+
column :metadata, 'JSON' # DuckDB-specific type
|
240
|
+
end
|
241
|
+
|
242
|
+
# Add columns
|
243
|
+
db.alter_table :products do
|
244
|
+
add_column :category_id, Integer
|
245
|
+
add_index :category_id
|
246
|
+
end
|
247
|
+
|
248
|
+
# Drop table
|
249
|
+
db.drop_table :products
|
250
|
+
```
|
251
|
+
|
252
|
+
### Schema Introspection
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
# List all tables
|
256
|
+
db.tables
|
257
|
+
# => [:users, :products, :orders]
|
258
|
+
|
259
|
+
# Get table schema
|
260
|
+
db.schema(:users)
|
261
|
+
# => [[:id, {:type=>:integer, :db_type=>"INTEGER", :primary_key=>true, ...}],
|
262
|
+
# [:name, {:type=>:string, :db_type=>"VARCHAR", :allow_null=>false, ...}], ...]
|
263
|
+
|
264
|
+
# Check if table exists
|
265
|
+
db.table_exists?(:users)
|
266
|
+
# => true
|
267
|
+
|
268
|
+
# Get indexes
|
269
|
+
db.indexes(:users)
|
270
|
+
# => {:users_name_index => {:columns=>[:name], :unique=>false, :primary=>false}}
|
271
|
+
```
|
272
|
+
|
273
|
+
## Data Types
|
274
|
+
|
275
|
+
### Supported Type Mappings
|
276
|
+
|
277
|
+
| Ruby Type | DuckDB Type | Notes |
|
278
|
+
| -------------------- | --------------- | ----------------------------- |
|
279
|
+
| String | VARCHAR/TEXT | Configurable size |
|
280
|
+
| Integer | INTEGER/BIGINT | Auto-sized based on value |
|
281
|
+
| Float | REAL/DOUBLE | Precision preserved |
|
282
|
+
| BigDecimal | DECIMAL/NUMERIC | Precision and scale supported |
|
283
|
+
| TrueClass/FalseClass | BOOLEAN | Native boolean support |
|
284
|
+
| Date | DATE | Date-only values |
|
285
|
+
| Time/DateTime | TIMESTAMP | Full datetime with timezone |
|
286
|
+
| Time (time-only) | TIME | Time-only values |
|
287
|
+
| String (binary) | BLOB | Binary data storage |
|
288
|
+
|
289
|
+
### Type Conversion Examples
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
# Automatic type conversion
|
293
|
+
users.insert(
|
294
|
+
name: 'Alice', # String -> VARCHAR
|
295
|
+
age: 28, # Integer -> INTEGER
|
296
|
+
salary: BigDecimal('75000.50'), # BigDecimal -> DECIMAL
|
297
|
+
active: true, # Boolean -> BOOLEAN
|
298
|
+
birth_date: Date.new(1995, 5, 15), # Date -> DATE
|
299
|
+
created_at: Time.now, # Time -> TIMESTAMP
|
300
|
+
profile_data: '{"key": "value"}' # String -> JSON (if column defined as JSON)
|
301
|
+
)
|
302
|
+
|
303
|
+
# Retrieved data is automatically converted back to Ruby types
|
304
|
+
user = users.first
|
305
|
+
user[:birth_date].class # => Date
|
306
|
+
user[:created_at].class # => Time
|
307
|
+
user[:active].class # => TrueClass
|
308
|
+
```
|
309
|
+
|
310
|
+
## Query Building
|
311
|
+
|
312
|
+
### Basic Queries
|
313
|
+
|
314
|
+
```ruby
|
315
|
+
users = db[:users]
|
316
|
+
|
317
|
+
# SELECT with conditions
|
318
|
+
users.where(active: true)
|
319
|
+
users.where { age > 25 }
|
320
|
+
users.where(Sequel.like(:name, 'John%'))
|
321
|
+
|
322
|
+
# Ordering and limiting
|
323
|
+
users.order(:name).limit(10)
|
324
|
+
users.order(Sequel.desc(:created_at)).first
|
325
|
+
|
326
|
+
# Aggregation
|
327
|
+
users.count
|
328
|
+
users.avg(:age)
|
329
|
+
users.group(:department).select(:department, Sequel.count(:id).as(:user_count))
|
330
|
+
```
|
331
|
+
|
332
|
+
### Advanced Queries
|
333
|
+
|
334
|
+
```ruby
|
335
|
+
# JOINs
|
336
|
+
db[:users]
|
337
|
+
.join(:orders, user_id: :id)
|
338
|
+
.select(:users__name, :orders__total)
|
339
|
+
.where { orders__total > 100 }
|
340
|
+
|
341
|
+
# Subqueries
|
342
|
+
high_value_users = db[:orders]
|
343
|
+
.group(:user_id)
|
344
|
+
.having { sum(:total) > 1000 }
|
345
|
+
.select(:user_id)
|
346
|
+
|
347
|
+
db[:users].where(id: high_value_users)
|
348
|
+
|
349
|
+
# Window functions (DuckDB-specific optimization)
|
350
|
+
db[:sales]
|
351
|
+
.select(
|
352
|
+
:product_id,
|
353
|
+
:amount,
|
354
|
+
Sequel.function(:row_number).over(partition: :product_id, order: :amount).as(:rank)
|
355
|
+
)
|
356
|
+
|
357
|
+
# Common Table Expressions (CTEs)
|
358
|
+
db.with(:high_spenders,
|
359
|
+
db[:orders].group(:user_id).having { sum(:total) > 1000 }.select(:user_id)
|
360
|
+
).from(:high_spenders)
|
361
|
+
.join(:users, id: :user_id)
|
362
|
+
.select(:users__name)
|
363
|
+
```
|
364
|
+
|
365
|
+
## Transactions
|
366
|
+
|
367
|
+
### Basic Transactions
|
368
|
+
|
369
|
+
```ruby
|
370
|
+
db.transaction do
|
371
|
+
users.insert(name: 'Alice', email: 'alice@example.com')
|
372
|
+
orders.insert(user_id: users.max(:id), total: 100.00)
|
373
|
+
# Automatically commits if no exceptions
|
374
|
+
end
|
375
|
+
|
376
|
+
# Manual rollback
|
377
|
+
db.transaction do
|
378
|
+
users.insert(name: 'Bob', email: 'bob@example.com')
|
379
|
+
raise Sequel::Rollback if some_condition
|
380
|
+
# Transaction will be rolled back
|
381
|
+
end
|
382
|
+
```
|
383
|
+
|
384
|
+
### Error Handling
|
385
|
+
|
386
|
+
```ruby
|
387
|
+
begin
|
388
|
+
db.transaction do
|
389
|
+
# Some database operations
|
390
|
+
users.insert(name: nil) # This will fail due to NOT NULL constraint
|
391
|
+
end
|
392
|
+
rescue Sequel::NotNullConstraintViolation => e
|
393
|
+
puts "Cannot insert user with null name: #{e.message}"
|
394
|
+
rescue Sequel::DatabaseError => e
|
395
|
+
puts "Database error: #{e.message}"
|
396
|
+
end
|
397
|
+
```
|
398
|
+
|
399
|
+
## DuckDB-Specific Features
|
400
|
+
|
401
|
+
### Analytical Queries
|
402
|
+
|
403
|
+
```ruby
|
404
|
+
# DuckDB excels at analytical workloads
|
405
|
+
sales_summary = db[:sales]
|
406
|
+
.select(
|
407
|
+
:product_category,
|
408
|
+
Sequel.function(:sum, :amount).as(:total_sales),
|
409
|
+
Sequel.function(:avg, :amount).as(:avg_sale),
|
410
|
+
Sequel.function(:count, :id).as(:transaction_count)
|
411
|
+
)
|
412
|
+
.group(:product_category)
|
413
|
+
.order(Sequel.desc(:total_sales))
|
414
|
+
|
415
|
+
# Window functions for analytics
|
416
|
+
monthly_trends = db[:sales]
|
417
|
+
.select(
|
418
|
+
:month,
|
419
|
+
:amount,
|
420
|
+
Sequel.function(:lag, :amount, 1).over(order: :month).as(:prev_month),
|
421
|
+
Sequel.function(:sum, :amount).over(order: :month).as(:running_total)
|
422
|
+
)
|
423
|
+
```
|
424
|
+
|
425
|
+
### Performance Optimizations
|
426
|
+
|
427
|
+
```ruby
|
428
|
+
# Bulk inserts (more efficient than individual inserts)
|
429
|
+
users_data = [
|
430
|
+
{name: 'User 1', email: 'user1@example.com'},
|
431
|
+
{name: 'User 2', email: 'user2@example.com'},
|
432
|
+
# ... many more records
|
433
|
+
]
|
434
|
+
|
435
|
+
db[:users].multi_insert(users_data)
|
436
|
+
|
437
|
+
# Use DuckDB's columnar storage advantages
|
438
|
+
# Query only needed columns for better performance
|
439
|
+
db[:large_table].select(:id, :name, :created_at).where(active: true)
|
440
|
+
```
|
441
|
+
|
442
|
+
## Model Integration
|
443
|
+
|
444
|
+
### Sequel::Model Usage
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
class User < Sequel::Model
|
448
|
+
# Sequel automatically introspects the users table schema
|
449
|
+
|
450
|
+
# Associations work normally
|
451
|
+
one_to_many :orders
|
452
|
+
|
453
|
+
# Validations
|
454
|
+
def validate
|
455
|
+
super
|
456
|
+
errors.add(:email, 'must be present') if !email || email.empty?
|
457
|
+
errors.add(:email, 'must be valid') unless email =~ /@/
|
458
|
+
end
|
459
|
+
|
460
|
+
# Custom methods
|
461
|
+
def full_name
|
462
|
+
"#{first_name} #{last_name}"
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
class Order < Sequel::Model
|
467
|
+
many_to_one :user
|
468
|
+
|
469
|
+
def total_with_tax(tax_rate = 0.08)
|
470
|
+
total * (1 + tax_rate)
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
# Usage
|
475
|
+
user = User.create(name: 'John Doe', email: 'john@example.com')
|
476
|
+
order = user.add_order(total: 99.99, status: 'pending')
|
477
|
+
|
478
|
+
# Associations work seamlessly
|
479
|
+
user.orders.where(status: 'completed').sum(:total)
|
480
|
+
```
|
481
|
+
|
482
|
+
## Error Handling
|
483
|
+
|
484
|
+
The adapter maps DuckDB errors to appropriate Sequel exception types:
|
485
|
+
|
486
|
+
```ruby
|
487
|
+
begin
|
488
|
+
# Various operations that might fail
|
489
|
+
db[:users].insert(name: nil) # NOT NULL violation
|
490
|
+
rescue Sequel::NotNullConstraintViolation => e
|
491
|
+
# Handle null constraint violation
|
492
|
+
rescue Sequel::UniqueConstraintViolation => e
|
493
|
+
# Handle unique constraint violation
|
494
|
+
rescue Sequel::ForeignKeyConstraintViolation => e
|
495
|
+
# Handle foreign key violation
|
496
|
+
rescue Sequel::CheckConstraintViolation => e
|
497
|
+
# Handle check constraint violation
|
498
|
+
rescue Sequel::DatabaseConnectionError => e
|
499
|
+
# Handle connection issues
|
500
|
+
rescue Sequel::DatabaseError => e
|
501
|
+
# Handle other database errors
|
502
|
+
end
|
503
|
+
```
|
504
|
+
|
505
|
+
## Troubleshooting
|
506
|
+
|
507
|
+
### Common Issues
|
508
|
+
|
509
|
+
#### Connection Problems
|
510
|
+
|
511
|
+
```ruby
|
512
|
+
# Issue: Cannot connect to database file
|
513
|
+
# Solution: Check file path and permissions
|
514
|
+
begin
|
515
|
+
db = Sequel.connect('duckdb:///path/to/database.duckdb')
|
516
|
+
rescue Sequel::DatabaseConnectionError => e
|
517
|
+
puts "Connection failed: #{e.message}"
|
518
|
+
# Check if directory exists and is writable
|
519
|
+
# Ensure DuckDB gem is properly installed
|
520
|
+
end
|
521
|
+
```
|
522
|
+
|
523
|
+
#### Memory Issues
|
524
|
+
|
525
|
+
```ruby
|
526
|
+
# Issue: Out of memory with large datasets
|
527
|
+
# Solution: Use streaming or limit result sets
|
528
|
+
db[:large_table].limit(1000).each do |row|
|
529
|
+
# Process row by row instead of loading all at once
|
530
|
+
end
|
531
|
+
|
532
|
+
# Or use DuckDB's memory configuration
|
533
|
+
db = Sequel.connect(
|
534
|
+
adapter: 'duckdb',
|
535
|
+
database: ':memory:',
|
536
|
+
config: { memory_limit: '2GB' }
|
537
|
+
)
|
538
|
+
```
|
539
|
+
|
540
|
+
#### Performance Issues
|
541
|
+
|
542
|
+
```ruby
|
543
|
+
# Issue: Slow queries
|
544
|
+
# Solution: Add appropriate indexes
|
545
|
+
db.add_index :users, :email
|
546
|
+
db.add_index :orders, [:user_id, :created_at]
|
547
|
+
|
548
|
+
# Use EXPLAIN to analyze query plans
|
549
|
+
puts db[:users].where(email: 'john@example.com').explain
|
550
|
+
```
|
551
|
+
|
552
|
+
### Debugging
|
553
|
+
|
554
|
+
```ruby
|
555
|
+
# Enable SQL logging to see generated queries
|
556
|
+
require 'logger'
|
557
|
+
db.loggers << Logger.new($stdout)
|
558
|
+
|
559
|
+
# This will now log all SQL queries
|
560
|
+
db[:users].where(active: true).all
|
561
|
+
# Logs: SELECT * FROM users WHERE (active = true)
|
562
|
+
```
|
563
|
+
|
564
|
+
### Version Compatibility
|
565
|
+
|
566
|
+
- **Ruby**: 3.1.0 or higher
|
567
|
+
- **Sequel**: 5.0 or higher
|
568
|
+
- **DuckDB**: 0.8.0 or higher
|
569
|
+
- **ruby-duckdb**: 1.0.0 or higher
|
570
|
+
|
571
|
+
### Getting Help
|
572
|
+
|
573
|
+
- **Documentation**: [Sequel Documentation](https://sequel.jeremyevans.net/documentation.html)
|
574
|
+
- **DuckDB Docs**: [DuckDB Documentation](https://duckdb.org/docs/)
|
575
|
+
- **Issues**: Report bugs on [GitHub Issues](https://github.com/aguynamedryan/sequel-duckdb/issues)
|
576
|
+
- **Discussions**: Join discussions on [GitHub Discussions](https://github.com/aguynamedryan/sequel-duckdb/discussions)
|
577
|
+
|
578
|
+
## Performance Tips
|
579
|
+
|
580
|
+
### Query Optimization
|
581
|
+
|
582
|
+
1. **Select only needed columns**: DuckDB's columnar storage makes this very efficient
|
583
|
+
```ruby
|
584
|
+
# Good
|
585
|
+
db[:users].select(:id, :name).where(active: true)
|
586
|
+
|
587
|
+
# Less efficient
|
588
|
+
db[:users].where(active: true) # Selects all columns
|
589
|
+
```
|
590
|
+
|
591
|
+
2. **Use appropriate indexes**: Especially for frequently queried columns
|
592
|
+
```ruby
|
593
|
+
db.add_index :users, :email
|
594
|
+
db.add_index :orders, [:user_id, :status]
|
595
|
+
```
|
596
|
+
|
597
|
+
3. **Leverage DuckDB's analytical capabilities**: Use window functions and aggregations
|
598
|
+
```ruby
|
599
|
+
# Efficient analytical query
|
600
|
+
db[:sales]
|
601
|
+
.select(
|
602
|
+
:product_id,
|
603
|
+
Sequel.function(:sum, :amount).as(:total),
|
604
|
+
Sequel.function(:rank).over(order: Sequel.desc(:amount)).as(:rank)
|
605
|
+
)
|
606
|
+
.group(:product_id)
|
607
|
+
```
|
608
|
+
|
609
|
+
### Memory Management
|
610
|
+
|
611
|
+
1. **Use streaming for large result sets**:
|
612
|
+
```ruby
|
613
|
+
db[:large_table].paged_each(rows_per_fetch: 1000) do |row|
|
614
|
+
# Process row by row
|
615
|
+
end
|
616
|
+
```
|
617
|
+
|
618
|
+
2. **Configure DuckDB memory limits**:
|
619
|
+
```ruby
|
620
|
+
db = Sequel.connect(
|
621
|
+
adapter: 'duckdb',
|
622
|
+
database: '/path/to/db.duckdb',
|
623
|
+
config: {
|
624
|
+
memory_limit: '4GB',
|
625
|
+
threads: 8
|
626
|
+
}
|
627
|
+
)
|
628
|
+
```
|
629
|
+
|
630
|
+
### Bulk Operations
|
631
|
+
|
632
|
+
1. **Use multi_insert for bulk data loading**:
|
633
|
+
```ruby
|
634
|
+
# Efficient bulk insert
|
635
|
+
data = 1000.times.map { |i| {name: "User #{i}", email: "user#{i}@example.com"} }
|
636
|
+
db[:users].multi_insert(data)
|
637
|
+
```
|
638
|
+
|
639
|
+
2. **Use transactions for multiple operations**:
|
640
|
+
```ruby
|
641
|
+
db.transaction do
|
642
|
+
# Multiple related operations
|
643
|
+
user_id = db[:users].insert(name: 'John', email: 'john@example.com')
|
644
|
+
db[:profiles].insert(user_id: user_id, bio: 'Software developer')
|
645
|
+
db[:preferences].insert(user_id: user_id, theme: 'dark')
|
646
|
+
end
|
647
|
+
```
|
648
|
+
|
649
|
+
## Documentation
|
650
|
+
|
651
|
+
### Complete API Reference
|
652
|
+
|
653
|
+
For comprehensive API documentation including all methods, configuration options, and advanced features, see:
|
654
|
+
|
655
|
+
- **[API_DOCUMENTATION.md](API_DOCUMENTATION.md)** - Complete API reference with examples
|
656
|
+
- **[docs/DUCKDB_SQL_PATTERNS.md](docs/DUCKDB_SQL_PATTERNS.md)** - Detailed SQL generation patterns and syntax
|
657
|
+
|
658
|
+
### SQL Generation Patterns
|
659
|
+
|
660
|
+
The sequel-duckdb adapter generates SQL optimized for DuckDB while maintaining Sequel compatibility. Key patterns include:
|
661
|
+
|
662
|
+
- **LIKE clauses**: Clean syntax without ESCAPE clauses
|
663
|
+
- **ILIKE support**: Converted to UPPER() LIKE UPPER() for case-insensitive matching
|
664
|
+
- **Regular expressions**: Using DuckDB's regexp_matches() function
|
665
|
+
- **Qualified columns**: Standard dot notation (table.column)
|
666
|
+
- **Recursive CTEs**: Automatic WITH RECURSIVE detection
|
667
|
+
- **Proper parentheses**: Consistent expression grouping
|
668
|
+
|
669
|
+
Example SQL patterns:
|
670
|
+
```ruby
|
671
|
+
# LIKE patterns
|
672
|
+
dataset.where(Sequel.like(:name, "%John%"))
|
673
|
+
# SQL: SELECT * FROM users WHERE (name LIKE '%John%')
|
674
|
+
|
675
|
+
# ILIKE patterns (case-insensitive)
|
676
|
+
dataset.where(Sequel.ilike(:name, "%john%"))
|
677
|
+
# SQL: SELECT * FROM users WHERE (UPPER(name) LIKE UPPER('%john%'))
|
678
|
+
|
679
|
+
# Regular expressions
|
680
|
+
dataset.where(name: /^John/)
|
681
|
+
# SQL: SELECT * FROM users WHERE (regexp_matches(name, '^John'))
|
682
|
+
```
|
683
|
+
|
684
|
+
For complete SQL pattern documentation, see [docs/DUCKDB_SQL_PATTERNS.md](docs/DUCKDB_SQL_PATTERNS.md).
|
685
|
+
|
686
|
+
## Contributing
|
687
|
+
|
688
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/aguynamedryan/sequel-duckdb.
|
689
|
+
|
690
|
+
## License
|
691
|
+
|
692
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rubocop/rake_task"
|
5
|
+
require "rake/testtask"
|
6
|
+
|
7
|
+
RuboCop::RakeTask.new
|
8
|
+
|
9
|
+
Rake::TestTask.new do |t|
|
10
|
+
t.libs << "test"
|
11
|
+
# Exclude performance tests by default
|
12
|
+
t.test_files = FileList["test/**/*_test.rb"].exclude("test/performance*_test.rb")
|
13
|
+
end
|
14
|
+
|
15
|
+
# Create a separate task for performance tests
|
16
|
+
Rake::TestTask.new(:test_performance) do |t|
|
17
|
+
t.libs << "test"
|
18
|
+
t.test_files = FileList["test/performance*_test.rb"]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Task to run all tests including performance
|
22
|
+
Rake::TestTask.new(:test_all) do |t|
|
23
|
+
t.libs << "test"
|
24
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
25
|
+
end
|
26
|
+
|
27
|
+
task default: %i[test rubocop]
|