diamond-orm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/diamond/api_catalog.rb +676 -0
- data/lib/diamond/ast.rb +397 -0
- data/lib/diamond/changeset.rb +36 -0
- data/lib/diamond/compiler/base.rb +17 -0
- data/lib/diamond/compiler/ddl.rb +94 -0
- data/lib/diamond/compiler/dml.rb +109 -0
- data/lib/diamond/compiler/dql.rb +390 -0
- data/lib/diamond/compiler/registry.rb +45 -0
- data/lib/diamond/cursor.rb +52 -0
- data/lib/diamond/domains/cte.rb +25 -0
- data/lib/diamond/domains/ddl.rb +11 -0
- data/lib/diamond/domains/dml.rb +106 -0
- data/lib/diamond/domains/dql.rb +395 -0
- data/lib/diamond/domains/dynamic_finders.rb +64 -0
- data/lib/diamond/dsl/default.rb +419 -0
- data/lib/diamond/engine.rb +226 -0
- data/lib/diamond/json_string.rb +13 -0
- data/lib/diamond/null_table.rb +10 -0
- data/lib/diamond/operator.rb +31 -0
- data/lib/diamond/operators/like.rb +142 -0
- data/lib/diamond/parser/proxy.rb +469 -0
- data/lib/diamond/parser/registry.rb +74 -0
- data/lib/diamond/parser.rb +571 -0
- data/lib/diamond/query_object.rb +463 -0
- data/lib/diamond/struct_factory.rb +262 -0
- data/lib/diamond/table.rb +32 -0
- data/lib/diamond/version.rb +3 -0
- data/lib/diamond.rb +436 -0
- data/sig/diamond.rbs +693 -0
- metadata +95 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
require_relative 'struct_factory'
|
|
2
|
+
require_relative 'compiler/base'
|
|
3
|
+
require_relative 'cursor'
|
|
4
|
+
|
|
5
|
+
module Diamond
|
|
6
|
+
class QueryObject
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
# Phase 7 query modes. :struct is the default — returns Diamond's
|
|
10
|
+
# frozen Structs. :hash returns native Extralite hash rows. :array
|
|
11
|
+
# returns positional arrays (SoA-friendly, zero allocation). :splat
|
|
12
|
+
# returns a single column's values (the ultimate pluck).
|
|
13
|
+
VALID_MODES = %i[struct hash array splat].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :table, :ast, :mode
|
|
16
|
+
|
|
17
|
+
def initialize(table, ast = [])
|
|
18
|
+
@table = table
|
|
19
|
+
@ast = ast
|
|
20
|
+
@cached_result = nil
|
|
21
|
+
@mode = :struct
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def materialize
|
|
25
|
+
return @cached_result if @cached_result
|
|
26
|
+
|
|
27
|
+
sql, params, transform_spec = Diamond::Compiler::Base.compile(@table, @ast)
|
|
28
|
+
|
|
29
|
+
# eager-loaded queries: hand the row layout to Extralite::Transform
|
|
30
|
+
# so the result is deduplicated and nested per the join graph.
|
|
31
|
+
if transform_spec
|
|
32
|
+
transform = Extralite::Transform.new(transform_spec)
|
|
33
|
+
@cached_result = []
|
|
34
|
+
Diamond.engine.db.query(transform, sql, *params).each do |row|
|
|
35
|
+
@cached_result << Diamond::StructFactory.create_eager(@table, row)
|
|
36
|
+
end
|
|
37
|
+
return @cached_result
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Phase 7: hash/array/splat modes bypass the StructFactory and
|
|
41
|
+
# hand back the raw Extralite rows. Much cheaper on the GC.
|
|
42
|
+
case @mode
|
|
43
|
+
when :hash
|
|
44
|
+
@cached_result = Diamond.engine.db.query_hash(sql, *params)
|
|
45
|
+
return @cached_result
|
|
46
|
+
when :array
|
|
47
|
+
@cached_result = Diamond.engine.db.query_array(sql, *params)
|
|
48
|
+
return @cached_result
|
|
49
|
+
when :splat
|
|
50
|
+
@cached_result = Diamond.engine.db.query_splat(sql, *params)
|
|
51
|
+
return @cached_result
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Phase 7: use prepared statement cache for :struct mode.
|
|
55
|
+
# Array fetch mode: rows arrive positionally, zero Hash
|
|
56
|
+
# allocations per row (see StructFactory.create_from_array).
|
|
57
|
+
stmt = Diamond.engine.prepared(sql, :array)
|
|
58
|
+
begin
|
|
59
|
+
stmt.bind(*params)
|
|
60
|
+
|
|
61
|
+
# union results carry no top-level projection; members come from
|
|
62
|
+
# the left side (sqlite names union output columns after it).
|
|
63
|
+
union_node = @ast.find { |n| n.is_a?(AST::Union) }
|
|
64
|
+
scope_ast = union_node ? union_node.left.ast : @ast
|
|
65
|
+
projection_node = scope_ast.find { |n| n.is_a?(AST::Projection) }
|
|
66
|
+
projected_columns = projection_node&.columns
|
|
67
|
+
|
|
68
|
+
@cached_result = []
|
|
69
|
+
raw_rows = []
|
|
70
|
+
stmt.each { |row| raw_rows << row }
|
|
71
|
+
@cached_result = Diamond::StructFactory.create_many(@table, raw_rows, projected_columns)
|
|
72
|
+
ensure
|
|
73
|
+
# Note: we don't close the stmt here — it's cached for reuse.
|
|
74
|
+
# The stmt will be closed when the engine is frozen (which doesn't
|
|
75
|
+
# happen) or explicitly closed by the user. Extralite manages
|
|
76
|
+
# prepared statement lifecycle.
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
@cached_result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# --- Phase 7 query mode terminals ---
|
|
83
|
+
|
|
84
|
+
def as_hash
|
|
85
|
+
with_mode(:hash)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def as_array
|
|
89
|
+
with_mode(:array)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def as_splat
|
|
93
|
+
with_mode(:splat)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def first(n = 1)
|
|
97
|
+
scope = has_order? ? self : order(resolve_pk!)
|
|
98
|
+
results = scope.limit(n).materialize
|
|
99
|
+
n == 1 ? results.first : results
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# without a block, returns an Enumerator so .lazy and Enumerable chains work.
|
|
103
|
+
# Early-return before compiling/preparing: the enumerator re-enters here
|
|
104
|
+
# with a block on iteration, so doing work now would leak one statement.
|
|
105
|
+
def each(&block)
|
|
106
|
+
return enum_for(:each) unless block_given?
|
|
107
|
+
|
|
108
|
+
compiled_sql, compiled_params, transform_spec = Diamond::Compiler::Base.compile(@table, @ast)
|
|
109
|
+
|
|
110
|
+
if transform_spec
|
|
111
|
+
transform = Extralite::Transform.new(transform_spec)
|
|
112
|
+
Diamond.engine.db.query(transform, compiled_sql, *compiled_params).each do |row|
|
|
113
|
+
yield Diamond::StructFactory.create_eager(@table, row)
|
|
114
|
+
end
|
|
115
|
+
return
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
stmt = Diamond.engine.db.prepare_array(compiled_sql)
|
|
119
|
+
stmt.bind(*compiled_params)
|
|
120
|
+
|
|
121
|
+
union_node = @ast.find { |n| n.is_a?(AST::Union) }
|
|
122
|
+
scope_ast = union_node ? union_node.left.ast : @ast
|
|
123
|
+
projection_node = scope_ast.find { |n| n.is_a?(AST::Projection) }
|
|
124
|
+
projected_columns = projection_node&.columns
|
|
125
|
+
|
|
126
|
+
cursor = Diamond::Cursor.new(@table, stmt, projected_columns)
|
|
127
|
+
|
|
128
|
+
cursor.each(&block)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def last(n = 1)
|
|
132
|
+
scope = has_order? ? self : order([resolve_pk!, :desc])
|
|
133
|
+
results = scope.limit(n).materialize
|
|
134
|
+
n == 1 ? results.first : results.reverse
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# skip the structs entirely - read values straight off the cursor.
|
|
138
|
+
def pluck(*columns)
|
|
139
|
+
raise ArgumentError, "pluck requires at least one column" if columns.empty?
|
|
140
|
+
validate_pluck_columns!(columns)
|
|
141
|
+
rows = pluck_rows(columns)
|
|
142
|
+
if columns.size == 1
|
|
143
|
+
rows.map { |row| row[0] }
|
|
144
|
+
else
|
|
145
|
+
rows.map(&:dup)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# first row's values only: single value for one column, tuple for
|
|
150
|
+
# several, nil on miss. `Users.pick(:name)` ~ `SELECT name ... LIMIT 1`.
|
|
151
|
+
def pick(*columns)
|
|
152
|
+
raise ArgumentError, "pick requires at least one column" if columns.empty?
|
|
153
|
+
validate_pluck_columns!(columns)
|
|
154
|
+
q = projected_query(columns).limit(1)
|
|
155
|
+
sql, params, _transform = Diamond::Compiler::Base.compile(q.table, q.ast)
|
|
156
|
+
row = fetch_first_array_row(sql, params)
|
|
157
|
+
return nil if row.nil?
|
|
158
|
+
columns.size == 1 ? row[0] : row.dup
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# primary-key values. `Users.ids` ~ `SELECT id FROM users`.
|
|
162
|
+
def ids
|
|
163
|
+
pluck(resolve_pk!)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Zero-translation JSON fast path. Pushes serialization into
|
|
167
|
+
# SQLite's JSON1 C engine: the normally-compiled query becomes a
|
|
168
|
+
# subquery whose rows are encoded by json_object and aggregated by
|
|
169
|
+
# json_group_array — Ruby receives ONE finished string. No
|
|
170
|
+
# StructFactory, no per-row Hash, no Ruby JSON encoding at all.
|
|
171
|
+
# Columns default to the table's schema columns; pass an explicit
|
|
172
|
+
# list to project (which also fixes key order). Returns a
|
|
173
|
+
# Diamond::JsonString so Rutile passes it to the Rack body with
|
|
174
|
+
# zero further processing.
|
|
175
|
+
#
|
|
176
|
+
# The inner query compiles through the normal path, so WHERE,
|
|
177
|
+
# JOIN, GROUP BY, ORDER BY, LIMIT, OFFSET, DISTINCT and UNION ALL
|
|
178
|
+
# all keep working — the wrapper only replaces the SELECT list
|
|
179
|
+
# with a single aggregate expression. One-shot query (no prepared
|
|
180
|
+
# cache): a single row crosses the Ruby/C boundary.
|
|
181
|
+
#
|
|
182
|
+
# Keys are validated identifiers (schema membership, same as
|
|
183
|
+
# pluck), so they interpolate safely as single-quoted literals;
|
|
184
|
+
# value refs are bare subquery-output names, mirroring the
|
|
185
|
+
# compiler's own unquoted column rendering. COALESCE keeps the
|
|
186
|
+
# empty set as '[]' on SQLite builds predating native empty-array
|
|
187
|
+
# aggregation.
|
|
188
|
+
#
|
|
189
|
+
# Explicit limits (fail loud): eager joins (object-graph transform
|
|
190
|
+
# is meaningless for a single string) and WITH/CTE chains (WITH
|
|
191
|
+
# cannot lead inside a subquery). Guards run before column
|
|
192
|
+
# validation: a WITH query's table is a NullTable (empty schema),
|
|
193
|
+
# so validation could never pass — the structural error is the
|
|
194
|
+
# meaningful one.
|
|
195
|
+
def to_json_array(*columns)
|
|
196
|
+
if @ast.any? { |n| n.is_a?(AST::Join) && n.eager }
|
|
197
|
+
raise ArgumentError, "to_json_array does not support eager joins"
|
|
198
|
+
end
|
|
199
|
+
if @ast.any? { |n| n.is_a?(AST::With) }
|
|
200
|
+
raise ArgumentError, "to_json_array does not support WITH/CTE chains"
|
|
201
|
+
end
|
|
202
|
+
columns = @table.schema[:columns] if columns.empty?
|
|
203
|
+
columns.each do |c|
|
|
204
|
+
raise Diamond::UnknownColumnError.build(@table.schema, c) unless @table.schema[:columns].include?(c)
|
|
205
|
+
end
|
|
206
|
+
# Union chains compile standalone (sides carry their own shape;
|
|
207
|
+
# appending a Projection would trip compile's "union terminates
|
|
208
|
+
# the chain" rule). The json_object refs resolve against the
|
|
209
|
+
# union output, named after the left side — sides must project
|
|
210
|
+
# those names (SELECT * on one table always qualifies).
|
|
211
|
+
has_union = @ast.any? { |n| n.is_a?(AST::Union) }
|
|
212
|
+
filtered = if has_union
|
|
213
|
+
@ast
|
|
214
|
+
else
|
|
215
|
+
new_nodes = columns.map { |c| AST::Column.new(c) }
|
|
216
|
+
@ast.reject { |n| n.is_a?(AST::Projection) } + [AST::Projection.new(new_nodes)]
|
|
217
|
+
end
|
|
218
|
+
q = Diamond::QueryObject.new(@table, filtered)
|
|
219
|
+
inner_sql, params, _transform = Diamond::Compiler::Base.compile(q.table, q.ast)
|
|
220
|
+
pairs = columns.map { |c| "'#{c}', #{c}" }.join(', ')
|
|
221
|
+
sql = "SELECT COALESCE(json_group_array(json_object(#{pairs})), '[]') FROM (#{inner_sql})"
|
|
222
|
+
Diamond::JsonString.new(Diamond.engine.db.query_single_splat(sql, *params))
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def exists?(filter = nil)
|
|
226
|
+
unless filter.nil?
|
|
227
|
+
return filter.is_a?(Hash) ? where(filter).exists? : find(filter).exists?
|
|
228
|
+
end
|
|
229
|
+
pk = resolve_pk!
|
|
230
|
+
col = AST::Column.new(pk)
|
|
231
|
+
filtered = @ast.reject { |n| n.is_a?(AST::Projection) } + [AST::Projection.new([col])]
|
|
232
|
+
q = Diamond::QueryObject.new(@table, filtered).limit(1)
|
|
233
|
+
sql, params, _transform = Diamond::Compiler::Base.compile(q.table, q.ast)
|
|
234
|
+
stmt = Diamond.engine.db.prepare_array(sql)
|
|
235
|
+
begin
|
|
236
|
+
stmt.bind(*params)
|
|
237
|
+
found = false
|
|
238
|
+
stmt.each { |_row| found = true; break }
|
|
239
|
+
found
|
|
240
|
+
ensure
|
|
241
|
+
begin
|
|
242
|
+
stmt.close unless stmt.closed?
|
|
243
|
+
rescue StandardError
|
|
244
|
+
# ensure must not raise
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# SQL-backed existence checks. With a block (or pattern args for
|
|
250
|
+
# any?) they fall through to Enumerable — same results as today,
|
|
251
|
+
# just without the SQL fast path. NOTE: never bare `super` here.
|
|
252
|
+
# The DSL modules are mixed into QueryObject *after* Enumerable,
|
|
253
|
+
# so `super` would land on the DSL delegate and recurse forever;
|
|
254
|
+
# bind Enumerable explicitly instead.
|
|
255
|
+
def any?(*args, &block)
|
|
256
|
+
return enumerable_fallback(:any?, *args, &block) if block || !args.empty?
|
|
257
|
+
exists?
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def none?(*args, &block)
|
|
261
|
+
return enumerable_fallback(:none?, *args, &block) if block || !args.empty?
|
|
262
|
+
!exists?
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def empty?
|
|
266
|
+
!exists?
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def count(column = nil)
|
|
270
|
+
col = column.nil? ? resolve_pk! : validate_aggregate_column!(column)
|
|
271
|
+
nodes = [AST::Function.new(:count, [AST::Column.new(col)])]
|
|
272
|
+
filtered = @ast.reject { |n| n.is_a?(AST::Projection) } + [AST::Projection.new(nodes)]
|
|
273
|
+
Diamond::QueryObject.new(@table, filtered).materialize.first.public_send(:"count_#{col}")
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Scalar aggregate terminals. Each builds one Function projection
|
|
277
|
+
# (exactly like count) and reads the auto-named member
|
|
278
|
+
# (SUM(age) -> sum_age). Empty scope yields one NULL row, so these
|
|
279
|
+
# return nil — never NoMethodError.
|
|
280
|
+
#
|
|
281
|
+
# Block form (or a non-column arg like an Numeric init) falls through
|
|
282
|
+
# to Enumerable#sum — same pattern as min/max below.
|
|
283
|
+
def sum(column = nil, &block)
|
|
284
|
+
if block
|
|
285
|
+
return column.nil? ? enumerable_fallback(:sum, &block) : enumerable_fallback(:sum, column, &block)
|
|
286
|
+
end
|
|
287
|
+
return enumerable_fallback(:sum) if column.nil?
|
|
288
|
+
return enumerable_fallback(:sum, column) unless column.is_a?(Symbol) || column.is_a?(String)
|
|
289
|
+
|
|
290
|
+
aggregate(:sum, column)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def minimum(column)
|
|
294
|
+
aggregate(:min, column)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def maximum(column)
|
|
298
|
+
aggregate(:max, column)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def average(column)
|
|
302
|
+
aggregate(:avg, column)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
alias avg average
|
|
306
|
+
|
|
307
|
+
# min/max with a column go to SQL; bare min/max, min(n), and
|
|
308
|
+
# block forms keep today's Enumerable behavior (see the super note
|
|
309
|
+
# on any? above for why this binds explicitly).
|
|
310
|
+
def min(column = nil, &block)
|
|
311
|
+
return minimum(column) if !block && !column.nil? && !column.is_a?(Integer)
|
|
312
|
+
column.nil? ? enumerable_fallback(:min, &block) : enumerable_fallback(:min, column, &block)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def max(column = nil, &block)
|
|
316
|
+
return maximum(column) if !block && !column.nil? && !column.is_a?(Integer)
|
|
317
|
+
column.nil? ? enumerable_fallback(:max, &block) : enumerable_fallback(:max, column, &block)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def method_missing(name, *args, &block)
|
|
321
|
+
if @table.schema[:columns].include?(name)
|
|
322
|
+
record = first
|
|
323
|
+
raise Diamond::RecordNotFound, "No record found for AST: #{@ast.inspect}" if record.nil?
|
|
324
|
+
record.public_send(name, *args, &block)
|
|
325
|
+
else
|
|
326
|
+
super
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def respond_to_missing?(name, include_private = false)
|
|
331
|
+
@table.schema[:columns].include?(name) || super
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def inspect
|
|
335
|
+
if @cached_result
|
|
336
|
+
"#<Diamond::QueryObject materialized: #{@cached_result.size} records>"
|
|
337
|
+
else
|
|
338
|
+
"#<Diamond::QueryObject table=#{@table.name} ast=[#{@ast.map(&:class).map(&:name).join(', ')}]>"
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# the SQL this chain compiles to, without running it. returns
|
|
343
|
+
# [sql, params] — params stay separate so you can bind them yourself.
|
|
344
|
+
def to_sql
|
|
345
|
+
sql, params, _transform = Diamond::Compiler::Base.compile(@table, @ast)
|
|
346
|
+
[sql, params]
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# sqlite's query plan for this chain. read-only, runs on the
|
|
350
|
+
# caller's own connection so it works from worker ractors too.
|
|
351
|
+
# returns extralite rows (:selectid, :order, :from, :detail).
|
|
352
|
+
def explain
|
|
353
|
+
sql, params, _transform = Diamond::Compiler::Base.compile(@table, @ast)
|
|
354
|
+
Diamond.engine.db.query("EXPLAIN QUERY PLAN #{sql}", *params)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# the chain's AST as an indented tree. for staring at what a block
|
|
358
|
+
# actually became.
|
|
359
|
+
def ast_tree
|
|
360
|
+
@ast.map { |n| AST.dump(n) }.join("\n")
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
private
|
|
364
|
+
|
|
365
|
+
# Bypass for Enumerable fallbacks. See the note on any?.
|
|
366
|
+
def enumerable_fallback(method_name, *args, &block)
|
|
367
|
+
Enumerable.instance_method(method_name).bind_call(self, *args, &block)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def validate_pluck_columns!(columns)
|
|
371
|
+
columns.each do |c|
|
|
372
|
+
raise Diamond::UnknownColumnError.build(@table.schema, c) unless @table.schema[:columns].include?(c)
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# fresh QueryObject projecting exactly `columns`, replacing any
|
|
377
|
+
# existing projection (same rule pluck has always used).
|
|
378
|
+
def projected_query(columns)
|
|
379
|
+
new_nodes = columns.map { |c| AST::Column.new(c) }
|
|
380
|
+
filtered = @ast.reject { |n| n.is_a?(AST::Projection) } + [AST::Projection.new(new_nodes)]
|
|
381
|
+
Diamond::QueryObject.new(@table, filtered)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# raw positional rows for a column projection (Array mode: row[i]
|
|
385
|
+
# lines up with columns[i]).
|
|
386
|
+
def pluck_rows(columns)
|
|
387
|
+
q = projected_query(columns)
|
|
388
|
+
sql, params, _transform = Diamond::Compiler::Base.compile(q.table, q.ast)
|
|
389
|
+
stmt = Diamond.engine.db.prepare_array(sql)
|
|
390
|
+
begin
|
|
391
|
+
stmt.bind(*params)
|
|
392
|
+
result = []
|
|
393
|
+
# dup: the cursor may reuse its row buffer across iterations
|
|
394
|
+
# (the old pluck body duped for the same reason).
|
|
395
|
+
stmt.each { |row| result << row.dup }
|
|
396
|
+
result
|
|
397
|
+
ensure
|
|
398
|
+
begin
|
|
399
|
+
stmt.close unless stmt.closed?
|
|
400
|
+
rescue StandardError
|
|
401
|
+
# ensure must not raise
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def fetch_first_array_row(sql, params)
|
|
407
|
+
stmt = Diamond.engine.db.prepare_array(sql)
|
|
408
|
+
begin
|
|
409
|
+
stmt.bind(*params)
|
|
410
|
+
found = nil
|
|
411
|
+
stmt.each { |row| found = row; break }
|
|
412
|
+
found
|
|
413
|
+
ensure
|
|
414
|
+
begin
|
|
415
|
+
stmt.close unless stmt.closed?
|
|
416
|
+
rescue StandardError
|
|
417
|
+
# ensure must not raise
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# one Function projection (SUM(age) -> sum_age), scalar back.
|
|
423
|
+
# Empty scope yields a single NULL row, so this returns nil there.
|
|
424
|
+
def aggregate(func, column)
|
|
425
|
+
col = validate_aggregate_column!(column)
|
|
426
|
+
nodes = [AST::Function.new(func, [AST::Column.new(col)])]
|
|
427
|
+
filtered = @ast.reject { |n| n.is_a?(AST::Projection) } + [AST::Projection.new(nodes)]
|
|
428
|
+
row = Diamond::QueryObject.new(@table, filtered).materialize.first
|
|
429
|
+
row&.public_send(:"#{func.to_s.downcase}_#{col}")
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def validate_aggregate_column!(column)
|
|
433
|
+
col = column.to_sym
|
|
434
|
+
unless @table.schema[:columns].include?(col)
|
|
435
|
+
raise Diamond::UnknownColumnError.build(@table.schema, col)
|
|
436
|
+
end
|
|
437
|
+
col
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def with_mode(new_mode)
|
|
441
|
+
raise ArgumentError, "Unknown mode #{new_mode.inspect}" unless VALID_MODES.include?(new_mode)
|
|
442
|
+
q = Diamond::QueryObject.new(@table, @ast.dup)
|
|
443
|
+
q.instance_variable_set(:@mode, new_mode)
|
|
444
|
+
q
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def has_order?
|
|
448
|
+
@ast.any? { |n| n.is_a?(AST::Order) }
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# pk or :id, and it better exist. fail here with a column error
|
|
452
|
+
# instead of letting sqlite complain about COUNT(missing). Also
|
|
453
|
+
# the implicit ORDER BY column for first/last.
|
|
454
|
+
def resolve_pk!
|
|
455
|
+
pk = @table.schema[:primary_key] || :id
|
|
456
|
+
unless @table.schema[:columns].include?(pk)
|
|
457
|
+
raise Diamond::UnknownColumnError.build(@table.schema, pk)
|
|
458
|
+
end
|
|
459
|
+
pk
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
end
|
|
463
|
+
end
|