praxis-mapper 3.1.1
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/.gitignore +26 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +83 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +102 -0
- data/Guardfile +11 -0
- data/LICENSE +22 -0
- data/README.md +19 -0
- data/Rakefile +14 -0
- data/lib/praxis-mapper/config_hash.rb +40 -0
- data/lib/praxis-mapper/connection_manager.rb +102 -0
- data/lib/praxis-mapper/finalizable.rb +38 -0
- data/lib/praxis-mapper/identity_map.rb +532 -0
- data/lib/praxis-mapper/logging.rb +22 -0
- data/lib/praxis-mapper/model.rb +430 -0
- data/lib/praxis-mapper/query/base.rb +213 -0
- data/lib/praxis-mapper/query/sql.rb +183 -0
- data/lib/praxis-mapper/query_statistics.rb +46 -0
- data/lib/praxis-mapper/resource.rb +226 -0
- data/lib/praxis-mapper/support/factory_girl.rb +104 -0
- data/lib/praxis-mapper/support/memory_query.rb +34 -0
- data/lib/praxis-mapper/support/memory_repository.rb +44 -0
- data/lib/praxis-mapper/support/schema_dumper.rb +66 -0
- data/lib/praxis-mapper/support/schema_loader.rb +56 -0
- data/lib/praxis-mapper/support.rb +2 -0
- data/lib/praxis-mapper/version.rb +5 -0
- data/lib/praxis-mapper.rb +60 -0
- data/praxis-mapper.gemspec +38 -0
- data/spec/praxis-mapper/connection_manager_spec.rb +117 -0
- data/spec/praxis-mapper/identity_map_spec.rb +905 -0
- data/spec/praxis-mapper/logging_spec.rb +9 -0
- data/spec/praxis-mapper/memory_repository_spec.rb +56 -0
- data/spec/praxis-mapper/model_spec.rb +389 -0
- data/spec/praxis-mapper/query/base_spec.rb +317 -0
- data/spec/praxis-mapper/query/sql_spec.rb +184 -0
- data/spec/praxis-mapper/resource_spec.rb +154 -0
- data/spec/praxis_mapper_spec.rb +21 -0
- data/spec/spec_fixtures.rb +12 -0
- data/spec/spec_helper.rb +63 -0
- data/spec/support/spec_models.rb +215 -0
- data/spec/support/spec_resources.rb +39 -0
- metadata +298 -0
@@ -0,0 +1,430 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# This is an abstract class.
|
3
|
+
# Does not have any ORM logic, but instead relies on a data store repository.
|
4
|
+
|
5
|
+
module Praxis::Mapper
|
6
|
+
class Model
|
7
|
+
extend Finalizable
|
8
|
+
|
9
|
+
attr_accessor :_resource, :identity_map, :_query
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :_identities
|
13
|
+
attr_reader :associations, :config, :serialized_fields, :contexts
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.inherited(klass)
|
17
|
+
super
|
18
|
+
|
19
|
+
klass.instance_eval do
|
20
|
+
@config = {
|
21
|
+
:belongs_to => {},
|
22
|
+
:excluded_scopes => [],
|
23
|
+
:identities => []
|
24
|
+
}
|
25
|
+
@associations = {}
|
26
|
+
@serialized_fields = {}
|
27
|
+
@contexts = Hash.new
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
# Internal finalize! logic
|
34
|
+
def self._finalize!
|
35
|
+
self.define_data_accessors *self.identities.flatten
|
36
|
+
|
37
|
+
self.associations.each do |name,config|
|
38
|
+
self.associations[name] = config.to_hash
|
39
|
+
end
|
40
|
+
|
41
|
+
self.define_associations
|
42
|
+
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
# Implements Praxis::Mapper DSL directive 'excluded_scopes'.
|
47
|
+
# Gets or sets the excluded scopes for this model.
|
48
|
+
# Exclusion means that the named condition cannot be applied.
|
49
|
+
#
|
50
|
+
# @param *scopes [Array] list of scopes to exclude
|
51
|
+
# @return [Array] configured list of scopes
|
52
|
+
# @example excluded_scopes :account, :deleted_at
|
53
|
+
def self.excluded_scopes(*scopes)
|
54
|
+
if scopes.any?
|
55
|
+
self.config[:excluded_scopes] = scopes
|
56
|
+
else
|
57
|
+
self.config.fetch(:excluded_scopes)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Gets or sets the repository for this model.
|
62
|
+
#
|
63
|
+
# @param name [Symbol] repository name
|
64
|
+
# @return [Symbol] repository name or :default
|
65
|
+
def self.repository_name(name=nil)
|
66
|
+
if name
|
67
|
+
self.config[:repository_name] = name
|
68
|
+
else
|
69
|
+
self.config.fetch(:repository_name, :default)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Implements Praxis::Mapper DSL directive 'table_name'.
|
74
|
+
# Gets or sets the SQL-like table name.
|
75
|
+
# Can also be thought of as a namespace in the repository.
|
76
|
+
#
|
77
|
+
# @param name [Symbol] table name
|
78
|
+
# @return [Symbol] table name or nil
|
79
|
+
# @example table_name 'json_array_model'
|
80
|
+
def self.table_name(name=nil)
|
81
|
+
if name
|
82
|
+
self.config[:table_name] = name
|
83
|
+
else
|
84
|
+
self.config.fetch(:table_name, nil)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Implements Praxis::Mapper DSL directive 'belongs_to'.
|
89
|
+
# If name and belongs_to_options are given, upserts the association.
|
90
|
+
# If only name is given, gets the named association.
|
91
|
+
# Else, returns all configured associations.
|
92
|
+
#
|
93
|
+
# @param name [String] name of association to set or get
|
94
|
+
# @param belongs_to_options [Hash] new association options
|
95
|
+
# @option :model [Model] the associated model
|
96
|
+
# @option :fk [String] associated field name
|
97
|
+
# @option :source_key [String] local field name
|
98
|
+
# @option :type [Symbol] type of mapping, :scalar or :array
|
99
|
+
# @return [Array] all configured associations
|
100
|
+
# @example
|
101
|
+
# belongs_to :parents, :model => ParentModel,
|
102
|
+
# :source_key => :parent_ids,
|
103
|
+
# :fk => :id,
|
104
|
+
# :type => :array
|
105
|
+
def self.belongs_to(name=nil, belongs_to_options={})
|
106
|
+
if !belongs_to_options.empty?
|
107
|
+
warn "DEPRECATION: `#{self}.belongs_to` is deprecated. Use `many_to_one` or `array_to_many` instead."
|
108
|
+
|
109
|
+
opts = {:fk => :id}.merge(belongs_to_options)
|
110
|
+
|
111
|
+
opts[:key] = opts.delete(:source_key)
|
112
|
+
opts[:primary_key] = opts.delete(:fk) if opts.has_key?(:fk)
|
113
|
+
|
114
|
+
if (opts.delete(:type) == :array)
|
115
|
+
opts[:type] = :array_to_many
|
116
|
+
else
|
117
|
+
opts[:type] = :many_to_one
|
118
|
+
end
|
119
|
+
|
120
|
+
self.associations[name] = opts
|
121
|
+
|
122
|
+
|
123
|
+
define_belongs_to(name, opts)
|
124
|
+
else
|
125
|
+
raise "Calling Model.belongs to fetch association information is no longer supported. Use Model.associations instead."
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
# Define one_to_many (aka: has_many)
|
131
|
+
def self.one_to_many(name, &block)
|
132
|
+
self.associations[name] = ConfigHash.from(type: :one_to_many, &block)
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
# Define many_to_one (aka: belongs_to)
|
137
|
+
def self.many_to_one(name, &block)
|
138
|
+
self.associations[name] = ConfigHash.from(type: :many_to_one, &block)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Define array_to_many (aka: belongs_to where the key attribute is an array)
|
142
|
+
def self.array_to_many(name, &block)
|
143
|
+
self.associations[name] = ConfigHash.from(type: :array_to_many, &block)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Define many_to_array (aka: has_many where the key attribute is an array)
|
147
|
+
def self.many_to_array(name, &block)
|
148
|
+
self.associations[name] = ConfigHash.from(type: :many_to_array, &block)
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
# Adds given identity to the list of model identities.
|
153
|
+
# May be an array for composite keys.
|
154
|
+
def self.identity(name)
|
155
|
+
@_identities ||= Array.new
|
156
|
+
@_identities << name
|
157
|
+
self.config[:identities] << name
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
# Implements Praxis::Mapper DSL directive 'identities'.
|
162
|
+
# Gets or sets list of identity fields.
|
163
|
+
#
|
164
|
+
# @param *names [Array] list of identity fields to set
|
165
|
+
# @return [Array] configured list of identity fields
|
166
|
+
# @example identities :id, :type
|
167
|
+
def self.identities(*names)
|
168
|
+
if names.any?
|
169
|
+
self.config[:identities] = names
|
170
|
+
@_identities = names
|
171
|
+
else
|
172
|
+
self.config.fetch(:identities)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
# Implements Praxis::Mapper DSL directive 'yaml'.
|
178
|
+
# This will perform YAML.load on serialized data.
|
179
|
+
#
|
180
|
+
# @param name [String] name of field that is serialized as YAML
|
181
|
+
# @param opts [Hash]
|
182
|
+
# @options :default [String] default value?
|
183
|
+
#
|
184
|
+
# @example yaml :parent_ids, :default => []
|
185
|
+
def self.yaml(name, opts={})
|
186
|
+
@serialized_fields[name] = :yaml
|
187
|
+
define_serialized_accessor(name, YAML, opts)
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
# Implements Praxis::Mapper DSL directive 'json'.
|
192
|
+
# This will perform JSON.load on serialized data.
|
193
|
+
#
|
194
|
+
# @param name [String] name of field that is serialized as JSON
|
195
|
+
# @param opts [Hash]
|
196
|
+
# @options :default [String] default value?
|
197
|
+
#
|
198
|
+
# @example yaml :parent_ids, :default => []
|
199
|
+
def self.json(name, opts={})
|
200
|
+
@serialized_fields[name] = :json
|
201
|
+
define_serialized_accessor(name, JSON, opts)
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.define_serialized_accessor(name, serializer, **opts)
|
205
|
+
define_method(name) do
|
206
|
+
@deserialized_data[name] ||= if (value = @data.fetch(name))
|
207
|
+
serializer.load(value)
|
208
|
+
else
|
209
|
+
opts[:default]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
define_method("_raw_#{name}".to_sym) do
|
214
|
+
@data.fetch name
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def self.context(name, &block)
|
219
|
+
default = Hash.new do |hash, key|
|
220
|
+
hash[key] = Array.new
|
221
|
+
end
|
222
|
+
@contexts[name] = ConfigHash.from(default, &block).to_hash
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
def self.define_data_accessors(*names)
|
227
|
+
names.each do |name|
|
228
|
+
self.define_data_accessor(name)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
|
233
|
+
def self.define_data_accessor(name)
|
234
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
235
|
+
def #{name}
|
236
|
+
@__#{name} ||= @data.fetch(#{name.inspect}) do
|
237
|
+
raise "field #{name.inspect} not loaded for #{self.inspect}."
|
238
|
+
end.freeze
|
239
|
+
end
|
240
|
+
RUBY
|
241
|
+
end
|
242
|
+
|
243
|
+
def self.define_associations
|
244
|
+
self.associations.each do |name, association|
|
245
|
+
self.define_association(name,association)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def self.define_association(name, association)
|
250
|
+
case association[:type]
|
251
|
+
when :many_to_one
|
252
|
+
self.define_many_to_one(name, association)
|
253
|
+
when :array_to_many
|
254
|
+
self.define_array_to_many(name, association)
|
255
|
+
when :one_to_many
|
256
|
+
self.define_one_to_many(name, association)
|
257
|
+
when :many_to_array
|
258
|
+
self.define_many_to_array(name, association)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# has_many
|
263
|
+
def self.define_one_to_many(name, association)
|
264
|
+
model = association[:model]
|
265
|
+
primary_key = association.fetch(:primary_key, :id)
|
266
|
+
|
267
|
+
if primary_key.kind_of?(Array)
|
268
|
+
define_method(name) do
|
269
|
+
pk = primary_key.collect { |k| self.send(k) }
|
270
|
+
self.identity_map.all(model,association[:key] => [pk])
|
271
|
+
end
|
272
|
+
else
|
273
|
+
define_method(name) do
|
274
|
+
pk = self.send(primary_key)
|
275
|
+
self.identity_map.all(model,association[:key] => [pk])
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def self.define_many_to_one(name, association)
|
281
|
+
model = association[:model]
|
282
|
+
primary_key = association.fetch(:primary_key, :id)
|
283
|
+
|
284
|
+
if association[:key].kind_of?(Array)
|
285
|
+
key = "["
|
286
|
+
key += association[:key].collect { |k| "self.#{k}" }.join(", ")
|
287
|
+
key += "]"
|
288
|
+
else
|
289
|
+
key = "self.#{association[:key]}"
|
290
|
+
end
|
291
|
+
|
292
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
293
|
+
def #{name}
|
294
|
+
return nil if #{key}.nil?
|
295
|
+
@__#{name} ||= self.identity_map.get(#{model.name},#{primary_key.inspect} => #{key})
|
296
|
+
end
|
297
|
+
RUBY
|
298
|
+
|
299
|
+
end
|
300
|
+
|
301
|
+
|
302
|
+
def self.define_array_to_many(name, association)
|
303
|
+
model = association[:model]
|
304
|
+
primary_key = association.fetch(:primary_key, :id)
|
305
|
+
key = association.fetch(:key)
|
306
|
+
|
307
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
308
|
+
def #{name}
|
309
|
+
return nil if #{key}.nil?
|
310
|
+
@__#{name} ||= self.identity_map.all(#{model.name},#{primary_key.inspect} => #{key})
|
311
|
+
end
|
312
|
+
RUBY
|
313
|
+
|
314
|
+
end
|
315
|
+
|
316
|
+
|
317
|
+
def self.define_many_to_array(name, association)
|
318
|
+
model = association[:model]
|
319
|
+
primary_key = association.fetch(:primary_key, :id)
|
320
|
+
key_name = association.fetch(:key)
|
321
|
+
|
322
|
+
if primary_key.kind_of?(Array)
|
323
|
+
key = "["
|
324
|
+
key += primary_key.collect { |k| "self.#{k}" }.join(", ")
|
325
|
+
key += "]"
|
326
|
+
else
|
327
|
+
key = "self.#{primary_key}"
|
328
|
+
end
|
329
|
+
|
330
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
331
|
+
def #{name}
|
332
|
+
key = #{key}
|
333
|
+
return nil if key.nil?
|
334
|
+
@__#{name} ||= self.identity_map.all(#{model.name}).
|
335
|
+
select { |record| record.#{key_name}.include? key }
|
336
|
+
end
|
337
|
+
RUBY
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
|
342
|
+
# The belongs_to association creates a one-to-one match with another model.
|
343
|
+
# In database terms, this association says that this class contains the foreign key.
|
344
|
+
#
|
345
|
+
# @param name [Symbol] name of association; typically the same as associated model name
|
346
|
+
# @param opts [Hash] association options
|
347
|
+
# @option :model [Model] the associated model
|
348
|
+
# @option :fk [String] associated field name
|
349
|
+
# @option :source_key [String] local field name
|
350
|
+
# @option :type [Symbol] type of mapping, :scalar or :array
|
351
|
+
#
|
352
|
+
# @example
|
353
|
+
# define_belongs_to(:customer, {:model => Customer, :fk => :id, :source_key => :customer_id, :type => scalar})
|
354
|
+
#
|
355
|
+
# @see http://guides.rubyonrails.org/v2.3.11/association_basics.html#belongs-to-association-reference
|
356
|
+
def self.define_belongs_to(name, opts)
|
357
|
+
model = opts.fetch(:model)
|
358
|
+
type = opts.fetch(:type, :many_to_one) # :scalar has no meaning other than it's not an array
|
359
|
+
|
360
|
+
case opts.fetch(:type, :many_to_one)
|
361
|
+
when :many_to_one
|
362
|
+
return self.define_many_to_one(name, opts)
|
363
|
+
when :array_to_many
|
364
|
+
return self.define_array_to_many(name, opts)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
|
369
|
+
# Looks up in the identity map first.
|
370
|
+
#
|
371
|
+
# @param condition ?
|
372
|
+
# @return [Model] matching record
|
373
|
+
def self.get(condition)
|
374
|
+
IdentityMap.current.get(self, condition)
|
375
|
+
end
|
376
|
+
|
377
|
+
|
378
|
+
# Looks up in the identity map first.
|
379
|
+
#
|
380
|
+
# @param condition [Hash] ?
|
381
|
+
# @return [Array<Model>] all matching records
|
382
|
+
def self.all(condition={})
|
383
|
+
IdentityMap.current.all(self, condition)
|
384
|
+
end
|
385
|
+
|
386
|
+
|
387
|
+
def initialize(data)
|
388
|
+
@data = data
|
389
|
+
@deserialized_data = {}
|
390
|
+
@query = nil
|
391
|
+
end
|
392
|
+
|
393
|
+
InspectedFields = [:@data, :@deserialized_data].freeze
|
394
|
+
def inspect
|
395
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} #{
|
396
|
+
instance_variables.select{|v| InspectedFields.include? v}.map {|var|
|
397
|
+
"#{var}: #{instance_variable_get(var).inspect}"
|
398
|
+
}.join("#{' '}")
|
399
|
+
}#{' '}>"
|
400
|
+
end
|
401
|
+
|
402
|
+
def respond_to_missing?(name, *)
|
403
|
+
@data.key?(name) || super
|
404
|
+
end
|
405
|
+
|
406
|
+
|
407
|
+
def method_missing(name, *args)
|
408
|
+
if @data.has_key? name
|
409
|
+
self.class.define_data_accessor(name)
|
410
|
+
self.send(name)
|
411
|
+
else
|
412
|
+
super
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
|
417
|
+
def identities
|
418
|
+
self.class._identities.each_with_object(Hash.new) do |identity, hash|
|
419
|
+
case identity
|
420
|
+
when Symbol
|
421
|
+
hash[identity] = @data[identity].freeze
|
422
|
+
else
|
423
|
+
hash[identity] = @data.values_at(*identity).collect(&:freeze)
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
end
|
429
|
+
|
430
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
module Praxis::Mapper
|
2
|
+
module Query
|
3
|
+
|
4
|
+
# Abstract base class for assembling read queries for a data store.
|
5
|
+
# May be implemented for SQL, CQL, etc.
|
6
|
+
# Collects query statistics.
|
7
|
+
#
|
8
|
+
# @see lib/support/memory_query.rb
|
9
|
+
class Base
|
10
|
+
MULTI_GET_BATCH_SIZE = 4_096
|
11
|
+
|
12
|
+
attr_reader :identity_map, :model, :statistics, :contexts
|
13
|
+
attr_writer :where
|
14
|
+
|
15
|
+
# Sets up a read query.
|
16
|
+
#
|
17
|
+
# @param identity_map [Praxis::Mapper::IdentityMap] handle to a Praxis::Mapper identity map
|
18
|
+
# @param model [Praxis::Mapper::Model] handle to a Praxis::Mapper model
|
19
|
+
# @param &block [Block] will be instance_eval'ed here
|
20
|
+
def initialize(identity_map, model, &block)
|
21
|
+
@identity_map = identity_map
|
22
|
+
@model = model
|
23
|
+
|
24
|
+
@select = nil
|
25
|
+
|
26
|
+
@where = nil
|
27
|
+
|
28
|
+
@limit = nil
|
29
|
+
@track = Set.new
|
30
|
+
@load = Set.new
|
31
|
+
@contexts = Set.new
|
32
|
+
|
33
|
+
@statistics = Hash.new(0) # general-purpose hash
|
34
|
+
|
35
|
+
if block_given?
|
36
|
+
self.instance_eval(&block)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return handle to configured data store
|
41
|
+
def connection
|
42
|
+
identity_map.connection(model.repository_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Gets or sets an SQL-like 'SELECT' clause to this query.
|
46
|
+
# TODO: fix any specs or code that uses alias
|
47
|
+
#
|
48
|
+
# @param *fields [Array] list of fields, of type Symbol, String, or Hash
|
49
|
+
# @return [Hash] current list of fields
|
50
|
+
#
|
51
|
+
# @example select(:account_id, "user_id", {"create_time" => :created_at})
|
52
|
+
def select(*fields)
|
53
|
+
if fields.any?
|
54
|
+
@select = {} if @select.nil?
|
55
|
+
fields.each do |field|
|
56
|
+
case field
|
57
|
+
when Symbol, String
|
58
|
+
@select[field] = nil
|
59
|
+
when Hash
|
60
|
+
field.each do |alias_name, column_name|
|
61
|
+
@select[alias_name] = column_name
|
62
|
+
end
|
63
|
+
else
|
64
|
+
raise "unknown field type: #{field.class.name}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
else
|
68
|
+
return @select
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
# Gets or sets an SQL-like 'WHERE' clause to this query.
|
74
|
+
#
|
75
|
+
# @param value [String] a 'WHERE' clause
|
76
|
+
#
|
77
|
+
# @example where("deployment_id=2")
|
78
|
+
def where(value=nil)
|
79
|
+
if value
|
80
|
+
@where = value
|
81
|
+
else
|
82
|
+
return @where
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Gets or sets an SQL-like 'LIMIT' clause to this query.
|
87
|
+
#
|
88
|
+
# @param value [String] a 'LIMIT' clause
|
89
|
+
#
|
90
|
+
# @example limit("LIMIT 10 OFFSET 20")
|
91
|
+
def limit(value=nil)
|
92
|
+
if value
|
93
|
+
@limit = value
|
94
|
+
else
|
95
|
+
return @limit
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# @param *values [Array] a list of associations to track
|
100
|
+
def track(*values, &block)
|
101
|
+
if values.any?
|
102
|
+
if block_given?
|
103
|
+
raise "block and multiple values not supported" if values.size > 1
|
104
|
+
@track << [values.first, block]
|
105
|
+
else
|
106
|
+
@track.merge(values)
|
107
|
+
end
|
108
|
+
else
|
109
|
+
return @track
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# @param *values [Array] a list of associations to load immediately after this
|
114
|
+
def load(*values, &block)
|
115
|
+
if values.any?
|
116
|
+
if block_given?
|
117
|
+
raise "block and multiple values not supported" if values.size > 1
|
118
|
+
@load << [values.first, block]
|
119
|
+
else
|
120
|
+
@load.merge(values)
|
121
|
+
end
|
122
|
+
else
|
123
|
+
return @load
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def context(name=nil)
|
128
|
+
@contexts << name
|
129
|
+
spec = model.contexts.fetch(name) do
|
130
|
+
raise "context #{name.inspect} not found for #{model}"
|
131
|
+
end
|
132
|
+
|
133
|
+
select *spec[:select]
|
134
|
+
track *spec[:track]
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
# @return [Array] a list of associated models
|
139
|
+
def tracked_associations
|
140
|
+
track.collect do |(name, _)|
|
141
|
+
model.associations.fetch(name) do
|
142
|
+
raise "association #{name.inspect} not found for #{model}"
|
143
|
+
end
|
144
|
+
end.uniq
|
145
|
+
end
|
146
|
+
|
147
|
+
# Executes multi-get read query and returns all matching records.
|
148
|
+
#
|
149
|
+
# @param identity [Symbol|Array] a simple or composite key for this model
|
150
|
+
# @param values [Array] list of identifier values (ideally a sorted set)
|
151
|
+
# @return [Array] list of matching records, wrapped as models
|
152
|
+
def multi_get(identity, values, select: nil)
|
153
|
+
if self.frozen?
|
154
|
+
raise TypeError.new "can not reuse a frozen query"
|
155
|
+
end
|
156
|
+
|
157
|
+
statistics[:multi_get] += 1
|
158
|
+
|
159
|
+
records = []
|
160
|
+
|
161
|
+
original_select = @select
|
162
|
+
self.select *select.flatten.uniq if select
|
163
|
+
|
164
|
+
values.each_slice(MULTI_GET_BATCH_SIZE) do |batch|
|
165
|
+
# create model objects for each row
|
166
|
+
records += _multi_get(identity, batch)
|
167
|
+
end
|
168
|
+
|
169
|
+
statistics[:records_loaded] += records.size
|
170
|
+
records
|
171
|
+
ensure
|
172
|
+
@select = original_select unless self.frozen?
|
173
|
+
end
|
174
|
+
|
175
|
+
# Executes assembled read query and returns all matching records.
|
176
|
+
#
|
177
|
+
# @return [Array] list of matching records, wrapped as models
|
178
|
+
def execute
|
179
|
+
if self.frozen?
|
180
|
+
raise TypeError.new "can not reuse a frozen query"
|
181
|
+
end
|
182
|
+
statistics[:execute] += 1
|
183
|
+
|
184
|
+
rows = _execute
|
185
|
+
|
186
|
+
statistics[:records_loaded] += rows.size
|
187
|
+
rows.collect do |row|
|
188
|
+
m = model.new(row)
|
189
|
+
m._query = self
|
190
|
+
m
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
# Subclasses Must Implement
|
196
|
+
def _multi_get(identity, values)
|
197
|
+
raise "subclass responsibility"
|
198
|
+
end
|
199
|
+
|
200
|
+
# Subclasses Must Implement
|
201
|
+
def _execute
|
202
|
+
raise "subclass responsibility"
|
203
|
+
end
|
204
|
+
|
205
|
+
# Subclasses Must Implement
|
206
|
+
# the sql or "sql-like" representation of the query
|
207
|
+
def describe
|
208
|
+
raise "subclass responsibility"
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|