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.
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rooq
4
+ # Context is the main entry point for executing queries.
5
+ # It wraps a Configuration and provides methods for query execution.
6
+ #
7
+ # Inspired by jOOQ's DSLContext.
8
+ # @see https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/DSLContext.html
9
+ #
10
+ # @example Using a single connection
11
+ # connection = PG.connect(dbname: 'myapp')
12
+ # ctx = Rooq::Context.using(connection)
13
+ #
14
+ # books = Schema::BOOKS
15
+ # result = ctx.fetch_all(
16
+ # Rooq::DSL.select(books.TITLE, books.AUTHOR)
17
+ # .from(books)
18
+ # .where(books.PUBLISHED_YEAR.gte(2020))
19
+ # )
20
+ # result.each { |row| puts row[:title] } # Symbol keys
21
+ #
22
+ # @example Using a connection pool
23
+ # pool = ConnectionPool.new { PG.connect(dbname: 'myapp') }
24
+ # ctx = Rooq::Context.using_pool(pool)
25
+ #
26
+ # # Connection is automatically acquired and released per query
27
+ # result = ctx.fetch_one(
28
+ # Rooq::DSL.select(books.ID).from(books).where(books.ID.eq(1))
29
+ # )
30
+ #
31
+ # @example Transactions
32
+ # ctx.transaction do
33
+ # ctx.execute(Rooq::DSL.insert_into(books).columns(books.TITLE).values("New Book"))
34
+ # ctx.execute(Rooq::DSL.update(books).set(books.TITLE, "Updated").where(books.ID.eq(1)))
35
+ # end
36
+ class Context
37
+ attr_reader :configuration
38
+
39
+ # Create a context with the given configuration.
40
+ # @param configuration [Configuration] the configuration
41
+ def initialize(configuration)
42
+ @configuration = configuration
43
+ end
44
+
45
+ # Create a context from a single connection.
46
+ # @param connection [Object] a database connection
47
+ # @param dialect [Dialect::Base] the SQL dialect (optional)
48
+ # @return [Context]
49
+ def self.using(connection, dialect: nil)
50
+ new(Configuration.from_connection(connection, dialect: dialect))
51
+ end
52
+
53
+ # Create a context from a connection pool.
54
+ # @param pool [ConnectionPool] a connection pool
55
+ # @param dialect [Dialect::Base] the SQL dialect (optional)
56
+ # @return [Context]
57
+ def self.using_pool(pool, dialect: nil)
58
+ new(Configuration.from_pool(pool, dialect: dialect))
59
+ end
60
+
61
+ # Execute a query and return a Result object.
62
+ # @param query [DSL::SelectQuery, DSL::InsertQuery, DSL::UpdateQuery, DSL::DeleteQuery] the query
63
+ # @return [Result] the result with symbol keys and type coercion
64
+ def execute(query)
65
+ rendered = render_query(query)
66
+ converted_params = parameter_converter.convert_all(rendered.params)
67
+
68
+ raw_result = @configuration.connection_provider.with_connection do |connection|
69
+ connection.exec_params(rendered.sql, converted_params)
70
+ end
71
+
72
+ Result.new(raw_result)
73
+ end
74
+
75
+ # Execute a query and return a single row with symbol keys.
76
+ # @param query [DSL::SelectQuery] the query
77
+ # @return [Hash, nil] the first row or nil if no results
78
+ def fetch_one(query)
79
+ result = execute(query)
80
+ return nil if result.empty?
81
+
82
+ result.first
83
+ end
84
+
85
+ # Execute a query and return all rows as an array with symbol keys.
86
+ # @param query [DSL::SelectQuery] the query
87
+ # @return [Array<Hash>] the rows with symbol keys
88
+ def fetch_all(query)
89
+ execute(query).to_a
90
+ end
91
+
92
+ # Execute a block within a transaction.
93
+ # Commits on success, rolls back on error.
94
+ # @yield the block to execute within the transaction
95
+ # @return [Object] the result of the block
96
+ def transaction(&block)
97
+ @configuration.connection_provider.with_connection do |connection|
98
+ if connection.respond_to?(:transaction)
99
+ connection.transaction(&block)
100
+ else
101
+ begin
102
+ connection.exec("BEGIN")
103
+ result = yield
104
+ connection.exec("COMMIT")
105
+ result
106
+ rescue StandardError
107
+ connection.exec("ROLLBACK")
108
+ raise
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ def parameter_converter
117
+ @parameter_converter ||= ParameterConverter.new
118
+ end
119
+
120
+ def render_query(query)
121
+ dialect = @configuration.dialect
122
+
123
+ case query
124
+ when DSL::SelectQuery
125
+ dialect.render_select(query)
126
+ when DSL::InsertQuery
127
+ dialect.render_insert(query)
128
+ when DSL::UpdateQuery
129
+ dialect.render_update(query)
130
+ when DSL::DeleteQuery
131
+ dialect.render_delete(query)
132
+ when DSL::SetOperation
133
+ dialect.render_set_operation(query)
134
+ when DSL::OrderedSetOperation
135
+ dialect.render_ordered_set_operation(query)
136
+ else
137
+ raise ArgumentError, "Unknown query type: #{query.class}"
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rooq
4
+ module Dialect
5
+ class Base
6
+ def render_select(query)
7
+ raise NotImplementedError
8
+ end
9
+
10
+ def render_insert(query)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def render_update(query)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def render_delete(query)
19
+ raise NotImplementedError
20
+ end
21
+
22
+ def render_condition(condition, params)
23
+ raise NotImplementedError
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,531 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rooq
4
+ module Dialect
5
+ class PostgreSQL < Base
6
+ def render_select(query)
7
+ params = []
8
+ sql_parts = []
9
+
10
+ # CTEs (WITH clause)
11
+ unless query.ctes.empty?
12
+ cte_parts = query.ctes.map { |cte| render_cte(cte, params) }
13
+ recursive = query.ctes.any?(&:recursive) ? "RECURSIVE " : ""
14
+ sql_parts << "WITH #{recursive}#{cte_parts.join(', ')}"
15
+ end
16
+
17
+ # SELECT clause
18
+ distinct = query.distinct_flag ? "DISTINCT " : ""
19
+ fields = render_select_fields(query.selected_fields, params)
20
+ sql_parts << "SELECT #{distinct}#{fields}"
21
+
22
+ # FROM clause
23
+ if query.from_table
24
+ from_sql = render_from_source(query.from_table, params)
25
+ from_sql = "#{from_sql} AS #{query.table_alias}" if query.table_alias
26
+ sql_parts << "FROM #{from_sql}"
27
+ end
28
+
29
+ # JOIN clauses
30
+ query.joins.each do |join|
31
+ sql_parts << render_join(join, params)
32
+ end
33
+
34
+ # WHERE clause
35
+ if query.conditions
36
+ condition_sql = render_condition(query.conditions, params)
37
+ sql_parts << "WHERE #{condition_sql}"
38
+ end
39
+
40
+ # GROUP BY clause
41
+ unless query.group_by_fields.empty?
42
+ group_parts = query.group_by_fields.map { |f| render_group_by_item(f, params) }
43
+ sql_parts << "GROUP BY #{group_parts.join(', ')}"
44
+ end
45
+
46
+ # HAVING clause
47
+ if query.having_condition
48
+ having_sql = render_condition(query.having_condition, params)
49
+ sql_parts << "HAVING #{having_sql}"
50
+ end
51
+
52
+ # ORDER BY clause
53
+ unless query.order_specs.empty?
54
+ order_parts = query.order_specs.map { |spec| render_order_spec(spec, params) }
55
+ sql_parts << "ORDER BY #{order_parts.join(', ')}"
56
+ end
57
+
58
+ # LIMIT clause
59
+ sql_parts << "LIMIT #{query.limit_value}" if query.limit_value
60
+
61
+ # OFFSET clause
62
+ sql_parts << "OFFSET #{query.offset_value}" if query.offset_value
63
+
64
+ # FOR UPDATE
65
+ sql_parts << "FOR UPDATE" if query.for_update_flag
66
+
67
+ RenderedQuery.new(sql_parts.join(" "), params)
68
+ end
69
+
70
+ def render_insert(query)
71
+ params = []
72
+ sql_parts = []
73
+
74
+ sql_parts << "INSERT INTO #{render_table_name(query.table)}"
75
+
76
+ # Columns
77
+ columns = query.column_list.map { |col| render_field_name(col) }
78
+ sql_parts << "(#{columns.join(', ')})"
79
+
80
+ # Values
81
+ value_groups = query.insert_values.map do |values|
82
+ placeholders = values.map do |value|
83
+ params << value
84
+ "$#{params.length}"
85
+ end
86
+ "(#{placeholders.join(', ')})"
87
+ end
88
+ sql_parts << "VALUES #{value_groups.join(', ')}"
89
+
90
+ # RETURNING clause
91
+ unless query.returning_fields.empty?
92
+ fields = render_select_fields(query.returning_fields, params)
93
+ sql_parts << "RETURNING #{fields}"
94
+ end
95
+
96
+ RenderedQuery.new(sql_parts.join(" "), params)
97
+ end
98
+
99
+ def render_update(query)
100
+ params = []
101
+ sql_parts = []
102
+
103
+ sql_parts << "UPDATE #{render_table_name(query.table)}"
104
+
105
+ # SET clause
106
+ set_parts = query.set_values.map do |field, value|
107
+ params << value
108
+ "#{render_field_name(field)} = $#{params.length}"
109
+ end
110
+ sql_parts << "SET #{set_parts.join(', ')}"
111
+
112
+ # WHERE clause
113
+ if query.conditions
114
+ condition_sql = render_condition(query.conditions, params)
115
+ sql_parts << "WHERE #{condition_sql}"
116
+ end
117
+
118
+ # RETURNING clause
119
+ unless query.returning_fields.empty?
120
+ fields = render_select_fields(query.returning_fields, params)
121
+ sql_parts << "RETURNING #{fields}"
122
+ end
123
+
124
+ RenderedQuery.new(sql_parts.join(" "), params)
125
+ end
126
+
127
+ def render_delete(query)
128
+ params = []
129
+ sql_parts = []
130
+
131
+ sql_parts << "DELETE FROM #{render_table_name(query.table)}"
132
+
133
+ # WHERE clause
134
+ if query.conditions
135
+ condition_sql = render_condition(query.conditions, params)
136
+ sql_parts << "WHERE #{condition_sql}"
137
+ end
138
+
139
+ # RETURNING clause
140
+ unless query.returning_fields.empty?
141
+ fields = render_select_fields(query.returning_fields, params)
142
+ sql_parts << "RETURNING #{fields}"
143
+ end
144
+
145
+ RenderedQuery.new(sql_parts.join(" "), params)
146
+ end
147
+
148
+ def render_set_operation(op)
149
+ params = []
150
+ sql = render_set_operation_sql(op, params)
151
+ RenderedQuery.new(sql, params)
152
+ end
153
+
154
+ def render_ordered_set_operation(op)
155
+ params = []
156
+ sql_parts = []
157
+
158
+ sql_parts << "(#{render_set_operation_sql(op.set_operation, params)})"
159
+
160
+ unless op.order_specs.empty?
161
+ order_parts = op.order_specs.map { |spec| render_order_spec(spec, params) }
162
+ sql_parts << "ORDER BY #{order_parts.join(', ')}"
163
+ end
164
+
165
+ sql_parts << "LIMIT #{op.limit_value}" if op.limit_value
166
+ sql_parts << "OFFSET #{op.offset_value}" if op.offset_value
167
+
168
+ RenderedQuery.new(sql_parts.join(" "), params)
169
+ end
170
+
171
+ def render_condition(condition, params)
172
+ case condition
173
+ when Condition
174
+ render_simple_condition(condition, params)
175
+ when CombinedCondition
176
+ render_combined_condition(condition, params)
177
+ when ExistsCondition
178
+ render_exists_condition(condition, params)
179
+ else
180
+ raise ArgumentError, "Unknown condition type: #{condition.class}"
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def render_set_operation_sql(op, params)
187
+ left_sql = case op.left
188
+ when DSL::SetOperation
189
+ render_set_operation_sql(op.left, params)
190
+ else
191
+ render_select(op.left).tap { |r| params.concat(r.params) }.sql
192
+ end
193
+
194
+ # Track offset for renumbering right query's placeholders
195
+ param_offset = params.length
196
+
197
+ right_result = case op.right
198
+ when DSL::SetOperation
199
+ render_set_operation_sql(op.right, params)
200
+ else
201
+ render_select(op.right).tap { |r| params.concat(r.params) }
202
+ end
203
+
204
+ # Renumber placeholders in right query if needed
205
+ right_sql = case right_result
206
+ when RenderedQuery
207
+ renumber_placeholders(right_result.sql, param_offset)
208
+ else
209
+ right_result
210
+ end
211
+
212
+ operator = op.operator.to_s.upcase
213
+ operator = "#{operator} ALL" if op.all
214
+
215
+ "(#{left_sql}) #{operator} (#{right_sql})"
216
+ end
217
+
218
+ def renumber_placeholders(sql, offset)
219
+ return sql if offset == 0
220
+ sql.gsub(/\$(\d+)/) { |_| "$#{Regexp.last_match(1).to_i + offset}" }
221
+ end
222
+
223
+ def render_cte(cte, params)
224
+ subquery_result = render_select(cte.query)
225
+ params.concat(subquery_result.params)
226
+ "#{cte.name} AS (#{subquery_result.sql})"
227
+ end
228
+
229
+ def render_select_fields(fields, params)
230
+ fields.map { |f| render_select_field(f, params) }.join(", ")
231
+ end
232
+
233
+ def render_select_field(field, params)
234
+ case field
235
+ when AliasedExpression
236
+ "#{render_expression(field.expression, params)} AS #{field.alias_name}"
237
+ else
238
+ render_expression(field, params)
239
+ end
240
+ end
241
+
242
+ def render_expression(expr, params)
243
+ case expr
244
+ when Field
245
+ expr.qualified_name
246
+ when Literal
247
+ if expr.value == :*
248
+ "*"
249
+ else
250
+ params << expr.value
251
+ "$#{params.length}"
252
+ end
253
+ when FunctionCall
254
+ render_function_call(expr, params)
255
+ when WindowFunction
256
+ render_window_function(expr, params)
257
+ when CaseExpression
258
+ render_case_expression(expr, params)
259
+ when CastExpression
260
+ render_cast_expression(expr, params)
261
+ when ArithmeticExpression
262
+ render_arithmetic_expression(expr, params)
263
+ when DSL::Subquery
264
+ "(#{render_select(expr.query).tap { |r| params.concat(r.params) }.sql})"
265
+ when Symbol
266
+ expr.to_s
267
+ else
268
+ expr.to_s
269
+ end
270
+ end
271
+
272
+ def render_function_call(func, params)
273
+ args = func.arguments.map { |arg| render_expression(arg, params) }
274
+ distinct = func.distinct ? "DISTINCT " : ""
275
+ "#{func.name.to_s.upcase}(#{distinct}#{args.join(', ')})"
276
+ end
277
+
278
+ def render_window_function(wf, params)
279
+ func_sql = render_expression(wf.function, params)
280
+ over_parts = []
281
+
282
+ unless wf.partition_by.empty?
283
+ partition_exprs = wf.partition_by.map { |e| render_expression(e, params) }
284
+ over_parts << "PARTITION BY #{partition_exprs.join(', ')}"
285
+ end
286
+
287
+ unless wf.order_by.empty?
288
+ order_exprs = wf.order_by.map { |e| render_order_spec(e, params) }
289
+ over_parts << "ORDER BY #{order_exprs.join(', ')}"
290
+ end
291
+
292
+ if wf.frame
293
+ over_parts << render_window_frame(wf.frame)
294
+ end
295
+
296
+ "#{func_sql} OVER (#{over_parts.join(' ')})"
297
+ end
298
+
299
+ def render_window_frame(frame)
300
+ type = frame.type.to_s.upcase
301
+ start_bound = render_frame_bound(frame.start_bound)
302
+
303
+ if frame.end_bound
304
+ end_bound = render_frame_bound(frame.end_bound)
305
+ "#{type} BETWEEN #{start_bound} AND #{end_bound}"
306
+ else
307
+ "#{type} #{start_bound}"
308
+ end
309
+ end
310
+
311
+ def render_frame_bound(bound)
312
+ case bound
313
+ when :unbounded_preceding
314
+ "UNBOUNDED PRECEDING"
315
+ when :current_row
316
+ "CURRENT ROW"
317
+ when :unbounded_following
318
+ "UNBOUNDED FOLLOWING"
319
+ when Array
320
+ direction, n = bound
321
+ "#{n} #{direction.to_s.upcase}"
322
+ else
323
+ bound.to_s
324
+ end
325
+ end
326
+
327
+ def render_case_expression(expr, params)
328
+ parts = ["CASE"]
329
+
330
+ expr.cases.each do |condition, result|
331
+ cond_sql = render_condition(condition, params)
332
+ result_sql = render_expression(result, params)
333
+ parts << "WHEN #{cond_sql} THEN #{result_sql}"
334
+ end
335
+
336
+ if expr.else_result
337
+ parts << "ELSE #{render_expression(expr.else_result, params)}"
338
+ end
339
+
340
+ parts << "END"
341
+ parts.join(" ")
342
+ end
343
+
344
+ def render_cast_expression(expr, params)
345
+ inner = render_expression(expr.expression, params)
346
+ "CAST(#{inner} AS #{expr.target_type})"
347
+ end
348
+
349
+ def render_arithmetic_expression(expr, params)
350
+ left = render_expression(expr.left, params)
351
+ right = render_expression(expr.right, params)
352
+ "(#{left} #{expr.operator} #{right})"
353
+ end
354
+
355
+ def render_from_source(source, params)
356
+ case source
357
+ when Table
358
+ source.name.to_s
359
+ when DSL::Subquery
360
+ "(#{render_select(source.query).tap { |r| params.concat(r.params) }.sql}) AS #{source.alias_name}"
361
+ when Symbol
362
+ source.to_s
363
+ else
364
+ source.to_s
365
+ end
366
+ end
367
+
368
+ def render_field_name(field)
369
+ case field
370
+ when Field
371
+ field.name.to_s
372
+ when Symbol
373
+ field.to_s
374
+ else
375
+ field.to_s
376
+ end
377
+ end
378
+
379
+ def render_table_name(table)
380
+ case table
381
+ when Table
382
+ table.name.to_s
383
+ when Symbol
384
+ table.to_s
385
+ else
386
+ table.to_s
387
+ end
388
+ end
389
+
390
+ def render_join(join, params)
391
+ join_type = case join.type
392
+ when :inner then "INNER JOIN"
393
+ when :left then "LEFT JOIN"
394
+ when :right then "RIGHT JOIN"
395
+ when :full then "FULL JOIN"
396
+ when :cross then "CROSS JOIN"
397
+ else raise ArgumentError, "Unknown join type: #{join.type}"
398
+ end
399
+
400
+ table_sql = render_table_name(join.table)
401
+ table_sql = "#{table_sql} AS #{join.table_alias}" if join.table_alias
402
+
403
+ if join.using_columns
404
+ columns = join.using_columns.map { |c| render_field_name(c) }
405
+ "#{join_type} #{table_sql} USING (#{columns.join(', ')})"
406
+ elsif join.condition
407
+ condition_sql = render_condition(join.condition, params)
408
+ "#{join_type} #{table_sql} ON #{condition_sql}"
409
+ else
410
+ join_type + " " + table_sql
411
+ end
412
+ end
413
+
414
+ def render_group_by_item(item, params)
415
+ case item
416
+ when DSL::GroupingSets
417
+ sets = item.sets.map { |s| "(#{s.map { |f| render_expression(f, params) }.join(', ')})" }
418
+ "GROUPING SETS (#{sets.join(', ')})"
419
+ when DSL::Cube
420
+ fields = item.fields.map { |f| render_expression(f, params) }
421
+ "CUBE (#{fields.join(', ')})"
422
+ when DSL::Rollup
423
+ fields = item.fields.map { |f| render_expression(f, params) }
424
+ "ROLLUP (#{fields.join(', ')})"
425
+ else
426
+ render_expression(item, params)
427
+ end
428
+ end
429
+
430
+ def render_order_spec(spec, params)
431
+ expr_sql = render_expression(spec.expression, params)
432
+ direction = spec.direction == :desc ? "DESC" : "ASC"
433
+ result = "#{expr_sql} #{direction}"
434
+
435
+ case spec.nulls
436
+ when :first
437
+ result += " NULLS FIRST"
438
+ when :last
439
+ result += " NULLS LAST"
440
+ end
441
+
442
+ result
443
+ end
444
+
445
+ def render_simple_condition(condition, params)
446
+ expr_sql = render_expression(condition.expression, params)
447
+
448
+ case condition.operator
449
+ when :eq
450
+ if condition.value.nil?
451
+ "#{expr_sql} IS NULL"
452
+ else
453
+ "#{expr_sql} = #{render_condition_value(condition.value, params)}"
454
+ end
455
+ when :ne
456
+ if condition.value.nil?
457
+ "#{expr_sql} IS NOT NULL"
458
+ else
459
+ "#{expr_sql} <> #{render_condition_value(condition.value, params)}"
460
+ end
461
+ when :gt
462
+ "#{expr_sql} > #{render_condition_value(condition.value, params)}"
463
+ when :lt
464
+ "#{expr_sql} < #{render_condition_value(condition.value, params)}"
465
+ when :gte
466
+ "#{expr_sql} >= #{render_condition_value(condition.value, params)}"
467
+ when :lte
468
+ "#{expr_sql} <= #{render_condition_value(condition.value, params)}"
469
+ when :in
470
+ if condition.value.is_a?(DSL::SelectQuery)
471
+ subquery = render_select(condition.value)
472
+ params.concat(subquery.params)
473
+ "#{expr_sql} IN (#{subquery.sql})"
474
+ else
475
+ placeholders = condition.value.map do |v|
476
+ params << v
477
+ "$#{params.length}"
478
+ end
479
+ "#{expr_sql} IN (#{placeholders.join(', ')})"
480
+ end
481
+ when :like
482
+ "#{expr_sql} LIKE #{render_condition_value(condition.value, params)}"
483
+ when :ilike
484
+ "#{expr_sql} ILIKE #{render_condition_value(condition.value, params)}"
485
+ when :between
486
+ min_sql = render_condition_value(condition.value[0], params)
487
+ max_sql = render_condition_value(condition.value[1], params)
488
+ "#{expr_sql} BETWEEN #{min_sql} AND #{max_sql}"
489
+ when :is_null
490
+ "#{expr_sql} IS NULL"
491
+ when :is_not_null
492
+ "#{expr_sql} IS NOT NULL"
493
+ else
494
+ raise ArgumentError, "Unknown operator: #{condition.operator}"
495
+ end
496
+ end
497
+
498
+ def render_combined_condition(condition, params)
499
+ parts = condition.conditions.map { |c| render_condition(c, params) }
500
+ connector = condition.operator == :and ? " AND " : " OR "
501
+ "(#{parts.join(connector)})"
502
+ end
503
+
504
+ def render_exists_condition(condition, params)
505
+ subquery = render_select(condition.subquery)
506
+ params.concat(subquery.params)
507
+ prefix = condition.negated ? "NOT " : ""
508
+ "#{prefix}EXISTS (#{subquery.sql})"
509
+ end
510
+
511
+ def render_condition_value(value, params)
512
+ if value.is_a?(Expression)
513
+ render_expression(value, params)
514
+ else
515
+ params << value
516
+ "$#{params.length}"
517
+ end
518
+ end
519
+ end
520
+
521
+ class RenderedQuery
522
+ attr_reader :sql, :params
523
+
524
+ def initialize(sql, params)
525
+ @sql = sql.freeze
526
+ @params = params.freeze
527
+ freeze
528
+ end
529
+ end
530
+ end
531
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dialect/base"
4
+ require_relative "dialect/postgresql"
5
+
6
+ module Rooq
7
+ module Dialect
8
+ end
9
+ end