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/lib/rooq/context.rb
ADDED
|
@@ -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
|