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
@@ -0,0 +1,740 @@
|
|
1
|
+
# Sequel Migration Examples for DuckDB
|
2
|
+
|
3
|
+
This document provides comprehensive examples of using Sequel migrations with DuckDB, covering common patterns, best practices, and DuckDB-specific considerations.
|
4
|
+
|
5
|
+
## Basic Migration Structure
|
6
|
+
|
7
|
+
### Creating a Migration
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# db/migrate/001_create_users.rb
|
11
|
+
Sequel.migration do
|
12
|
+
up do
|
13
|
+
create_table(:users) do
|
14
|
+
primary_key :id
|
15
|
+
String :name, null: false, size: 255
|
16
|
+
String :email, unique: true, null: false
|
17
|
+
Integer :age
|
18
|
+
Boolean :active, default: true
|
19
|
+
DateTime :created_at, null: false
|
20
|
+
DateTime :updated_at, null: false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
down do
|
25
|
+
drop_table(:users)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
### Running Migrations
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
# Setup database connection
|
34
|
+
require 'sequel'
|
35
|
+
db = Sequel.connect('duckdb:///path/to/database.duckdb')
|
36
|
+
|
37
|
+
# Run migrations
|
38
|
+
Sequel::Migrator.run(db, 'db/migrate')
|
39
|
+
|
40
|
+
# Run migrations to specific version
|
41
|
+
Sequel::Migrator.run(db, 'db/migrate', target: 5)
|
42
|
+
|
43
|
+
# Check current migration version
|
44
|
+
puts "Current version: #{db[:schema_info].first[:version]}"
|
45
|
+
```
|
46
|
+
|
47
|
+
## Table Operations
|
48
|
+
|
49
|
+
### Creating Tables with Various Column Types
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# db/migrate/002_create_products.rb
|
53
|
+
Sequel.migration do
|
54
|
+
up do
|
55
|
+
create_table(:products) do
|
56
|
+
primary_key :id
|
57
|
+
|
58
|
+
# String types
|
59
|
+
String :name, null: false, size: 255
|
60
|
+
String :sku, size: 50, unique: true
|
61
|
+
Text :description
|
62
|
+
|
63
|
+
# Numeric types
|
64
|
+
Integer :stock_quantity, default: 0
|
65
|
+
Decimal :price, size: [10, 2], null: false
|
66
|
+
Float :weight
|
67
|
+
|
68
|
+
# Date/time types
|
69
|
+
Date :release_date
|
70
|
+
DateTime :created_at, null: false
|
71
|
+
DateTime :updated_at, null: false
|
72
|
+
Time :daily_update_time
|
73
|
+
|
74
|
+
# Boolean
|
75
|
+
Boolean :active, default: true
|
76
|
+
Boolean :featured, default: false
|
77
|
+
|
78
|
+
# JSON (DuckDB-specific)
|
79
|
+
column :metadata, 'JSON'
|
80
|
+
column :tags, 'VARCHAR[]' # Array type
|
81
|
+
|
82
|
+
# Constraints
|
83
|
+
constraint(:positive_price) { price > 0 }
|
84
|
+
constraint(:valid_stock) { stock_quantity >= 0 }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
down do
|
89
|
+
drop_table(:products)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
### Altering Tables
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
# db/migrate/003_add_category_to_products.rb
|
98
|
+
Sequel.migration do
|
99
|
+
up do
|
100
|
+
alter_table(:products) do
|
101
|
+
add_column :category_id, Integer
|
102
|
+
add_column :brand, String, size: 100
|
103
|
+
add_column :discontinued_at, DateTime
|
104
|
+
|
105
|
+
# Add foreign key constraint
|
106
|
+
add_foreign_key [:category_id], :categories, key: [:id]
|
107
|
+
|
108
|
+
# Add index
|
109
|
+
add_index :category_id
|
110
|
+
add_index [:brand, :active]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
down do
|
115
|
+
alter_table(:products) do
|
116
|
+
drop_foreign_key [:category_id]
|
117
|
+
drop_index [:brand, :active]
|
118
|
+
drop_index :category_id
|
119
|
+
drop_column :discontinued_at
|
120
|
+
drop_column :brand
|
121
|
+
drop_column :category_id
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
### Modifying Columns
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
# db/migrate/004_modify_user_columns.rb
|
131
|
+
Sequel.migration do
|
132
|
+
up do
|
133
|
+
alter_table(:users) do
|
134
|
+
# Change column type
|
135
|
+
set_column_type :age, Integer
|
136
|
+
|
137
|
+
# Change column default
|
138
|
+
set_column_default :active, false
|
139
|
+
|
140
|
+
# Add/remove null constraint
|
141
|
+
set_column_allow_null :email, false
|
142
|
+
|
143
|
+
# Rename column
|
144
|
+
rename_column :name, :full_name
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
down do
|
149
|
+
alter_table(:users) do
|
150
|
+
rename_column :full_name, :name
|
151
|
+
set_column_allow_null :email, true
|
152
|
+
set_column_default :active, true
|
153
|
+
set_column_type :age, String
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
## Index Management
|
160
|
+
|
161
|
+
### Creating Indexes
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
# db/migrate/005_add_indexes.rb
|
165
|
+
Sequel.migration do
|
166
|
+
up do
|
167
|
+
# Single column index
|
168
|
+
add_index :users, :email
|
169
|
+
|
170
|
+
# Multi-column index
|
171
|
+
add_index :products, [:category_id, :active]
|
172
|
+
|
173
|
+
# Unique index
|
174
|
+
add_index :products, :sku, unique: true
|
175
|
+
|
176
|
+
# Named index
|
177
|
+
add_index :users, :created_at, name: :idx_users_created_at
|
178
|
+
|
179
|
+
# Partial index (with WHERE clause)
|
180
|
+
add_index :products, :name, where: { active: true }
|
181
|
+
end
|
182
|
+
|
183
|
+
down do
|
184
|
+
drop_index :products, :name, where: { active: true }
|
185
|
+
drop_index :users, :created_at, name: :idx_users_created_at
|
186
|
+
drop_index :products, :sku, unique: true
|
187
|
+
drop_index :products, [:category_id, :active]
|
188
|
+
drop_index :users, :email
|
189
|
+
end
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
## Constraints and Relationships
|
194
|
+
|
195
|
+
### Foreign Key Constraints
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
# db/migrate/006_create_orders_with_relationships.rb
|
199
|
+
Sequel.migration do
|
200
|
+
up do
|
201
|
+
create_table(:categories) do
|
202
|
+
primary_key :id
|
203
|
+
String :name, null: false, unique: true
|
204
|
+
String :description
|
205
|
+
DateTime :created_at, null: false
|
206
|
+
end
|
207
|
+
|
208
|
+
create_table(:orders) do
|
209
|
+
primary_key :id
|
210
|
+
foreign_key :user_id, :users, null: false, on_delete: :cascade
|
211
|
+
|
212
|
+
Decimal :total, size: [10, 2], null: false
|
213
|
+
String :status, default: 'pending'
|
214
|
+
DateTime :created_at, null: false
|
215
|
+
DateTime :updated_at, null: false
|
216
|
+
|
217
|
+
# Composite foreign key example
|
218
|
+
# foreign_key [:user_id, :product_id], :user_products
|
219
|
+
end
|
220
|
+
|
221
|
+
create_table(:order_items) do
|
222
|
+
primary_key :id
|
223
|
+
foreign_key :order_id, :orders, null: false, on_delete: :cascade
|
224
|
+
foreign_key :product_id, :products, null: false
|
225
|
+
|
226
|
+
Integer :quantity, null: false, default: 1
|
227
|
+
Decimal :unit_price, size: [10, 2], null: false
|
228
|
+
|
229
|
+
# Ensure positive values
|
230
|
+
constraint(:positive_quantity) { quantity > 0 }
|
231
|
+
constraint(:positive_price) { unit_price > 0 }
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
down do
|
236
|
+
drop_table(:order_items)
|
237
|
+
drop_table(:orders)
|
238
|
+
drop_table(:categories)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
```
|
242
|
+
|
243
|
+
### Check Constraints
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
# db/migrate/007_add_check_constraints.rb
|
247
|
+
Sequel.migration do
|
248
|
+
up do
|
249
|
+
alter_table(:users) do
|
250
|
+
# Email format validation
|
251
|
+
add_constraint(:valid_email) { email.like('%@%') }
|
252
|
+
|
253
|
+
# Age range validation
|
254
|
+
add_constraint(:valid_age) { (age >= 0) & (age <= 150) }
|
255
|
+
end
|
256
|
+
|
257
|
+
alter_table(:products) do
|
258
|
+
# Price must be positive
|
259
|
+
add_constraint(:positive_price) { price > 0 }
|
260
|
+
|
261
|
+
# Stock quantity must be non-negative
|
262
|
+
add_constraint(:non_negative_stock) { stock_quantity >= 0 }
|
263
|
+
|
264
|
+
# SKU format validation (example pattern)
|
265
|
+
add_constraint(:valid_sku_format) { sku.like('SKU-%') }
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
down do
|
270
|
+
alter_table(:products) do
|
271
|
+
drop_constraint(:valid_sku_format)
|
272
|
+
drop_constraint(:non_negative_stock)
|
273
|
+
drop_constraint(:positive_price)
|
274
|
+
end
|
275
|
+
|
276
|
+
alter_table(:users) do
|
277
|
+
drop_constraint(:valid_age)
|
278
|
+
drop_constraint(:valid_email)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
```
|
283
|
+
|
284
|
+
## DuckDB-Specific Features
|
285
|
+
|
286
|
+
### JSON and Array Columns
|
287
|
+
|
288
|
+
```ruby
|
289
|
+
# db/migrate/008_add_json_and_array_columns.rb
|
290
|
+
Sequel.migration do
|
291
|
+
up do
|
292
|
+
alter_table(:products) do
|
293
|
+
# JSON column for flexible metadata
|
294
|
+
add_column :specifications, 'JSON'
|
295
|
+
|
296
|
+
# Array columns
|
297
|
+
add_column :tags, 'VARCHAR[]'
|
298
|
+
add_column :category_path, 'INTEGER[]'
|
299
|
+
|
300
|
+
# Map column (key-value pairs)
|
301
|
+
add_column :attributes, 'MAP(VARCHAR, VARCHAR)'
|
302
|
+
end
|
303
|
+
|
304
|
+
# Example of populating JSON data
|
305
|
+
run <<~SQL
|
306
|
+
UPDATE products
|
307
|
+
SET specifications = '{"weight": "1.5kg", "dimensions": "10x20x5cm"}'
|
308
|
+
WHERE specifications IS NULL
|
309
|
+
SQL
|
310
|
+
|
311
|
+
# Example of populating array data
|
312
|
+
run <<~SQL
|
313
|
+
UPDATE products
|
314
|
+
SET tags = ARRAY['electronics', 'gadget']
|
315
|
+
WHERE tags IS NULL
|
316
|
+
SQL
|
317
|
+
end
|
318
|
+
|
319
|
+
down do
|
320
|
+
alter_table(:products) do
|
321
|
+
drop_column :attributes
|
322
|
+
drop_column :category_path
|
323
|
+
drop_column :tags
|
324
|
+
drop_column :specifications
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
```
|
329
|
+
|
330
|
+
### Views and Materialized Views
|
331
|
+
|
332
|
+
```ruby
|
333
|
+
# db/migrate/009_create_views.rb
|
334
|
+
Sequel.migration do
|
335
|
+
up do
|
336
|
+
# Create a view for active products with category info
|
337
|
+
run <<~SQL
|
338
|
+
CREATE VIEW active_products_view AS
|
339
|
+
SELECT
|
340
|
+
p.id,
|
341
|
+
p.name,
|
342
|
+
p.price,
|
343
|
+
p.stock_quantity,
|
344
|
+
c.name as category_name
|
345
|
+
FROM products p
|
346
|
+
LEFT JOIN categories c ON p.category_id = c.id
|
347
|
+
WHERE p.active = true
|
348
|
+
SQL
|
349
|
+
|
350
|
+
# Create a view for order summaries
|
351
|
+
run <<~SQL
|
352
|
+
CREATE VIEW order_summaries AS
|
353
|
+
SELECT
|
354
|
+
o.id as order_id,
|
355
|
+
u.full_name as customer_name,
|
356
|
+
o.total,
|
357
|
+
o.status,
|
358
|
+
COUNT(oi.id) as item_count,
|
359
|
+
o.created_at
|
360
|
+
FROM orders o
|
361
|
+
JOIN users u ON o.user_id = u.id
|
362
|
+
LEFT JOIN order_items oi ON o.id = oi.order_id
|
363
|
+
GROUP BY o.id, u.full_name, o.total, o.status, o.created_at
|
364
|
+
SQL
|
365
|
+
end
|
366
|
+
|
367
|
+
down do
|
368
|
+
run "DROP VIEW IF EXISTS order_summaries"
|
369
|
+
run "DROP VIEW IF EXISTS active_products_view"
|
370
|
+
end
|
371
|
+
end
|
372
|
+
```
|
373
|
+
|
374
|
+
### Sequences (Alternative to Auto-increment)
|
375
|
+
|
376
|
+
```ruby
|
377
|
+
# db/migrate/010_create_sequences.rb
|
378
|
+
Sequel.migration do
|
379
|
+
up do
|
380
|
+
# Create custom sequence for order numbers
|
381
|
+
run "CREATE SEQUENCE order_number_seq START 1000"
|
382
|
+
|
383
|
+
alter_table(:orders) do
|
384
|
+
add_column :order_number, String, unique: true
|
385
|
+
end
|
386
|
+
|
387
|
+
# Set default to use sequence
|
388
|
+
run <<~SQL
|
389
|
+
UPDATE orders
|
390
|
+
SET order_number = 'ORD-' || LPAD(nextval('order_number_seq')::TEXT, 6, '0')
|
391
|
+
WHERE order_number IS NULL
|
392
|
+
SQL
|
393
|
+
end
|
394
|
+
|
395
|
+
down do
|
396
|
+
alter_table(:orders) do
|
397
|
+
drop_column :order_number
|
398
|
+
end
|
399
|
+
|
400
|
+
run "DROP SEQUENCE IF EXISTS order_number_seq"
|
401
|
+
end
|
402
|
+
end
|
403
|
+
```
|
404
|
+
|
405
|
+
## Data Migration Patterns
|
406
|
+
|
407
|
+
### Populating Initial Data
|
408
|
+
|
409
|
+
```ruby
|
410
|
+
# db/migrate/011_populate_initial_data.rb
|
411
|
+
Sequel.migration do
|
412
|
+
up do
|
413
|
+
# Insert default categories
|
414
|
+
categories_data = [
|
415
|
+
{ name: 'Electronics', description: 'Electronic devices and gadgets' },
|
416
|
+
{ name: 'Books', description: 'Books and publications' },
|
417
|
+
{ name: 'Clothing', description: 'Apparel and accessories' }
|
418
|
+
]
|
419
|
+
|
420
|
+
categories_data.each do |category|
|
421
|
+
run <<~SQL
|
422
|
+
INSERT INTO categories (name, description, created_at)
|
423
|
+
VALUES ('#{category[:name]}', '#{category[:description]}', NOW())
|
424
|
+
SQL
|
425
|
+
end
|
426
|
+
|
427
|
+
# Or use Sequel's dataset methods
|
428
|
+
# self[:categories].multi_insert(categories_data.map { |c| c.merge(created_at: Time.now) })
|
429
|
+
end
|
430
|
+
|
431
|
+
down do
|
432
|
+
run "DELETE FROM categories WHERE name IN ('Electronics', 'Books', 'Clothing')"
|
433
|
+
end
|
434
|
+
end
|
435
|
+
```
|
436
|
+
|
437
|
+
### Data Transformation
|
438
|
+
|
439
|
+
```ruby
|
440
|
+
# db/migrate/012_transform_user_data.rb
|
441
|
+
Sequel.migration do
|
442
|
+
up do
|
443
|
+
# Split full_name into first_name and last_name
|
444
|
+
alter_table(:users) do
|
445
|
+
add_column :first_name, String, size: 100
|
446
|
+
add_column :last_name, String, size: 100
|
447
|
+
end
|
448
|
+
|
449
|
+
# Transform existing data
|
450
|
+
run <<~SQL
|
451
|
+
UPDATE users
|
452
|
+
SET
|
453
|
+
first_name = SPLIT_PART(full_name, ' ', 1),
|
454
|
+
last_name = CASE
|
455
|
+
WHEN ARRAY_LENGTH(STRING_SPLIT(full_name, ' ')) > 1
|
456
|
+
THEN ARRAY_TO_STRING(ARRAY_SLICE(STRING_SPLIT(full_name, ' '), 2, NULL), ' ')
|
457
|
+
ELSE ''
|
458
|
+
END
|
459
|
+
WHERE full_name IS NOT NULL
|
460
|
+
SQL
|
461
|
+
end
|
462
|
+
|
463
|
+
down do
|
464
|
+
alter_table(:users) do
|
465
|
+
drop_column :last_name
|
466
|
+
drop_column :first_name
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
```
|
471
|
+
|
472
|
+
## Performance Optimization Migrations
|
473
|
+
|
474
|
+
### Adding Indexes for Query Performance
|
475
|
+
|
476
|
+
```ruby
|
477
|
+
# db/migrate/013_optimize_query_performance.rb
|
478
|
+
Sequel.migration do
|
479
|
+
up do
|
480
|
+
# Index for common WHERE clauses
|
481
|
+
add_index :orders, [:status, :created_at]
|
482
|
+
add_index :products, [:active, :category_id, :price]
|
483
|
+
|
484
|
+
# Index for JOIN operations
|
485
|
+
add_index :order_items, [:order_id, :product_id]
|
486
|
+
|
487
|
+
# Index for sorting operations
|
488
|
+
add_index :users, [:created_at, :id] # Composite for pagination
|
489
|
+
|
490
|
+
# Partial indexes for common filtered queries
|
491
|
+
add_index :products, :price, where: { active: true }
|
492
|
+
add_index :orders, :created_at, where: { status: 'completed' }
|
493
|
+
end
|
494
|
+
|
495
|
+
down do
|
496
|
+
drop_index :orders, :created_at, where: { status: 'completed' }
|
497
|
+
drop_index :products, :price, where: { active: true }
|
498
|
+
drop_index :users, [:created_at, :id]
|
499
|
+
drop_index :order_items, [:order_id, :product_id]
|
500
|
+
drop_index :products, [:active, :category_id, :price]
|
501
|
+
drop_index :orders, [:status, :created_at]
|
502
|
+
end
|
503
|
+
end
|
504
|
+
```
|
505
|
+
|
506
|
+
## Migration Best Practices
|
507
|
+
|
508
|
+
### 1. Reversible Migrations
|
509
|
+
|
510
|
+
Always provide both `up` and `down` methods:
|
511
|
+
|
512
|
+
```ruby
|
513
|
+
Sequel.migration do
|
514
|
+
up do
|
515
|
+
# Forward migration
|
516
|
+
create_table(:example) do
|
517
|
+
primary_key :id
|
518
|
+
String :name
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
down do
|
523
|
+
# Reverse migration
|
524
|
+
drop_table(:example)
|
525
|
+
end
|
526
|
+
end
|
527
|
+
```
|
528
|
+
|
529
|
+
### 2. Safe Column Additions
|
530
|
+
|
531
|
+
When adding columns with NOT NULL constraints:
|
532
|
+
|
533
|
+
```ruby
|
534
|
+
Sequel.migration do
|
535
|
+
up do
|
536
|
+
# Step 1: Add column as nullable
|
537
|
+
alter_table(:users) do
|
538
|
+
add_column :phone, String
|
539
|
+
end
|
540
|
+
|
541
|
+
# Step 2: Populate with default values
|
542
|
+
run "UPDATE users SET phone = 'N/A' WHERE phone IS NULL"
|
543
|
+
|
544
|
+
# Step 3: Make it NOT NULL
|
545
|
+
alter_table(:users) do
|
546
|
+
set_column_allow_null :phone, false
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
down do
|
551
|
+
alter_table(:users) do
|
552
|
+
drop_column :phone
|
553
|
+
end
|
554
|
+
end
|
555
|
+
end
|
556
|
+
```
|
557
|
+
|
558
|
+
### 3. Large Data Migrations
|
559
|
+
|
560
|
+
For large datasets, use batched operations:
|
561
|
+
|
562
|
+
```ruby
|
563
|
+
Sequel.migration do
|
564
|
+
up do
|
565
|
+
# Process in batches to avoid memory issues
|
566
|
+
batch_size = 1000
|
567
|
+
offset = 0
|
568
|
+
|
569
|
+
loop do
|
570
|
+
batch_processed = run <<~SQL
|
571
|
+
UPDATE products
|
572
|
+
SET updated_at = NOW()
|
573
|
+
WHERE id IN (
|
574
|
+
SELECT id FROM products
|
575
|
+
WHERE updated_at IS NULL
|
576
|
+
LIMIT #{batch_size} OFFSET #{offset}
|
577
|
+
)
|
578
|
+
SQL
|
579
|
+
|
580
|
+
break if batch_processed == 0
|
581
|
+
offset += batch_size
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
down do
|
586
|
+
# Reverse operation if needed
|
587
|
+
end
|
588
|
+
end
|
589
|
+
```
|
590
|
+
|
591
|
+
### 4. Testing Migrations
|
592
|
+
|
593
|
+
```ruby
|
594
|
+
# test/migration_test.rb
|
595
|
+
require_relative 'spec_helper'
|
596
|
+
|
597
|
+
class MigrationTest < SequelDuckDBTest::TestCase
|
598
|
+
def test_migration_001_creates_users_table
|
599
|
+
# Run specific migration
|
600
|
+
Sequel::Migrator.run(@db, 'db/migrate', target: 1)
|
601
|
+
|
602
|
+
# Verify table exists
|
603
|
+
assert @db.table_exists?(:users)
|
604
|
+
|
605
|
+
# Verify schema
|
606
|
+
schema = @db.schema(:users)
|
607
|
+
assert schema.any? { |col| col[0] == :id && col[1][:primary_key] }
|
608
|
+
assert schema.any? { |col| col[0] == :name && !col[1][:allow_null] }
|
609
|
+
end
|
610
|
+
|
611
|
+
def test_migration_rollback
|
612
|
+
# Run migration
|
613
|
+
Sequel::Migrator.run(@db, 'db/migrate', target: 1)
|
614
|
+
assert @db.table_exists?(:users)
|
615
|
+
|
616
|
+
# Rollback
|
617
|
+
Sequel::Migrator.run(@db, 'db/migrate', target: 0)
|
618
|
+
refute @db.table_exists?(:users)
|
619
|
+
end
|
620
|
+
end
|
621
|
+
```
|
622
|
+
|
623
|
+
## Migration Runner Script
|
624
|
+
|
625
|
+
Create a script to manage migrations:
|
626
|
+
|
627
|
+
```ruby
|
628
|
+
#!/usr/bin/env ruby
|
629
|
+
# bin/migrate
|
630
|
+
|
631
|
+
require 'sequel'
|
632
|
+
require_relative '../lib/sequel/duckdb'
|
633
|
+
|
634
|
+
# Configuration
|
635
|
+
DB_URL = ENV['DATABASE_URL'] || 'duckdb:///db/development.duckdb'
|
636
|
+
MIGRATIONS_DIR = File.expand_path('../db/migrate', __dir__)
|
637
|
+
|
638
|
+
# Connect to database
|
639
|
+
db = Sequel.connect(DB_URL)
|
640
|
+
|
641
|
+
# Parse command line arguments
|
642
|
+
command = ARGV[0]
|
643
|
+
target = ARGV[1]&.to_i
|
644
|
+
|
645
|
+
case command
|
646
|
+
when 'up', nil
|
647
|
+
puts "Running migrations..."
|
648
|
+
if target
|
649
|
+
Sequel::Migrator.run(db, MIGRATIONS_DIR, target: target)
|
650
|
+
puts "Migrated to version #{target}"
|
651
|
+
else
|
652
|
+
Sequel::Migrator.run(db, MIGRATIONS_DIR)
|
653
|
+
puts "Migrated to latest version"
|
654
|
+
end
|
655
|
+
|
656
|
+
when 'down'
|
657
|
+
target ||= 0
|
658
|
+
puts "Rolling back to version #{target}..."
|
659
|
+
Sequel::Migrator.run(db, MIGRATIONS_DIR, target: target)
|
660
|
+
puts "Rolled back to version #{target}"
|
661
|
+
|
662
|
+
when 'version'
|
663
|
+
version = db[:schema_info].first[:version] rescue 0
|
664
|
+
puts "Current migration version: #{version}"
|
665
|
+
|
666
|
+
when 'create'
|
667
|
+
name = ARGV[1]
|
668
|
+
unless name
|
669
|
+
puts "Usage: bin/migrate create migration_name"
|
670
|
+
exit 1
|
671
|
+
end
|
672
|
+
|
673
|
+
# Find next migration number
|
674
|
+
existing = Dir[File.join(MIGRATIONS_DIR, '*.rb')]
|
675
|
+
next_num = existing.map { |f| File.basename(f)[/^\d+/].to_i }.max.to_i + 1
|
676
|
+
|
677
|
+
# Create migration file
|
678
|
+
filename = format('%03d_%s.rb', next_num, name)
|
679
|
+
filepath = File.join(MIGRATIONS_DIR, filename)
|
680
|
+
|
681
|
+
File.write(filepath, <<~RUBY)
|
682
|
+
Sequel.migration do
|
683
|
+
up do
|
684
|
+
# Add your migration code here
|
685
|
+
end
|
686
|
+
|
687
|
+
down do
|
688
|
+
# Add your rollback code here
|
689
|
+
end
|
690
|
+
end
|
691
|
+
RUBY
|
692
|
+
|
693
|
+
puts "Created migration: #{filepath}"
|
694
|
+
|
695
|
+
else
|
696
|
+
puts <<~USAGE
|
697
|
+
Usage: bin/migrate [command] [options]
|
698
|
+
|
699
|
+
Commands:
|
700
|
+
up [version] - Run migrations up to specified version (or latest)
|
701
|
+
down [version] - Roll back to specified version (default: 0)
|
702
|
+
version - Show current migration version
|
703
|
+
create <name> - Create a new migration file
|
704
|
+
|
705
|
+
Examples:
|
706
|
+
bin/migrate # Run all pending migrations
|
707
|
+
bin/migrate up 5 # Migrate to version 5
|
708
|
+
bin/migrate down 3 # Roll back to version 3
|
709
|
+
bin/migrate version # Show current version
|
710
|
+
bin/migrate create add_users # Create new migration
|
711
|
+
USAGE
|
712
|
+
end
|
713
|
+
```
|
714
|
+
|
715
|
+
Make the script executable:
|
716
|
+
|
717
|
+
```bash
|
718
|
+
chmod +x bin/migrate
|
719
|
+
```
|
720
|
+
|
721
|
+
## Usage Examples
|
722
|
+
|
723
|
+
```bash
|
724
|
+
# Run all pending migrations
|
725
|
+
./bin/migrate
|
726
|
+
|
727
|
+
# Migrate to specific version
|
728
|
+
./bin/migrate up 5
|
729
|
+
|
730
|
+
# Roll back to previous version
|
731
|
+
./bin/migrate down 4
|
732
|
+
|
733
|
+
# Check current version
|
734
|
+
./bin/migrate version
|
735
|
+
|
736
|
+
# Create new migration
|
737
|
+
./bin/migrate create add_user_preferences
|
738
|
+
```
|
739
|
+
|
740
|
+
This comprehensive guide covers the most common migration patterns and DuckDB-specific considerations when using Sequel migrations. Remember to always test your migrations thoroughly and keep them reversible for safe deployment practices.
|