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.
- checksums.yaml +7 -0
- data/.tool-versions +1 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +33 -0
- data/CLAUDE.md +54 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +116 -0
- data/LICENSE +661 -0
- data/README.md +98 -0
- data/Rakefile +130 -0
- data/USAGE.md +850 -0
- data/exe/rooq +7 -0
- data/lib/rooq/adapters/postgresql.rb +117 -0
- data/lib/rooq/adapters.rb +3 -0
- data/lib/rooq/cli.rb +230 -0
- data/lib/rooq/condition.rb +104 -0
- data/lib/rooq/configuration.rb +56 -0
- data/lib/rooq/connection.rb +131 -0
- data/lib/rooq/context.rb +141 -0
- data/lib/rooq/dialect/base.rb +27 -0
- data/lib/rooq/dialect/postgresql.rb +531 -0
- data/lib/rooq/dialect.rb +9 -0
- data/lib/rooq/dsl/delete_query.rb +37 -0
- data/lib/rooq/dsl/insert_query.rb +43 -0
- data/lib/rooq/dsl/select_query.rb +301 -0
- data/lib/rooq/dsl/update_query.rb +44 -0
- data/lib/rooq/dsl.rb +28 -0
- data/lib/rooq/executor.rb +65 -0
- data/lib/rooq/expression.rb +494 -0
- data/lib/rooq/field.rb +71 -0
- data/lib/rooq/generator/code_generator.rb +91 -0
- data/lib/rooq/generator/introspector.rb +265 -0
- data/lib/rooq/generator.rb +9 -0
- data/lib/rooq/parameter_converter.rb +98 -0
- data/lib/rooq/query_validator.rb +176 -0
- data/lib/rooq/result.rb +248 -0
- data/lib/rooq/schema_validator.rb +56 -0
- data/lib/rooq/table.rb +69 -0
- data/lib/rooq/version.rb +5 -0
- data/lib/rooq.rb +25 -0
- data/rooq.gemspec +35 -0
- data/sorbet/config +4 -0
- metadata +115 -0
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
|
+
```
|