rooq 1.0.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.
data/USAGE.md ADDED
@@ -0,0 +1,850 @@
1
+ # rOOQ Usage Guide
2
+
3
+ ## Table of Contents
4
+
5
+ - [Defining Tables](#defining-tables)
6
+ - [SELECT Queries](#select-queries)
7
+ - [WHERE Conditions](#where-conditions)
8
+ - [JOINs](#joins)
9
+ - [GROUP BY and HAVING](#group-by-and-having)
10
+ - [Aggregate Functions](#aggregate-functions)
11
+ - [Window Functions](#window-functions)
12
+ - [Common Table Expressions (CTEs)](#common-table-expressions-ctes)
13
+ - [Set Operations](#set-operations)
14
+ - [CASE WHEN Expressions](#case-when-expressions)
15
+ - [INSERT Queries](#insert-queries)
16
+ - [UPDATE Queries](#update-queries)
17
+ - [DELETE Queries](#delete-queries)
18
+ - [Subqueries](#subqueries)
19
+ - [Executing Queries](#executing-queries)
20
+ - [Getting SQL and Parameters](#getting-sql-and-parameters)
21
+ - [Using Context](#using-context-recommended)
22
+ - [Using Executor](#using-executor-low-level)
23
+ - [Type Handling](#type-handling)
24
+ - [Result Type Coercion](#result-type-coercion)
25
+ - [Parameter Type Conversion](#parameter-type-conversion)
26
+ - [Query Validation](#query-validation-development-mode)
27
+ - [Code Generation](#code-generation)
28
+ - [Immutability](#immutability)
29
+
30
+ ## Defining Tables
31
+
32
+ ```ruby
33
+ books = Rooq::Table.new(:books) do |t|
34
+ t.field :id, :integer
35
+ t.field :title, :string
36
+ t.field :author_id, :integer
37
+ t.field :published_in, :integer
38
+ t.field :price, :decimal
39
+ end
40
+
41
+ authors = Rooq::Table.new(:authors) do |t|
42
+ t.field :id, :integer
43
+ t.field :name, :string
44
+ end
45
+ ```
46
+
47
+ ## SELECT Queries
48
+
49
+ ### Basic SELECT
50
+
51
+ ```ruby
52
+ # Select specific columns
53
+ query = Rooq::DSL.select(books.TITLE, books.PUBLISHED_IN)
54
+ .from(books)
55
+
56
+ # Select all columns
57
+ query = Rooq::DSL.select(*books.asterisk)
58
+ .from(books)
59
+
60
+ # DISTINCT
61
+ query = Rooq::DSL.select(books.AUTHOR_ID)
62
+ .from(books)
63
+ .distinct
64
+ ```
65
+
66
+ ### Column Aliases
67
+
68
+ ```ruby
69
+ query = Rooq::DSL.select(
70
+ books.TITLE.as(:book_title),
71
+ books.PUBLISHED_IN.as(:year)
72
+ ).from(books)
73
+ ```
74
+
75
+ ### Ordering
76
+
77
+ ```ruby
78
+ # Simple ordering
79
+ query = Rooq::DSL.select(books.TITLE)
80
+ .from(books)
81
+ .order_by(books.TITLE.asc)
82
+
83
+ # Multiple columns
84
+ query = Rooq::DSL.select(books.TITLE)
85
+ .from(books)
86
+ .order_by(books.PUBLISHED_IN.desc, books.TITLE.asc)
87
+
88
+ # NULLS FIRST/LAST
89
+ query = Rooq::DSL.select(books.TITLE)
90
+ .from(books)
91
+ .order_by(books.AUTHOR_ID.asc.nulls_last)
92
+ ```
93
+
94
+ ### LIMIT and OFFSET
95
+
96
+ ```ruby
97
+ query = Rooq::DSL.select(books.TITLE)
98
+ .from(books)
99
+ .limit(10)
100
+ .offset(20)
101
+ ```
102
+
103
+ ### FOR UPDATE (Row Locking)
104
+
105
+ ```ruby
106
+ query = Rooq::DSL.select(books.TITLE)
107
+ .from(books)
108
+ .where(books.ID.eq(1))
109
+ .for_update
110
+ ```
111
+
112
+ ## WHERE Conditions
113
+
114
+ ### Comparison Operators
115
+
116
+ ```ruby
117
+ # Equality
118
+ .where(books.ID.eq(1))
119
+
120
+ # Not equal
121
+ .where(books.ID.ne(1))
122
+
123
+ # Greater than / Less than
124
+ .where(books.PUBLISHED_IN.gt(2010))
125
+ .where(books.PUBLISHED_IN.lt(2020))
126
+
127
+ # Greater/less than or equal
128
+ .where(books.PUBLISHED_IN.gte(2010))
129
+ .where(books.PUBLISHED_IN.lte(2020))
130
+
131
+ # NULL checks
132
+ .where(books.AUTHOR_ID.is_null)
133
+ .where(books.AUTHOR_ID.is_not_null)
134
+
135
+ # Also handles nil values automatically
136
+ .where(books.AUTHOR_ID.eq(nil)) # IS NULL
137
+ .where(books.AUTHOR_ID.ne(nil)) # IS NOT NULL
138
+ ```
139
+
140
+ ### IN / LIKE / BETWEEN
141
+
142
+ ```ruby
143
+ # IN
144
+ .where(books.PUBLISHED_IN.in([2010, 2011, 2012]))
145
+
146
+ # LIKE
147
+ .where(books.TITLE.like("%Ruby%"))
148
+
149
+ # ILIKE (case-insensitive, PostgreSQL)
150
+ .where(books.TITLE.ilike("%ruby%"))
151
+
152
+ # BETWEEN
153
+ .where(books.PUBLISHED_IN.between(2010, 2020))
154
+ ```
155
+
156
+ ### Combining Conditions
157
+
158
+ ```ruby
159
+ # AND
160
+ .where(books.PUBLISHED_IN.gte(2010).and(books.PUBLISHED_IN.lte(2020)))
161
+
162
+ # OR
163
+ .where(books.PUBLISHED_IN.eq(2010).or(books.PUBLISHED_IN.eq(2020)))
164
+
165
+ # Chaining where adds AND
166
+ query = Rooq::DSL.select(books.TITLE)
167
+ .from(books)
168
+ .where(books.PUBLISHED_IN.gte(2010))
169
+ .and_where(books.AUTHOR_ID.eq(1))
170
+
171
+ # or_where for OR conditions
172
+ query = Rooq::DSL.select(books.TITLE)
173
+ .from(books)
174
+ .where(books.PUBLISHED_IN.eq(2010))
175
+ .or_where(books.PUBLISHED_IN.eq(2020))
176
+ ```
177
+
178
+ ## JOINs
179
+
180
+ ### INNER JOIN
181
+
182
+ ```ruby
183
+ query = Rooq::DSL.select(books.TITLE, authors.NAME)
184
+ .from(books)
185
+ .inner_join(authors).on(books.AUTHOR_ID.eq(authors.ID))
186
+ ```
187
+
188
+ ### LEFT/RIGHT/FULL JOIN
189
+
190
+ ```ruby
191
+ # LEFT JOIN
192
+ .left_join(authors).on(books.AUTHOR_ID.eq(authors.ID))
193
+
194
+ # RIGHT JOIN
195
+ .right_join(authors).on(books.AUTHOR_ID.eq(authors.ID))
196
+
197
+ # FULL JOIN
198
+ .full_join(authors).on(books.AUTHOR_ID.eq(authors.ID))
199
+
200
+ # CROSS JOIN
201
+ .cross_join(categories)
202
+ ```
203
+
204
+ ### USING Clause
205
+
206
+ ```ruby
207
+ .inner_join(authors).using(:author_id)
208
+ ```
209
+
210
+ ### Table Aliases
211
+
212
+ ```ruby
213
+ query = Rooq::DSL.select(books.TITLE)
214
+ .from(books, as: :b)
215
+ .inner_join(authors, as: :a).on(books.AUTHOR_ID.eq(authors.ID))
216
+ ```
217
+
218
+ ## GROUP BY and HAVING
219
+
220
+ ```ruby
221
+ query = Rooq::DSL.select(
222
+ books.AUTHOR_ID,
223
+ Rooq::Aggregates.count(books.ID).as(:book_count)
224
+ )
225
+ .from(books)
226
+ .group_by(books.AUTHOR_ID)
227
+ .having(Rooq::Aggregates.count(books.ID).gt(5))
228
+ ```
229
+
230
+ ### Advanced Grouping
231
+
232
+ ```ruby
233
+ # GROUPING SETS
234
+ .group_by(Rooq::DSL::GroupingSets.new(
235
+ [books.AUTHOR_ID],
236
+ [books.PUBLISHED_IN],
237
+ []
238
+ ))
239
+
240
+ # CUBE
241
+ .group_by(Rooq::DSL::Cube.new(books.AUTHOR_ID, books.PUBLISHED_IN))
242
+
243
+ # ROLLUP
244
+ .group_by(Rooq::DSL::Rollup.new(books.AUTHOR_ID, books.PUBLISHED_IN))
245
+ ```
246
+
247
+ ## Aggregate Functions
248
+
249
+ ```ruby
250
+ # COUNT
251
+ Rooq::Aggregates.count # COUNT(*)
252
+ Rooq::Aggregates.count(books.ID) # COUNT(books.id)
253
+ Rooq::Aggregates.count(books.AUTHOR_ID, distinct: true) # COUNT(DISTINCT books.author_id)
254
+
255
+ # SUM, AVG, MIN, MAX
256
+ Rooq::Aggregates.sum(books.PRICE)
257
+ Rooq::Aggregates.avg(books.PRICE)
258
+ Rooq::Aggregates.min(books.PUBLISHED_IN)
259
+ Rooq::Aggregates.max(books.PUBLISHED_IN)
260
+
261
+ # STRING_AGG (PostgreSQL)
262
+ Rooq::Aggregates.string_agg(books.TITLE, ', ')
263
+
264
+ # ARRAY_AGG (PostgreSQL)
265
+ Rooq::Aggregates.array_agg(books.TITLE)
266
+ ```
267
+
268
+ ## Window Functions
269
+
270
+ ```ruby
271
+ # ROW_NUMBER
272
+ Rooq::WindowFunctions.row_number
273
+ .partition_by(books.AUTHOR_ID)
274
+ .order_by(books.PUBLISHED_IN.desc)
275
+ .as(:row_num)
276
+
277
+ # RANK / DENSE_RANK
278
+ Rooq::WindowFunctions.rank
279
+ .order_by(books.PRICE.desc)
280
+
281
+ Rooq::WindowFunctions.dense_rank
282
+ .partition_by(books.AUTHOR_ID)
283
+ .order_by(books.PRICE.desc)
284
+
285
+ # LAG / LEAD
286
+ Rooq::WindowFunctions.lag(books.PRICE, 1)
287
+ .partition_by(books.AUTHOR_ID)
288
+ .order_by(books.PUBLISHED_IN.asc)
289
+
290
+ Rooq::WindowFunctions.lead(books.PRICE, 1, 0)
291
+ .partition_by(books.AUTHOR_ID)
292
+ .order_by(books.PUBLISHED_IN.asc)
293
+
294
+ # FIRST_VALUE / LAST_VALUE
295
+ Rooq::WindowFunctions.first_value(books.TITLE)
296
+ .partition_by(books.AUTHOR_ID)
297
+ .order_by(books.PUBLISHED_IN.asc)
298
+
299
+ # NTH_VALUE
300
+ Rooq::WindowFunctions.nth_value(books.TITLE, 2)
301
+ .partition_by(books.AUTHOR_ID)
302
+ .order_by(books.PUBLISHED_IN.asc)
303
+
304
+ # NTILE
305
+ Rooq::WindowFunctions.ntile(4)
306
+ .order_by(books.PRICE.desc)
307
+ ```
308
+
309
+ ### Window Frame Specifications
310
+
311
+ ```ruby
312
+ Rooq::WindowFunctions.sum(books.PRICE)
313
+ .partition_by(books.AUTHOR_ID)
314
+ .order_by(books.PUBLISHED_IN.asc)
315
+ .rows_between(:unbounded_preceding, :current_row)
316
+
317
+ # Other frame options:
318
+ .rows(:unbounded_preceding)
319
+ .rows(:current_row)
320
+ .rows_between(:current_row, :unbounded_following)
321
+ .rows_between([:preceding, 3], [:following, 3])
322
+
323
+ # RANGE frames
324
+ .range_between(:unbounded_preceding, :current_row)
325
+ ```
326
+
327
+ ## Common Table Expressions (CTEs)
328
+
329
+ ```ruby
330
+ # Simple CTE
331
+ recent_books = Rooq::DSL.select(books.ID, books.TITLE)
332
+ .from(books)
333
+ .where(books.PUBLISHED_IN.gte(2020))
334
+
335
+ query = Rooq::DSL.select(Rooq::Literal.new(:*))
336
+ .from(:recent_books)
337
+ .with(:recent_books, recent_books)
338
+
339
+ # Recursive CTE
340
+ base_query = Rooq::DSL.select(categories.ID, categories.NAME, categories.PARENT_ID)
341
+ .from(categories)
342
+ .where(categories.PARENT_ID.is_null)
343
+
344
+ recursive_query = Rooq::DSL.select(categories.ID, categories.NAME, categories.PARENT_ID)
345
+ .from(categories)
346
+ .inner_join(:category_tree)
347
+ .on(categories.PARENT_ID.eq(Rooq::Field.new(:id, :category_tree, :integer)))
348
+
349
+ query = Rooq::DSL.select(Rooq::Literal.new(:*))
350
+ .from(:category_tree)
351
+ .with(:category_tree, base_query.union(recursive_query), recursive: true)
352
+ ```
353
+
354
+ ## Set Operations
355
+
356
+ ```ruby
357
+ # UNION (removes duplicates)
358
+ query1.union(query2)
359
+
360
+ # UNION ALL (keeps duplicates)
361
+ query1.union(query2, all: true)
362
+
363
+ # INTERSECT
364
+ query1.intersect(query2)
365
+
366
+ # EXCEPT
367
+ query1.except(query2)
368
+
369
+ # Chaining and ordering
370
+ query1.union(query2)
371
+ .union(query3)
372
+ .order_by(books.TITLE.asc)
373
+ .limit(10)
374
+ ```
375
+
376
+ ## CASE WHEN Expressions
377
+
378
+ ```ruby
379
+ price_category = Rooq::CaseExpression.new
380
+ .when(books.PRICE.lt(10), Rooq::Literal.new("cheap"))
381
+ .when(books.PRICE.lt(50), Rooq::Literal.new("moderate"))
382
+ .else(Rooq::Literal.new("expensive"))
383
+ .as(:price_category)
384
+
385
+ query = Rooq::DSL.select(books.TITLE, price_category)
386
+ .from(books)
387
+ ```
388
+
389
+ ## INSERT Queries
390
+
391
+ ```ruby
392
+ # Single row
393
+ query = Rooq::DSL.insert_into(books)
394
+ .columns(:title, :author_id, :published_in)
395
+ .values("The Ruby Way", 1, 2023)
396
+
397
+ # Multiple rows
398
+ query = Rooq::DSL.insert_into(books)
399
+ .columns(:title, :author_id)
400
+ .values("Book 1", 1)
401
+ .values("Book 2", 2)
402
+
403
+ # RETURNING clause
404
+ query = Rooq::DSL.insert_into(books)
405
+ .columns(:title, :author_id)
406
+ .values("New Book", 1)
407
+ .returning(books.ID)
408
+ ```
409
+
410
+ ## UPDATE Queries
411
+
412
+ ```ruby
413
+ query = Rooq::DSL.update(books)
414
+ .set(:title, "Updated Title")
415
+ .set(:published_in, 2024)
416
+ .where(books.ID.eq(1))
417
+
418
+ # RETURNING clause
419
+ query = Rooq::DSL.update(books)
420
+ .set(:price, 29.99)
421
+ .where(books.ID.eq(1))
422
+ .returning(books.ID, books.PRICE)
423
+ ```
424
+
425
+ ## DELETE Queries
426
+
427
+ ```ruby
428
+ query = Rooq::DSL.delete_from(books)
429
+ .where(books.ID.eq(1))
430
+
431
+ # RETURNING clause
432
+ query = Rooq::DSL.delete_from(books)
433
+ .where(books.PUBLISHED_IN.lt(2000))
434
+ .returning(books.ID, books.TITLE)
435
+ ```
436
+
437
+ ## Subqueries
438
+
439
+ ### In FROM Clause
440
+
441
+ ```ruby
442
+ subquery = Rooq::DSL.select(books.AUTHOR_ID, Rooq::Aggregates.count(books.ID).as(:book_count))
443
+ .from(books)
444
+ .group_by(books.AUTHOR_ID)
445
+ .as_subquery(:author_stats)
446
+
447
+ query = Rooq::DSL.select(Rooq::Literal.new(:*))
448
+ .from(subquery)
449
+ .where(Rooq::Field.new(:book_count, :author_stats, :integer).gt(5))
450
+ ```
451
+
452
+ ### In WHERE Clause (IN)
453
+
454
+ ```ruby
455
+ author_ids = Rooq::DSL.select(authors.ID)
456
+ .from(authors)
457
+ .where(authors.NAME.like("%Smith%"))
458
+
459
+ query = Rooq::DSL.select(books.TITLE)
460
+ .from(books)
461
+ .where(books.AUTHOR_ID.in(author_ids))
462
+ ```
463
+
464
+ ### EXISTS / NOT EXISTS
465
+
466
+ ```ruby
467
+ subquery = Rooq::DSL.select(Rooq::Literal.new(1))
468
+ .from(authors)
469
+ .where(authors.ID.eq(books.AUTHOR_ID))
470
+
471
+ # EXISTS
472
+ query = Rooq::DSL.select(books.TITLE)
473
+ .from(books)
474
+ .where(Rooq.exists(subquery))
475
+
476
+ # NOT EXISTS
477
+ query = Rooq::DSL.select(books.TITLE)
478
+ .from(books)
479
+ .where(Rooq.not_exists(subquery))
480
+ ```
481
+
482
+ ## Executing Queries
483
+
484
+ ### Getting SQL and Parameters
485
+
486
+ ```ruby
487
+ # Get SQL and parameters without executing
488
+ result = query.to_sql
489
+ puts result.sql # The SQL string with $1, $2, etc.
490
+ puts result.params # Array of parameter values
491
+ ```
492
+
493
+ ### Using Context (Recommended)
494
+
495
+ Context is the main entry point for executing queries. It manages connections and provides a clean API for query execution.
496
+
497
+ #### Single Connection
498
+
499
+ Use this when you want to manage the connection lifecycle yourself:
500
+
501
+ ```ruby
502
+ require "pg"
503
+ require "rooq"
504
+
505
+ # Connect to database
506
+ connection = PG.connect(dbname: "myapp_development")
507
+
508
+ # Create context from connection
509
+ ctx = Rooq::Context.using(connection)
510
+
511
+ # Define tables (or use generated schema)
512
+ books = Rooq::Table.new(:books) do |t|
513
+ t.field :id, :integer
514
+ t.field :title, :string
515
+ t.field :author_id, :integer
516
+ end
517
+
518
+ # Execute queries
519
+ query = Rooq::DSL.select(books.TITLE, books.AUTHOR_ID)
520
+ .from(books)
521
+ .where(books.ID.eq(1))
522
+
523
+ # Fetch a single row (results use symbol keys)
524
+ row = ctx.fetch_one(query)
525
+ puts row[:title] if row
526
+
527
+ # Fetch all rows
528
+ rows = ctx.fetch_all(
529
+ Rooq::DSL.select(books.TITLE).from(books).limit(10)
530
+ )
531
+ rows.each { |r| puts r[:title] }
532
+
533
+ # Execute without fetching (for INSERT/UPDATE/DELETE)
534
+ ctx.execute(
535
+ Rooq::DSL.insert_into(books)
536
+ .columns(:title, :author_id)
537
+ .values("New Book", 1)
538
+ )
539
+
540
+ # Don't forget to close when done
541
+ connection.close
542
+ ```
543
+
544
+ #### Connection Pool
545
+
546
+ Use this for applications that need to handle multiple concurrent requests:
547
+
548
+ ```ruby
549
+ require "pg"
550
+ require "rooq"
551
+
552
+ # Create a connection pool
553
+ pool = Rooq::Adapters::PostgreSQL::ConnectionPool.new(size: 10, timeout: 5) do
554
+ PG.connect(
555
+ dbname: "myapp_production",
556
+ host: "localhost",
557
+ user: "postgres",
558
+ password: "secret"
559
+ )
560
+ end
561
+
562
+ # Create context from pool
563
+ ctx = Rooq::Context.using_pool(pool)
564
+
565
+ # Connections are automatically acquired and released per query
566
+ books = Schema::BOOKS # Assuming generated schema
567
+ rows = ctx.fetch_all(
568
+ Rooq::DSL.select(books.TITLE).from(books)
569
+ )
570
+
571
+ # Each query gets its own connection from the pool
572
+ # Multiple threads can safely use the same context
573
+ Thread.new do
574
+ ctx.fetch_all(Rooq::DSL.select(books.ID).from(books))
575
+ end
576
+
577
+ # Shutdown pool when application exits
578
+ pool.shutdown
579
+ ```
580
+
581
+ #### Transactions
582
+
583
+ ```ruby
584
+ ctx = Rooq::Context.using(connection)
585
+
586
+ # Transaction commits on success, rolls back on error
587
+ ctx.transaction do
588
+ ctx.execute(
589
+ Rooq::DSL.insert_into(books)
590
+ .columns(:title, :author_id)
591
+ .values("Book 1", 1)
592
+ )
593
+
594
+ ctx.execute(
595
+ Rooq::DSL.update(authors)
596
+ .set(:book_count, Rooq::Literal.new("book_count + 1"))
597
+ .where(authors.ID.eq(1))
598
+ )
599
+ end
600
+
601
+ # If any query fails, all changes are rolled back
602
+ begin
603
+ ctx.transaction do
604
+ ctx.execute(Rooq::DSL.insert_into(books).columns(:title).values("Book"))
605
+ raise "Something went wrong!" # This triggers rollback
606
+ end
607
+ rescue RuntimeError
608
+ puts "Transaction was rolled back"
609
+ end
610
+ ```
611
+
612
+ #### With RETURNING Clause
613
+
614
+ ```ruby
615
+ # INSERT with RETURNING
616
+ query = Rooq::DSL.insert_into(books)
617
+ .columns(:title, :author_id)
618
+ .values("New Book", 1)
619
+ .returning(books.ID, books.TITLE)
620
+
621
+ result = ctx.fetch_one(query)
622
+ puts "Created book ##{result['id']}: #{result['title']}"
623
+
624
+ # UPDATE with RETURNING
625
+ query = Rooq::DSL.update(books)
626
+ .set(:title, "Updated Title")
627
+ .where(books.ID.eq(1))
628
+ .returning(books.ID, books.TITLE)
629
+
630
+ result = ctx.fetch_one(query)
631
+ puts "Updated: #{result['title']}"
632
+
633
+ # DELETE with RETURNING
634
+ query = Rooq::DSL.delete_from(books)
635
+ .where(books.ID.eq(1))
636
+ .returning(books.ID, books.TITLE)
637
+
638
+ deleted = ctx.fetch_one(query)
639
+ puts "Deleted: #{deleted['title']}" if deleted
640
+ ```
641
+
642
+ ### Using Executor (Low-level)
643
+
644
+ For more control over execution, use the Executor class directly:
645
+
646
+ ```ruby
647
+ executor = Rooq::Executor.new(pg_connection)
648
+
649
+ # Execute and get raw PG::Result
650
+ result = executor.execute(query)
651
+
652
+ # Fetch helpers
653
+ row = executor.fetch_one(query) # Single row or nil
654
+ rows = executor.fetch_all(query) # Array of rows
655
+
656
+ # Lifecycle hooks
657
+ executor.on_before_execute do |rendered|
658
+ puts "SQL: #{rendered.sql}"
659
+ puts "Params: #{rendered.params}"
660
+ end
661
+
662
+ executor.on_after_execute do |rendered, result|
663
+ puts "Returned #{result.ntuples} rows"
664
+ end
665
+ ```
666
+
667
+ ## Type Handling
668
+
669
+ ### Result Type Coercion
670
+
671
+ Results automatically convert PostgreSQL types to Ruby types:
672
+
673
+ ```ruby
674
+ # Results use symbol keys
675
+ row = ctx.fetch_one(query)
676
+ row[:title] # String
677
+ row[:id] # Integer (not string)
678
+ row[:created_at] # Time object
679
+ row[:birth_date] # Date object
680
+ row[:tags] # Array (from PostgreSQL array)
681
+ row[:metadata] # Hash (from JSON/JSONB)
682
+ row[:settings] # Hash (from JSONB)
683
+ ```
684
+
685
+ Supported conversions:
686
+ - `json`, `jsonb` → Ruby Hash or Array
687
+ - `integer[]`, `bigint[]` → Array of integers
688
+ - `text[]`, `varchar[]` → Array of strings
689
+ - `timestamp`, `timestamptz` → Time
690
+ - `date` → Date
691
+ - `boolean` → true/false
692
+ - `integer`, `bigint`, `smallint` → Integer
693
+ - `real`, `double precision`, `numeric` → Float
694
+
695
+ ### Parameter Type Conversion
696
+
697
+ Parameters are automatically converted when executing queries:
698
+
699
+ ```ruby
700
+ # Time/Date parameters
701
+ created_after = Time.now - 86400 # 24 hours ago
702
+ query = Rooq::DSL.select(books.TITLE)
703
+ .from(books)
704
+ .where(books.CREATED_AT.gte(created_after))
705
+ ctx.fetch_all(query) # Time converted to ISO 8601
706
+
707
+ # Hash parameters (converted to JSON)
708
+ metadata = { tags: ["ruby", "sql"], priority: "high" }
709
+ query = Rooq::DSL.insert_into(books)
710
+ .columns(:title, :metadata)
711
+ .values("My Book", metadata)
712
+ ctx.execute(query) # Hash converted to JSON string
713
+
714
+ # Array parameters (for array columns)
715
+ tags = ["programming", "ruby"]
716
+ query = Rooq::DSL.insert_into(books)
717
+ .columns(:title, :tags)
718
+ .values("Ruby Guide", tags)
719
+ ctx.execute(query) # Array converted to PostgreSQL array literal
720
+
721
+ # Date parameters
722
+ published = Date.new(2024, 1, 15)
723
+ query = Rooq::DSL.select(books.TITLE)
724
+ .from(books)
725
+ .where(books.PUBLISHED_DATE.eq(published))
726
+ ctx.fetch_all(query) # Date converted to ISO 8601
727
+ ```
728
+
729
+ Supported parameter conversions:
730
+ - `Time`, `DateTime` → ISO 8601 string
731
+ - `Date` → ISO 8601 date string
732
+ - `Hash` → JSON string
733
+ - `Array` of primitives → PostgreSQL array literal (`{1,2,3}`)
734
+ - `Array` of hashes → JSON array string
735
+ - `Symbol` → String
736
+
737
+ ## Query Validation (Development Mode)
738
+
739
+ ```ruby
740
+ # Create a validating executor for development
741
+ validator = Rooq::QueryValidator.new(schema)
742
+ executor = Rooq::ValidatingExecutor.new(pg_connection, validator)
743
+
744
+ # Queries are validated against the schema before execution
745
+ executor.execute(query) # Raises ValidationError if query references invalid tables/columns
746
+ ```
747
+
748
+ ## Code Generation
749
+
750
+ Generate Ruby table definitions from your PostgreSQL database schema.
751
+
752
+ ### Using the CLI (Recommended)
753
+
754
+ ```bash
755
+ # Generate schema to lib/schema.rb (default)
756
+ rooq generate -d myapp_development
757
+
758
+ # Generate with custom namespace (writes to lib/my_app/db.rb)
759
+ rooq generate -d myapp_development -n MyApp::DB
760
+
761
+ # Generate to custom file
762
+ rooq generate -d myapp_development -o db/schema.rb
763
+
764
+ # Generate without Sorbet types
765
+ rooq generate -d myapp_development --no-typed
766
+
767
+ # Print to stdout instead of file
768
+ rooq generate -d myapp_development --stdout
769
+
770
+ # Full connection options
771
+ rooq generate -d myapp -h localhost -p 5432 -U postgres -W secret -s public
772
+ ```
773
+
774
+ ### Using Ruby API
775
+
776
+ ```ruby
777
+ require "pg"
778
+ require "rooq"
779
+
780
+ # Connect to database
781
+ connection = PG.connect(dbname: "myapp_development")
782
+
783
+ # Introspect schema
784
+ introspector = Rooq::Generator::Introspector.new(connection)
785
+ schema_info = introspector.introspect_schema(schema: "public")
786
+
787
+ # Generate code with Sorbet types and custom namespace
788
+ generator = Rooq::Generator::CodeGenerator.new(schema_info, namespace: "MyApp::DB")
789
+ puts generator.generate
790
+
791
+ # Generate code without Sorbet types
792
+ generator = Rooq::Generator::CodeGenerator.new(schema_info, typed: false)
793
+ puts generator.generate
794
+ ```
795
+
796
+ ### Generated Code with Sorbet Types
797
+
798
+ ```ruby
799
+ # typed: strict
800
+ # frozen_string_literal: true
801
+
802
+ require "rooq"
803
+ require "sorbet-runtime"
804
+
805
+ module MyApp::DB
806
+ extend T::Sig
807
+
808
+ USERS = T.let(Rooq::Table.new(:users) do |t|
809
+ t.field :id, :integer
810
+ t.field :name, :string
811
+ t.field :email, :string
812
+ end, Rooq::Table)
813
+
814
+ USER_ACCOUNTS = T.let(Rooq::Table.new(:user_accounts) do |t|
815
+ t.field :id, :integer
816
+ t.field :user_id, :integer
817
+ t.field :account_type, :string
818
+ end, Rooq::Table)
819
+ end
820
+ ```
821
+
822
+ ### Generated Code without Sorbet Types
823
+
824
+ ```ruby
825
+ # frozen_string_literal: true
826
+
827
+ require "rooq"
828
+
829
+ module Schema
830
+ USERS = Rooq::Table.new(:users) do |t|
831
+ t.field :id, :integer
832
+ t.field :name, :string
833
+ t.field :email, :string
834
+ end
835
+ end
836
+ ```
837
+
838
+ ## Immutability
839
+
840
+ All query objects are immutable. Each builder method returns a new query object:
841
+
842
+ ```ruby
843
+ query1 = Rooq::DSL.select(books.TITLE).from(books)
844
+ query2 = query1.where(books.PUBLISHED_IN.eq(2020)) # query1 is unchanged
845
+ query3 = query1.where(books.PUBLISHED_IN.eq(2021)) # Also based on query1
846
+
847
+ query1.to_sql.sql # "SELECT books.title FROM books"
848
+ query2.to_sql.sql # "SELECT books.title FROM books WHERE books.published_in = $1"
849
+ query3.to_sql.sql # "SELECT books.title FROM books WHERE books.published_in = $1"
850
+ ```