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,395 @@
|
|
|
1
|
+
module Diamond
|
|
2
|
+
module Domains
|
|
3
|
+
require_relative 'dml'
|
|
4
|
+
require_relative 'ddl'
|
|
5
|
+
require_relative 'cte'
|
|
6
|
+
require_relative 'dynamic_finders'
|
|
7
|
+
|
|
8
|
+
module DQL
|
|
9
|
+
# how each chainable node reconciles with the nodes already on the
|
|
10
|
+
# chain. builders validate args and construct nodes; _chain applies
|
|
11
|
+
# the strategy. new AST node types declare one line here (unknown
|
|
12
|
+
# classes accumulate).
|
|
13
|
+
RECONCILE = {
|
|
14
|
+
AST::Where => :accumulate,
|
|
15
|
+
AST::Join => :accumulate,
|
|
16
|
+
AST::With => :accumulate,
|
|
17
|
+
AST::Order => :merge,
|
|
18
|
+
AST::Limit => :replace,
|
|
19
|
+
AST::Offset => :replace,
|
|
20
|
+
AST::GroupBy => :replace,
|
|
21
|
+
AST::Having => :replace,
|
|
22
|
+
AST::Projection => :once,
|
|
23
|
+
AST::Distinct => :replace
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# the one operation every chain call funnels through. wraps a bare
|
|
27
|
+
# Table into a QueryObject, then reconciles the node into the ast.
|
|
28
|
+
# `.or` passes strategy: :fold_or explicitly — the single escape
|
|
29
|
+
# hatch, since folding rewrites history instead of appending.
|
|
30
|
+
def _chain(node, strategy: nil)
|
|
31
|
+
strategy ||= RECONCILE.fetch(node.class, :accumulate)
|
|
32
|
+
table = self.is_a?(Diamond::Table) ? self : @table
|
|
33
|
+
base = self.is_a?(Diamond::Table) ? [] : @ast
|
|
34
|
+
q = Diamond::QueryObject.new(table, DQL.apply_strategy(strategy, base, node))
|
|
35
|
+
# Phase 7: carry the current mode across chain calls (so .first /
|
|
36
|
+
# .last / .limit / etc. don't reset .as_hash to .struct).
|
|
37
|
+
if self.is_a?(Diamond::QueryObject)
|
|
38
|
+
q.instance_variable_set(:@mode, @mode)
|
|
39
|
+
end
|
|
40
|
+
q
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.apply_strategy(strategy, ast, node)
|
|
44
|
+
case strategy
|
|
45
|
+
when :accumulate
|
|
46
|
+
ast + [node]
|
|
47
|
+
when :replace
|
|
48
|
+
ast.reject { |n| n.instance_of?(node.class) } + [node]
|
|
49
|
+
when :merge # Order: concat specs into a single node
|
|
50
|
+
existing = ast.find { |n| n.is_a?(AST::Order) }
|
|
51
|
+
merged = AST::Order.new((existing ? existing.specs : []) + node.specs)
|
|
52
|
+
ast.reject { |n| n.is_a?(AST::Order) } + [merged]
|
|
53
|
+
when :once # Projection: second one is a usage error
|
|
54
|
+
raise "derive() called twice; use it once on each chain" if ast.any? { |n| n.is_a?(AST::Projection) }
|
|
55
|
+
|
|
56
|
+
ast + [node]
|
|
57
|
+
when :fold_or # .or: merge into the last Where, else append fresh
|
|
58
|
+
idx = ast.rindex { |n| n.is_a?(AST::Where) }
|
|
59
|
+
if idx
|
|
60
|
+
combined = AST::Or.new(ast[idx].condition, node.condition)
|
|
61
|
+
duped = ast.dup
|
|
62
|
+
duped[idx] = AST::Where.new(combined)
|
|
63
|
+
duped
|
|
64
|
+
else
|
|
65
|
+
ast + [node]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def _build_where(&block)
|
|
71
|
+
condition = Parser.parse_block(block, _schema_for_dsl, _scope_for_dsl)
|
|
72
|
+
raise "Where block must return an AST condition" unless condition.is_a?(AST::Node)
|
|
73
|
+
_chain(AST::Where.new(condition))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def _build_or_where(&block)
|
|
77
|
+
condition = Parser.parse_block(block, _schema_for_dsl, _scope_for_dsl)
|
|
78
|
+
raise "Or block must return an AST condition" unless condition.is_a?(AST::Node)
|
|
79
|
+
_chain(AST::Where.new(condition), strategy: :fold_or)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def _build_where_node(condition_node)
|
|
83
|
+
raise "Where node must be an AST::Node" unless condition_node.is_a?(AST::Node)
|
|
84
|
+
_chain(AST::Where.new(condition_node))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Operators allowed as `{ op: value }` hashes inside where-hash
|
|
88
|
+
# values. Anything else raises with this list in the message.
|
|
89
|
+
HASH_OPERATORS = %i[not gt gte lt lte in nin like].freeze
|
|
90
|
+
|
|
91
|
+
def dispatch_where_arg(arg)
|
|
92
|
+
case arg
|
|
93
|
+
when Hash then _build_hash_where(arg)
|
|
94
|
+
when Array then _build_or_chain(arg)
|
|
95
|
+
else raise ArgumentError, "where expects a Hash, Array of Hashes, or a block, got #{arg.class}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Single hash: AND across columns.
|
|
100
|
+
def _build_hash_where(hash)
|
|
101
|
+
raise ArgumentError, "where hash must not be empty" if hash.empty?
|
|
102
|
+
_build_where_node(combine_and(hash.map { |col, val| hash_condition(col, val) }))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Array of hashes: OR across hashes, AND within each hash.
|
|
106
|
+
def _build_or_chain(hashes)
|
|
107
|
+
raise ArgumentError, "where array must not be empty" if hashes.empty?
|
|
108
|
+
unless hashes.all? { |h| h.is_a?(Hash) }
|
|
109
|
+
raise ArgumentError, "where array must contain only Hashes"
|
|
110
|
+
end
|
|
111
|
+
node = combine_and(hashes.first.map { |col, val| hash_condition(col, val) })
|
|
112
|
+
hashes.drop(1).each do |h|
|
|
113
|
+
node = AST::Or.new(node, combine_and(h.map { |col, val| hash_condition(col, val) }))
|
|
114
|
+
end
|
|
115
|
+
_build_where_node(node)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def combine_and(conds)
|
|
119
|
+
raise ArgumentError, "where hash must not be empty" if conds.empty?
|
|
120
|
+
conds.reduce { |acc, c| AST::And.new(acc, c) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# One column => one AST condition node. Values take runtime data
|
|
124
|
+
# directly (no parsing), so locals, method returns, and constants
|
|
125
|
+
# all work here — this is the value path blocks can't provide.
|
|
126
|
+
def hash_condition(col, val)
|
|
127
|
+
col = col.to_sym
|
|
128
|
+
Diamond::Parser.validate_column!(col, _schema_for_dsl)
|
|
129
|
+
column = AST::Column.new(col)
|
|
130
|
+
case val
|
|
131
|
+
when Hash
|
|
132
|
+
unknown = val.keys.map(&:to_sym) - HASH_OPERATORS
|
|
133
|
+
unless unknown.empty?
|
|
134
|
+
raise ArgumentError, "unknown where operator(s) #{unknown.inspect} on #{col}; allowed: #{HASH_OPERATORS.inspect}"
|
|
135
|
+
end
|
|
136
|
+
combine_and(val.map { |op, v| hash_operator(column, col, op.to_sym, v) })
|
|
137
|
+
when Range
|
|
138
|
+
if val.begin.nil? || val.end.nil?
|
|
139
|
+
raise ArgumentError, "where range on #{col} must have both bounds (no endless/beginless ranges)"
|
|
140
|
+
end
|
|
141
|
+
AST::Between.new(column, AST::Literal.new(val.begin), AST::Literal.new(val.end))
|
|
142
|
+
when Array
|
|
143
|
+
# Empty arrays compile to 1=0 natively (same as where_in) —
|
|
144
|
+
# no special case needed.
|
|
145
|
+
AST::In.new(column, val.map { |v| AST::Literal.new(v) })
|
|
146
|
+
when nil
|
|
147
|
+
AST::IsNull.new(column)
|
|
148
|
+
else
|
|
149
|
+
AST::Equality.new(column, AST::Literal.new(val))
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def hash_operator(column, col, op, v)
|
|
154
|
+
case op
|
|
155
|
+
when :not
|
|
156
|
+
if v.nil?
|
|
157
|
+
AST::IsNotNull.new(column)
|
|
158
|
+
elsif v.is_a?(Array)
|
|
159
|
+
AST::NotIn.new(column, v.map { |e| AST::Literal.new(e) })
|
|
160
|
+
else
|
|
161
|
+
AST::NotEqual.new(column, AST::Literal.new(v))
|
|
162
|
+
end
|
|
163
|
+
when :gt then AST::GreaterThan.new(column, AST::Literal.new(v))
|
|
164
|
+
when :gte then AST::GreaterEqual.new(column, AST::Literal.new(v))
|
|
165
|
+
when :lt then AST::LessThan.new(column, AST::Literal.new(v))
|
|
166
|
+
when :lte then AST::LessEqual.new(column, AST::Literal.new(v))
|
|
167
|
+
when :in then AST::In.new(column, Array(v).map { |e| AST::Literal.new(e) })
|
|
168
|
+
when :nin then AST::NotIn.new(column, Array(v).map { |e| AST::Literal.new(e) })
|
|
169
|
+
when :like then AST::Like.new(column, AST::Literal.new(v))
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def _build_projection(*args, &block)
|
|
174
|
+
nodes = if args.empty? && block_given?
|
|
175
|
+
Parser.parse_derive(block, _schema_for_dsl)
|
|
176
|
+
else
|
|
177
|
+
args.map do |a|
|
|
178
|
+
if a.is_a?(Symbol)
|
|
179
|
+
unless _schema_for_dsl[:columns].include?(a)
|
|
180
|
+
raise Diamond::UnknownColumnError.build(_schema_for_dsl, a)
|
|
181
|
+
end
|
|
182
|
+
AST::Column.new(a)
|
|
183
|
+
elsif a.is_a?(Array) && a.size == 2
|
|
184
|
+
_qualified_pair_column(a[0].to_sym, a[1].to_sym)
|
|
185
|
+
else
|
|
186
|
+
a
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
_chain(AST::Projection.new(nodes))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def _build_join(table_name, type, on, eager: false, single: nil, as: nil)
|
|
194
|
+
if on.nil?
|
|
195
|
+
on = _resolve_join_keys(table_name)
|
|
196
|
+
end
|
|
197
|
+
single = _detect_single_join(table_name) if single.nil?
|
|
198
|
+
_chain(AST::Join.new(table_name, type, on, eager: eager, single: single, as: as))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# `.includes(:posts)` is sugar for `.join(:posts, eager: true)`.
|
|
202
|
+
# supports a single table or a list. each eagerly-loaded child becomes
|
|
203
|
+
# a nested struct array on the parent.
|
|
204
|
+
# Auto-detects cardinality from FKs: belongs_to -> single, has_many -> collection.
|
|
205
|
+
# `as:` renames the struct member (only meaningful for a single table).
|
|
206
|
+
def _build_includes(*tables, **opts)
|
|
207
|
+
single = opts.fetch(:single, nil)
|
|
208
|
+
as = opts.fetch(:as, nil)
|
|
209
|
+
if as && tables.size > 1
|
|
210
|
+
raise ArgumentError, "includes with `as:` only supports a single table, got #{tables.size}"
|
|
211
|
+
end
|
|
212
|
+
result = self
|
|
213
|
+
tables.each do |table_name|
|
|
214
|
+
result = result._build_join(table_name, :left, nil, eager: true, single: single, as: as)
|
|
215
|
+
end
|
|
216
|
+
result
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def _build_order(*args, **kwargs)
|
|
220
|
+
_chain(AST::Order.new(_order_pairs(*args, **kwargs)))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Replace any existing ordering (order merges). Bare call with
|
|
224
|
+
# no pairs clears ordering entirely.
|
|
225
|
+
def _build_reorder(*args, **kwargs)
|
|
226
|
+
pairs = args.empty? && kwargs.empty? ? [] : _order_pairs(*args, **kwargs)
|
|
227
|
+
_chain(AST::Order.new(pairs), strategy: :replace)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Pure pair parsing shared by order (merge) and reorder
|
|
231
|
+
# (replace). Same validation, same errors.
|
|
232
|
+
def _order_pairs(*args, **kwargs)
|
|
233
|
+
pairs = []
|
|
234
|
+
args.each do |a|
|
|
235
|
+
qualified = _qualified_sort_pair(a)
|
|
236
|
+
if qualified
|
|
237
|
+
pairs << qualified
|
|
238
|
+
next
|
|
239
|
+
end
|
|
240
|
+
sym, dir =
|
|
241
|
+
if a.is_a?(Array) && a.size == 2 && a[1].is_a?(Symbol)
|
|
242
|
+
[a[0].to_sym, a[1]]
|
|
243
|
+
else
|
|
244
|
+
[a.to_sym, :asc]
|
|
245
|
+
end
|
|
246
|
+
raise Diamond::UnknownColumnError.build(_schema_for_dsl, sym) unless _schema_for_dsl[:columns].include?(sym)
|
|
247
|
+
unless %i[asc desc].include?(dir)
|
|
248
|
+
raise ArgumentError, "direction must be :asc or :desc, got #{dir.inspect}"
|
|
249
|
+
end
|
|
250
|
+
pairs << [sym, dir]
|
|
251
|
+
end
|
|
252
|
+
kwargs.each do |col, direction|
|
|
253
|
+
sym = col.to_sym
|
|
254
|
+
raise Diamond::UnknownColumnError.build(_schema_for_dsl, sym) unless _schema_for_dsl[:columns].include?(sym)
|
|
255
|
+
d = direction.to_s.downcase.to_sym
|
|
256
|
+
unless %i[asc desc].include?(d)
|
|
257
|
+
raise ArgumentError, "direction must be :asc or :desc, got #{direction.inspect}"
|
|
258
|
+
end
|
|
259
|
+
pairs << [sym, d]
|
|
260
|
+
end
|
|
261
|
+
raise ArgumentError, "order requires at least one column" if pairs.empty?
|
|
262
|
+
|
|
263
|
+
pairs
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def _build_limit(n)
|
|
267
|
+
raise ArgumentError, "limit must be Integer >= 0, got #{n.inspect}" unless n.is_a?(Integer) && n >= 0
|
|
268
|
+
_chain(AST::Limit.new(n))
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def _build_offset(n)
|
|
272
|
+
raise ArgumentError, "offset must be Integer >= 0, got #{n.inspect}" unless n.is_a?(Integer) && n >= 0
|
|
273
|
+
_chain(AST::Offset.new(n))
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def _build_group(columns)
|
|
277
|
+
validated = columns.map do |c|
|
|
278
|
+
if c.is_a?(Array)
|
|
279
|
+
unless c.size == 2
|
|
280
|
+
raise ArgumentError, "group takes columns or [table, column] pairs, got #{c.inspect}"
|
|
281
|
+
end
|
|
282
|
+
_qualified_pair_column(c[0].to_sym, c[1].to_sym)
|
|
283
|
+
else
|
|
284
|
+
sym = c.to_sym
|
|
285
|
+
raise Diamond::UnknownColumnError.build(_schema_for_dsl, sym) unless _schema_for_dsl[:columns].include?(sym)
|
|
286
|
+
sym
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
_chain(AST::GroupBy.new(validated))
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def _build_having(&block)
|
|
293
|
+
condition = Parser.parse_block(block, _schema_for_dsl, _scope_for_dsl)
|
|
294
|
+
raise "Having block must return an AST condition" unless condition.is_a?(AST::Node)
|
|
295
|
+
_chain(AST::Having.new(condition))
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# --- Context Hooks (Used by the DSL modules) ---
|
|
299
|
+
def _schema_for_dsl
|
|
300
|
+
self.is_a?(Diamond::Table) ? @schema : @table.schema
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# tables the current chain can filter on: the base table plus every
|
|
304
|
+
# joined table, each mapped to its schema. lets `tags.tag` in a
|
|
305
|
+
# block resolve to a qualified column. unknown join targets are
|
|
306
|
+
# skipped (their own errors surface at compile time).
|
|
307
|
+
def _scope_for_dsl
|
|
308
|
+
base = self.is_a?(Diamond::Table) ? @name : @table.name
|
|
309
|
+
scope = { base => _schema_for_dsl }
|
|
310
|
+
joins = self.is_a?(Diamond::Table) ? [] : @ast.select { |n| n.is_a?(AST::Join) }
|
|
311
|
+
joins.each do |j|
|
|
312
|
+
sch = Diamond.engine.schema_cache[j.table_name]
|
|
313
|
+
scope[j.table_name] = sch if sch
|
|
314
|
+
end
|
|
315
|
+
scope
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
private
|
|
319
|
+
|
|
320
|
+
# [table, col] or [table, col, dir] in order/group/derive args.
|
|
321
|
+
# returns [AST::Column(table:), dir] or nil when `a` isn't a
|
|
322
|
+
# qualified shape (caller falls back to the plain path). bare
|
|
323
|
+
# [col, :asc/:desc] pairs keep their old meaning.
|
|
324
|
+
def _qualified_sort_pair(a)
|
|
325
|
+
return nil unless a.is_a?(Array) && (a.size == 2 || a.size == 3)
|
|
326
|
+
return nil unless a[1].is_a?(Symbol) && !%i[asc desc].include?(a[1])
|
|
327
|
+
|
|
328
|
+
dir = a.size == 3 ? a[2] : :asc
|
|
329
|
+
unless %i[asc desc].include?(dir)
|
|
330
|
+
raise ArgumentError, "direction must be :asc or :desc, got #{dir.inspect}"
|
|
331
|
+
end
|
|
332
|
+
[_qualified_pair_column(a[0].to_sym, a[1].to_sym), dir]
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# validate a [table, column] pair against the chain scope and build
|
|
336
|
+
# a qualified Column. join-first rule mirrors where-blocks.
|
|
337
|
+
def _qualified_pair_column(t, c)
|
|
338
|
+
scope = _scope_for_dsl
|
|
339
|
+
if scope.key?(t)
|
|
340
|
+
sch = scope[t]
|
|
341
|
+
raise Diamond::UnknownColumnError.build(sch, c) unless sch[:columns].include?(c)
|
|
342
|
+
|
|
343
|
+
AST::Column.new(c, table: t)
|
|
344
|
+
elsif Diamond.engine.schema_cache.key?(t)
|
|
345
|
+
raise ArgumentError,
|
|
346
|
+
"using '#{t}.#{c}' needs `.join(:#{t})` first " \
|
|
347
|
+
"(joins must come before the clause that uses them)"
|
|
348
|
+
else
|
|
349
|
+
raise ArgumentError, "unknown table '#{t}' in [table, column] pair"
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Convention for the returned `on` hash (consumed by
|
|
354
|
+
# Compiler::DQL.render_on_clauses): `{ <joined_local> => <current_ref> }`.
|
|
355
|
+
# Either side of the FK edge can hold the local column, so normalize.
|
|
356
|
+
def _resolve_join_keys(target_table)
|
|
357
|
+
current = _current_table_name
|
|
358
|
+
fks = Diamond.engine.foreign_keys
|
|
359
|
+
|
|
360
|
+
if fks[current]
|
|
361
|
+
fks[current].each do |fk|
|
|
362
|
+
return { fk[:ref_col] => fk[:local] } if fk[:ref_table] == target_table
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
if fks[target_table]
|
|
367
|
+
fks[target_table].each do |fk|
|
|
368
|
+
return { fk[:local] => fk[:ref_col] } if fk[:ref_table] == current
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
raise Diamond::TableNotFound,
|
|
373
|
+
"No foreign key connects '#{current}' and '#{target_table}'. " \
|
|
374
|
+
"Pass `on: { <local>: <ref> }` explicitly."
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Detect if join is to-one (belongs_to) vs to-many (has_many)
|
|
378
|
+
# Current table has FK to target -> belongs_to -> single (to-one)
|
|
379
|
+
# Target table has FK to current -> has_many -> collection (to-many)
|
|
380
|
+
# Default: collection (safer)
|
|
381
|
+
def _detect_single_join(target_table)
|
|
382
|
+
current = _current_table_name
|
|
383
|
+
fks = Diamond.engine.foreign_keys
|
|
384
|
+
|
|
385
|
+
return true if fks[current]&.any? { |fk| fk[:ref_table] == target_table }
|
|
386
|
+
return false if fks[target_table]&.any? { |fk| fk[:ref_table] == current }
|
|
387
|
+
false
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def _current_table_name
|
|
391
|
+
self.is_a?(Diamond::Table) ? @name : @table.name
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Diamond
|
|
2
|
+
module Domains
|
|
3
|
+
module DynamicFinders
|
|
4
|
+
FINDER_PREFIX = 'by_'.freeze
|
|
5
|
+
AND_SEPARATOR = '_and_'.freeze
|
|
6
|
+
CACHES_KEY = Diamond::RACTOR_KEYS[:finder_cols]
|
|
7
|
+
|
|
8
|
+
# Per-Ractor cache for finder name -> column symbols. the parse is pure
|
|
9
|
+
# (name only), so a hot `by_id` loop doesn't re-split every call.
|
|
10
|
+
# validation still runs per call against the live schema.
|
|
11
|
+
def self.cache
|
|
12
|
+
Ractor.current[CACHES_KEY] ||= {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.clear_caches!
|
|
16
|
+
Ractor.current[CACHES_KEY] = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def method_missing(name, *args, &block)
|
|
20
|
+
method_str = name.to_s
|
|
21
|
+
return super unless method_str.start_with?(FINDER_PREFIX)
|
|
22
|
+
|
|
23
|
+
cache = DynamicFinders.cache
|
|
24
|
+
columns = cache[method_str]
|
|
25
|
+
unless columns
|
|
26
|
+
column_strs = method_str.delete_prefix(FINDER_PREFIX).split(AND_SEPARATOR)
|
|
27
|
+
if column_strs.empty?
|
|
28
|
+
raise ArgumentError, "Invalid dynamic finder '#{name}': no columns after prefix"
|
|
29
|
+
end
|
|
30
|
+
columns = column_strs.map(&:to_sym)
|
|
31
|
+
cache[method_str] = columns
|
|
32
|
+
end
|
|
33
|
+
unless args.size == columns.size
|
|
34
|
+
raise ArgumentError,
|
|
35
|
+
"wrong number of arguments for #{name} (given #{args.size}, expected #{columns.size})"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
columns.each do |col|
|
|
39
|
+
unless _schema_for_dsl[:columns].include?(col)
|
|
40
|
+
raise Diamond::UnknownColumnError.build(_schema_for_dsl, col)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
pairs = columns.zip(args)
|
|
45
|
+
condition = pairs.drop(1).reduce(build_equality(pairs.first)) do |acc, pair|
|
|
46
|
+
AST::And.new(acc, build_equality(pair))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
_chain(AST::Where.new(condition))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def respond_to_missing?(name, include_private = false)
|
|
53
|
+
name.to_s.start_with?(FINDER_PREFIX) || super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def build_equality(pair)
|
|
59
|
+
col, val = pair
|
|
60
|
+
AST::Equality.new(AST::Column.new(col), AST::Literal.new(val))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|