occams-record 1.4.0 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +101 -120
- data/lib/occams-record/batches/offset_limit/raw_query.rb +5 -1
- data/lib/occams-record/binds_converter/abstract.rb +71 -0
- data/lib/occams-record/binds_converter/named.rb +35 -0
- data/lib/occams-record/binds_converter/positional.rb +20 -0
- data/lib/occams-record/binds_converter.rb +23 -0
- data/lib/occams-record/cursor.rb +1 -2
- data/lib/occams-record/eager_loaders/ad_hoc_base.rb +27 -14
- data/lib/occams-record/eager_loaders/base.rb +36 -6
- data/lib/occams-record/eager_loaders/belongs_to.rb +0 -1
- data/lib/occams-record/eager_loaders/builder.rb +12 -4
- data/lib/occams-record/eager_loaders/context.rb +21 -9
- data/lib/occams-record/eager_loaders/eager_loaders.rb +1 -0
- data/lib/occams-record/eager_loaders/habtm.rb +5 -7
- data/lib/occams-record/eager_loaders/has_one.rb +0 -1
- data/lib/occams-record/eager_loaders/polymorphic_belongs_to.rb +36 -7
- data/lib/occams-record/eager_loaders/through.rb +32 -17
- data/lib/occams-record/eager_loaders/tracer.rb +16 -0
- data/lib/occams-record/errors.rb +6 -2
- data/lib/occams-record/merge.rb +4 -6
- data/lib/occams-record/pluck.rb +38 -0
- data/lib/occams-record/query.rb +45 -13
- data/lib/occams-record/raw_query.rb +62 -25
- data/lib/occams-record/results/results.rb +25 -30
- data/lib/occams-record/results/row.rb +33 -16
- data/lib/occams-record/type_caster.rb +62 -0
- data/lib/occams-record/version.rb +1 -1
- data/lib/occams-record.rb +7 -0
- metadata +13 -20
@@ -10,6 +10,12 @@ module OccamsRecord
|
|
10
10
|
# Specify an association to be eager-loaded. For maximum memory savings, only SELECT the
|
11
11
|
# colums you actually need.
|
12
12
|
#
|
13
|
+
# If you pass a block to nest more eager loads, you may call it with one of two forms: with an argument and without:
|
14
|
+
#
|
15
|
+
# If you ommit the block argument, the "self" inside the block will be the eager loader. You can call "eager_load" and "scope" directly.
|
16
|
+
#
|
17
|
+
# If you include the block argument, the "self" inside the block is the same as the self outside the block. The argument will be the eager loader, which you can use to make additional "eager_load" or "scope" calls.
|
18
|
+
#
|
13
19
|
# @param assoc [Symbol] name of association
|
14
20
|
# @param scope [Proc] a scope to apply to the query (optional). It will be passed an
|
15
21
|
# ActiveRecord::Relation on which you may call all the normal query hethods (select, where, etc) as well as any scopes you've defined on the model.
|
@@ -18,11 +24,12 @@ module OccamsRecord
|
|
18
24
|
# @param as [Symbol] Load the association usign a different attribute name
|
19
25
|
# @param from [Symbol] Opposite of `as`. `assoc` is the custom name and `from` is the name of association on the ActiveRecord model.
|
20
26
|
# @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
|
27
|
+
# @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
|
21
28
|
# @yield a block where you may perform eager loading on *this* association (optional)
|
22
29
|
# @return self
|
23
30
|
#
|
24
|
-
def eager_load(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, &builder)
|
25
|
-
@eager_loaders.add(assoc, scope, select: select, use: use, as: as, from: from, optimizer: optimizer, &builder)
|
31
|
+
def eager_load(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, active_record_fallback: nil, &builder)
|
32
|
+
@eager_loaders.add(assoc, scope, select: select, use: use, as: as, from: from, optimizer: optimizer, active_record_fallback: active_record_fallback, &builder)
|
26
33
|
self
|
27
34
|
end
|
28
35
|
|
@@ -39,11 +46,12 @@ module OccamsRecord
|
|
39
46
|
# @param as [Symbol] Load the association usign a different attribute name
|
40
47
|
# @param from [Symbol] Opposite of `as`. `assoc` is the custom name and `from` is the name of association on the ActiveRecord model.
|
41
48
|
# @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
|
49
|
+
# @param active_record_fallback [Symbol] If passed, the record(s) will be converted to read-only ActiveRecord objects. Options are :lazy (allow lazy loading) or :strict (enables strict loading)
|
42
50
|
# @return [OccamsRecord::EagerLoaders::Base]
|
43
51
|
#
|
44
|
-
def nest(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select)
|
52
|
+
def nest(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, active_record_fallback: nil)
|
45
53
|
raise ArgumentError, "OccamsRecord::EagerLoaders::Builder#nest does not accept a block!" if block_given?
|
46
|
-
@eager_loaders.add(assoc, scope, select: select, use: use, as: as, from: from, optimizer: optimizer) ||
|
54
|
+
@eager_loaders.add(assoc, scope, select: select, use: use, as: as, from: from, optimizer: optimizer, active_record_fallback: active_record_fallback) ||
|
47
55
|
raise("OccamsRecord::EagerLoaders::Builder#nest may not be called under a polymorphic association")
|
48
56
|
end
|
49
57
|
|
@@ -14,14 +14,19 @@ module OccamsRecord
|
|
14
14
|
# @return [ActiveRecord::Base]
|
15
15
|
attr_reader :model
|
16
16
|
|
17
|
+
# @return [OccamsRecord::EagerLoaders::Tracer]
|
18
|
+
attr_reader :tracer
|
19
|
+
|
17
20
|
#
|
18
21
|
# Initialize a new eager loading context.
|
19
22
|
#
|
20
23
|
# @param mode [ActiveRecord::Base] the model that contains the associations that will be referenced.
|
24
|
+
# @param tracer [OccamsRecord::EagerLoaders::Tracer] the eager loader that owns this context (if any)
|
21
25
|
# @param polymorphic [Boolean] When true, model is allowed to change, and it's assumed that not every loader is applicable to every model.
|
22
26
|
#
|
23
|
-
def initialize(model = nil, polymorphic: false)
|
27
|
+
def initialize(model = nil, tracer: nil, polymorphic: false)
|
24
28
|
@model, @polymorphic = model, polymorphic
|
29
|
+
@tracer = tracer || OccamsRecord::EagerLoaders::Tracer.new("root")
|
25
30
|
@loaders = []
|
26
31
|
@dynamic_loaders = []
|
27
32
|
end
|
@@ -72,10 +77,11 @@ module OccamsRecord
|
|
72
77
|
# @param as [Symbol] Load the association usign a different attribute name
|
73
78
|
# @param from [Symbol] Opposite of `as`. `assoc` is the custom name and `from` is the name of association on the ActiveRecord model.
|
74
79
|
# @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
|
80
|
+
# @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
|
75
81
|
# @yield a block where you may perform eager loading on *this* association (optional)
|
76
82
|
# @return [OccamsRecord::EagerLoaders::Base] the new loader. if @model is nil, nil will be returned.
|
77
83
|
#
|
78
|
-
def add(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, &builder)
|
84
|
+
def add(assoc, scope = nil, select: nil, use: nil, as: nil, from: nil, optimizer: :select, active_record_fallback: nil, &builder)
|
79
85
|
if from
|
80
86
|
real_assoc = from
|
81
87
|
custom_name = assoc
|
@@ -88,11 +94,11 @@ module OccamsRecord
|
|
88
94
|
end
|
89
95
|
|
90
96
|
if @model
|
91
|
-
loader = build_loader!(real_assoc, custom_name, scope, select, use, optimizer, builder)
|
97
|
+
loader = build_loader!(real_assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback)
|
92
98
|
@loaders << loader
|
93
99
|
loader
|
94
100
|
else
|
95
|
-
@dynamic_loaders << [real_assoc, custom_name, scope, select, use, optimizer, builder]
|
101
|
+
@dynamic_loaders << [real_assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback]
|
96
102
|
nil
|
97
103
|
end
|
98
104
|
end
|
@@ -111,21 +117,27 @@ module OccamsRecord
|
|
111
117
|
nil
|
112
118
|
end
|
113
119
|
|
120
|
+
# Iterate over each loader
|
121
|
+
def each
|
122
|
+
return @loaders.each unless block_given?
|
123
|
+
@loaders.each { |l| yield l }
|
124
|
+
end
|
125
|
+
|
114
126
|
private
|
115
127
|
|
116
|
-
def build_loader!(assoc, custom_name, scope, select, use, optimizer, builder)
|
117
|
-
build_loader(assoc, custom_name, scope, select, use, optimizer, builder) ||
|
118
|
-
raise("OccamsRecord: No
|
128
|
+
def build_loader!(assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback)
|
129
|
+
build_loader(assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback) ||
|
130
|
+
raise("OccamsRecord: No association `:#{assoc}` on `#{@model.name}` or subclasses")
|
119
131
|
end
|
120
132
|
|
121
|
-
def build_loader(assoc, custom_name, scope, select, use, optimizer, builder)
|
133
|
+
def build_loader(assoc, custom_name, scope, select, use, optimizer, builder, active_record_fallback)
|
122
134
|
ref = @model.reflections[assoc.to_s] ||
|
123
135
|
@model.subclasses.map(&:reflections).detect { |x| x.has_key? assoc.to_s }&.[](assoc.to_s)
|
124
136
|
return nil if ref.nil?
|
125
137
|
|
126
138
|
scope ||= ->(q) { q.select select } if select
|
127
139
|
loader_class = !!ref.through_reflection ? EagerLoaders::Through : EagerLoaders.fetch!(ref)
|
128
|
-
loader_class.new(ref, scope, use: use, as: custom_name, optimizer: optimizer, &builder)
|
140
|
+
loader_class.new(ref, scope, use: use, as: custom_name, optimizer: optimizer, parent: @tracer, active_record_fallback: active_record_fallback, &builder)
|
129
141
|
end
|
130
142
|
end
|
131
143
|
end
|
@@ -5,6 +5,7 @@ module OccamsRecord
|
|
5
5
|
module EagerLoaders
|
6
6
|
autoload :Builder, 'occams-record/eager_loaders/builder'
|
7
7
|
autoload :Context, 'occams-record/eager_loaders/context'
|
8
|
+
autoload :Tracer, 'occams-record/eager_loaders/tracer'
|
8
9
|
|
9
10
|
autoload :Base, 'occams-record/eager_loaders/base'
|
10
11
|
autoload :BelongsTo, 'occams-record/eager_loaders/belongs_to'
|
@@ -24,23 +24,21 @@ module OccamsRecord
|
|
24
24
|
# @param join_rows [Array<Array<String>>] raw join'd ids from the db
|
25
25
|
#
|
26
26
|
def merge!(assoc_rows, rows, join_rows)
|
27
|
-
joins_by_id = join_rows.
|
27
|
+
joins_by_id = join_rows.each_with_object({}) { |join, acc|
|
28
28
|
id = join[0].to_s
|
29
|
-
|
30
|
-
|
31
|
-
a
|
29
|
+
acc[id] ||= []
|
30
|
+
acc[id] << join[1].to_s
|
32
31
|
}
|
33
32
|
|
34
33
|
assoc_order_cache = {} # maintains the original order of assoc_rows
|
35
|
-
assoc_rows_by_id = assoc_rows.each_with_index.
|
34
|
+
assoc_rows_by_id = assoc_rows.each_with_index.each_with_object({}) { |(row, idx), acc|
|
36
35
|
begin
|
37
36
|
id = row.send(@ref.association_primary_key).to_s
|
38
37
|
rescue NoMethodError => e
|
39
38
|
raise MissingColumnError.new(row, e.name)
|
40
39
|
end
|
41
40
|
assoc_order_cache[id] = idx
|
42
|
-
|
43
|
-
a
|
41
|
+
acc[id] = row
|
44
42
|
}
|
45
43
|
|
46
44
|
assign = "#{name}="
|
@@ -7,6 +7,12 @@ module OccamsRecord
|
|
7
7
|
# @return [String] association name
|
8
8
|
attr_reader :name
|
9
9
|
|
10
|
+
# @return [OccamsRecord::EagerLoaders::Tracer | nil] a reference to this eager loader and its parent (if any)
|
11
|
+
attr_reader :tracer
|
12
|
+
|
13
|
+
# @return [OccamsRecord::EagerLoaders::Context]
|
14
|
+
attr_reader :eager_loaders
|
15
|
+
|
10
16
|
#
|
11
17
|
# @param ref [ActiveRecord::Association] the ActiveRecord association
|
12
18
|
# @param scope [Proc] a scope to apply to the query (optional). It will be passed an
|
@@ -14,15 +20,39 @@ module OccamsRecord
|
|
14
20
|
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
15
21
|
# @param as [Symbol] Load the association usign a different attribute name
|
16
22
|
# @param optimizer [Symbol] Only used for `through` associations. A no op here.
|
23
|
+
# @param parent [OccamsRecord::EagerLoaders::Tracer] the eager loader this one is nested under (if any)
|
24
|
+
# @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
|
17
25
|
# @yield perform eager loading on *this* association (optional)
|
18
26
|
#
|
19
|
-
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: nil, &builder)
|
20
|
-
@ref, @
|
27
|
+
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: nil, parent: nil, active_record_fallback: nil, &builder)
|
28
|
+
@ref, @scopes, @use = ref, Array(scope), use
|
21
29
|
@name = (as || ref.name).to_s
|
22
30
|
@foreign_type = @ref.foreign_type.to_sym
|
23
31
|
@foreign_key = @ref.foreign_key.to_sym
|
24
|
-
@
|
25
|
-
|
32
|
+
@tracer = Tracer.new(name, parent)
|
33
|
+
@eager_loaders = EagerLoaders::Context.new(nil, tracer: @tracer, polymorphic: true)
|
34
|
+
@active_record_fallback = active_record_fallback
|
35
|
+
if builder
|
36
|
+
if builder.arity > 0
|
37
|
+
builder.call(self)
|
38
|
+
else
|
39
|
+
instance_exec(&builder)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# An alternative to passing a "scope" lambda to the constructor. Your block is passed the query
|
46
|
+
# so you can call select, where, order, etc on it.
|
47
|
+
#
|
48
|
+
# If you call scope multiple times, the results will be additive.
|
49
|
+
#
|
50
|
+
# @yield [ActiveRecord::Relation] a relation to modify with select, where, order, etc
|
51
|
+
# @return self
|
52
|
+
#
|
53
|
+
def scope(&scope)
|
54
|
+
@scopes << scope if scope
|
55
|
+
self
|
26
56
|
end
|
27
57
|
|
28
58
|
#
|
@@ -35,7 +65,7 @@ module OccamsRecord
|
|
35
65
|
query(rows) { |scope|
|
36
66
|
eager_loaders = @eager_loaders.dup
|
37
67
|
eager_loaders.model = scope.klass
|
38
|
-
assoc_rows = Query.new(scope, use: @use, eager_loaders: eager_loaders, query_logger: query_logger, measurements: measurements).run
|
68
|
+
assoc_rows = Query.new(scope, use: @use, eager_loaders: eager_loaders, query_logger: query_logger, measurements: measurements, active_record_fallback: @active_record_fallback).run
|
39
69
|
merge! assoc_rows, rows
|
40
70
|
}
|
41
71
|
nil
|
@@ -55,7 +85,6 @@ module OccamsRecord
|
|
55
85
|
next if type.nil? or type == ""
|
56
86
|
model = type.constantize
|
57
87
|
ids = rows_of_type.map(&@foreign_key).uniq
|
58
|
-
ids.sort! if $occams_record_test
|
59
88
|
q = base_scope(model).where(@ref.active_record_primary_key => ids)
|
60
89
|
yield q if ids.any?
|
61
90
|
end
|
@@ -83,7 +112,7 @@ module OccamsRecord
|
|
83
112
|
def base_scope(model)
|
84
113
|
q = model.all
|
85
114
|
q = q.instance_exec(&@ref.scope) if @ref.scope
|
86
|
-
q = @
|
115
|
+
q = @scopes.reduce(q) { |acc, scope| scope.(acc) }
|
87
116
|
q
|
88
117
|
end
|
89
118
|
end
|
@@ -9,7 +9,7 @@ module OccamsRecord
|
|
9
9
|
#
|
10
10
|
# See documentation for OccamsRecord::EagerLoaders::Base.
|
11
11
|
#
|
12
|
-
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, &builder)
|
12
|
+
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, parent: nil, active_record_fallback: nil, &builder)
|
13
13
|
super
|
14
14
|
|
15
15
|
unless @ref.macro == :has_one or @ref.macro == :has_many
|
@@ -20,7 +20,7 @@ module OccamsRecord
|
|
20
20
|
raise ArgumentError, "#{@ref.active_record.name}##{@ref.name} cannot be eager loaded because these `through` associations are polymorphic: #{names.join ', '}"
|
21
21
|
end
|
22
22
|
unless @optimizer == :none or @optimizer == :select
|
23
|
-
raise ArgumentError, "Unrecognized optimizer '#{@optimizer}'"
|
23
|
+
raise ArgumentError, "Unrecognized optimizer '#{@optimizer}' (valid options are :none, :select)"
|
24
24
|
end
|
25
25
|
|
26
26
|
chain = @ref.chain.reverse
|
@@ -31,7 +31,6 @@ module OccamsRecord
|
|
31
31
|
@loader = build_loader
|
32
32
|
end
|
33
33
|
|
34
|
-
# TODO make not hacky
|
35
34
|
def through_name
|
36
35
|
@loader.name
|
37
36
|
end
|
@@ -47,6 +46,7 @@ module OccamsRecord
|
|
47
46
|
|
48
47
|
private
|
49
48
|
|
49
|
+
# starting at the top of the chain, recurse and return the leaf node(s)
|
50
50
|
def reduce(node, depth = 0)
|
51
51
|
link = @chain[depth]
|
52
52
|
case link&.macro
|
@@ -69,33 +69,48 @@ module OccamsRecord
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
72
|
+
# build all the nested eager loaders
|
72
73
|
def build_loader
|
73
74
|
head = @chain[0]
|
74
75
|
links = @chain[1..-2]
|
75
76
|
tail = @chain[-1]
|
76
77
|
|
77
|
-
outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref,
|
78
|
+
outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref, optimized_scope(head), parent: tracer.parent)
|
79
|
+
outer_loader.tracer.through = true
|
78
80
|
|
79
|
-
links.
|
81
|
+
inner_loader = links.
|
80
82
|
reduce(outer_loader) { |loader, link|
|
81
|
-
loader.nest(link.ref.source_reflection.name,
|
83
|
+
nested_loader = loader.nest(link.ref.source_reflection.name, optimized_scope(link))
|
84
|
+
nested_loader.tracer.through = true
|
85
|
+
nested_loader
|
82
86
|
}.
|
83
|
-
nest(tail.ref.source_reflection.name, @
|
87
|
+
nest(tail.ref.source_reflection.name, @scopes, use: @use, as: @as, active_record_fallback: @active_record_fallback)
|
84
88
|
|
89
|
+
@eager_loaders.each { |loader| inner_loader.eager_loaders << loader }
|
90
|
+
inner_loader.tracer.name = tracer.name
|
85
91
|
outer_loader
|
86
92
|
end
|
87
93
|
|
88
|
-
def
|
89
|
-
|
94
|
+
def optimized_scope(link)
|
95
|
+
case @optimizer
|
96
|
+
when :select
|
97
|
+
optimized_select(link)
|
98
|
+
when :none
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
end
|
90
102
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
103
|
+
# only select the ids/foreign keys required to link the parent/child records
|
104
|
+
def optimized_select(link)
|
105
|
+
cols =
|
106
|
+
case link.macro
|
107
|
+
when :belongs_to
|
108
|
+
[link.ref.association_primary_key]
|
109
|
+
when :has_one, :has_many
|
110
|
+
[link.ref.association_primary_key, link.ref.foreign_key]
|
111
|
+
else
|
112
|
+
raise "Unsupported through chain link type '#{link.macro}'"
|
113
|
+
end
|
99
114
|
|
100
115
|
case link.next_ref.source_reflection.macro
|
101
116
|
when :belongs_to
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module OccamsRecord
|
2
|
+
module EagerLoaders
|
3
|
+
# A low-memory way to trace the path of eager loads from any point back to the root query
|
4
|
+
Tracer = Struct.new(:name, :parent, :through) do
|
5
|
+
def to_s
|
6
|
+
lookup.join(".")
|
7
|
+
end
|
8
|
+
|
9
|
+
def lookup(trace = self)
|
10
|
+
return [] if trace.nil?
|
11
|
+
name = trace.through ? "through(#{trace.name})" : trace.name
|
12
|
+
lookup(trace.parent) << name
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/occams-record/errors.rb
CHANGED
@@ -13,6 +13,7 @@ module OccamsRecord
|
|
13
13
|
def initialize(record, name)
|
14
14
|
@record, @name = record, name
|
15
15
|
@model_name = record.class.model_name
|
16
|
+
@load_trace = record.class.eager_loader_trace
|
16
17
|
end
|
17
18
|
|
18
19
|
# @return [String]
|
@@ -25,7 +26,8 @@ module OccamsRecord
|
|
25
26
|
class MissingColumnError < MissingDataError
|
26
27
|
# @return [String]
|
27
28
|
def message
|
28
|
-
|
29
|
+
loads = @load_trace.to_s
|
30
|
+
"Column '#{name}' is unavailable on #{model_name} because it was not included in the SELECT statement! Occams Record trace: #{loads}"
|
29
31
|
end
|
30
32
|
end
|
31
33
|
|
@@ -33,7 +35,8 @@ module OccamsRecord
|
|
33
35
|
class MissingEagerLoadError < MissingDataError
|
34
36
|
# @return [String]
|
35
37
|
def message
|
36
|
-
|
38
|
+
loads = @load_trace.to_s
|
39
|
+
"Association '#{name}' is unavailable on #{model_name} because it was not eager loaded! Occams Record trace: #{loads}"
|
37
40
|
end
|
38
41
|
end
|
39
42
|
|
@@ -45,6 +48,7 @@ module OccamsRecord
|
|
45
48
|
attr_reader :attrs
|
46
49
|
|
47
50
|
# @param model_name [String]
|
51
|
+
#
|
48
52
|
# @param attrs [Hash]
|
49
53
|
def initialize(model_name, attrs)
|
50
54
|
@model_name, @attrs = model_name, attrs
|
data/lib/occams-record/merge.rb
CHANGED
@@ -35,14 +35,13 @@ module OccamsRecord
|
|
35
35
|
# Optimized for merges where there's a single mapping key pair (which is the vast majority)
|
36
36
|
if mapping.size == 1
|
37
37
|
target_attr, assoc_attr = target_attrs[0], assoc_attrs[0]
|
38
|
-
assoc_rows_by_ids = assoc_rows.
|
38
|
+
assoc_rows_by_ids = assoc_rows.each_with_object({}) { |assoc_row, acc|
|
39
39
|
begin
|
40
40
|
id = assoc_row.send assoc_attr
|
41
41
|
rescue NoMethodError => e
|
42
42
|
raise MissingColumnError.new(assoc_row, e.name)
|
43
43
|
end
|
44
|
-
|
45
|
-
a
|
44
|
+
acc[id] ||= assoc_row
|
46
45
|
}
|
47
46
|
|
48
47
|
target_rows.each do |row|
|
@@ -56,14 +55,13 @@ module OccamsRecord
|
|
56
55
|
|
57
56
|
# Slower but works with any number of mapping key pairs
|
58
57
|
else
|
59
|
-
assoc_rows_by_ids = assoc_rows.
|
58
|
+
assoc_rows_by_ids = assoc_rows.each_with_object({}) { |assoc_row, acc|
|
60
59
|
begin
|
61
60
|
ids = assoc_attrs.map { |attr| assoc_row.send attr }
|
62
61
|
rescue NoMethodError => e
|
63
62
|
raise MissingColumnError.new(assoc_row, e.name)
|
64
63
|
end
|
65
|
-
|
66
|
-
a
|
64
|
+
acc[ids] ||= assoc_row
|
67
65
|
}
|
68
66
|
|
69
67
|
target_rows.each do |row|
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module OccamsRecord
|
2
|
+
module Pluck
|
3
|
+
private
|
4
|
+
|
5
|
+
def pluck_results(results, model: nil)
|
6
|
+
casters = TypeCaster.generate(results.columns, results.column_types, model: model)
|
7
|
+
if results[0]&.size == 1
|
8
|
+
pluck_results_single(results, casters)
|
9
|
+
else
|
10
|
+
pluck_results_multi(results, casters)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# returns an array of values
|
15
|
+
def pluck_results_single(results, casters)
|
16
|
+
col = results.columns[0]
|
17
|
+
caster = casters[col]
|
18
|
+
if caster
|
19
|
+
results.map { |row|
|
20
|
+
val = row[col]
|
21
|
+
caster.(val)
|
22
|
+
}
|
23
|
+
else
|
24
|
+
results.map { |row| row[col] }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# returns an array of arrays
|
29
|
+
def pluck_results_multi(results, casters)
|
30
|
+
results.map { |row|
|
31
|
+
row.map { |col, val|
|
32
|
+
caster = casters[col]
|
33
|
+
caster ? caster.(val) : val
|
34
|
+
}
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/occams-record/query.rb
CHANGED
@@ -18,10 +18,11 @@ module OccamsRecord
|
|
18
18
|
# @param scope [ActiveRecord::Relation]
|
19
19
|
# @param use [Module] optional Module to include in the result class
|
20
20
|
# @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
|
21
|
+
# @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
|
21
22
|
# @return [OccamsRecord::Query]
|
22
23
|
#
|
23
|
-
def self.query(scope, use: nil, query_logger: nil)
|
24
|
-
Query.new(scope, use: use, query_logger: query_logger)
|
24
|
+
def self.query(scope, use: nil, active_record_fallback: nil, query_logger: nil)
|
25
|
+
Query.new(scope, use: use, query_logger: query_logger, active_record_fallback: active_record_fallback)
|
25
26
|
end
|
26
27
|
|
27
28
|
#
|
@@ -35,6 +36,7 @@ module OccamsRecord
|
|
35
36
|
attr_reader :scope
|
36
37
|
|
37
38
|
include OccamsRecord::Batches::CursorHelpers
|
39
|
+
include OccamsRecord::Pluck
|
38
40
|
include EagerLoaders::Builder
|
39
41
|
include Enumerable
|
40
42
|
include Measureable
|
@@ -47,13 +49,15 @@ module OccamsRecord
|
|
47
49
|
# @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
|
48
50
|
# @param eager_loaders [OccamsRecord::EagerLoaders::Context]
|
49
51
|
# @param measurements [Array]
|
52
|
+
# @param active_record_fallback [Symbol] If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)
|
50
53
|
#
|
51
|
-
def initialize(scope, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil)
|
54
|
+
def initialize(scope, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil, active_record_fallback: nil)
|
52
55
|
@model = scope.klass
|
53
56
|
@scope = scope
|
54
57
|
@eager_loaders = eager_loaders || EagerLoaders::Context.new(@model)
|
55
58
|
@use = use
|
56
59
|
@query_logger, @measurements = query_logger, measurements
|
60
|
+
@active_record_fallback = active_record_fallback
|
57
61
|
end
|
58
62
|
|
59
63
|
#
|
@@ -92,16 +96,19 @@ module OccamsRecord
|
|
92
96
|
#
|
93
97
|
def run
|
94
98
|
sql = block_given? ? yield(scope).to_sql : scope.to_sql
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
99
|
+
return [] if sql.blank? # return early in case ActiveRecord::QueryMethods#none was used
|
100
|
+
|
101
|
+
@query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
|
102
|
+
result =
|
103
|
+
if measure?
|
104
|
+
record_start_time!
|
105
|
+
measure!(model.table_name, sql) {
|
106
|
+
model.connection.exec_query sql
|
107
|
+
}
|
108
|
+
else
|
109
|
+
model.connection.exec_query sql
|
110
|
+
end
|
111
|
+
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: model, modules: @use, tracer: @eager_loaders.tracer, active_record_fallback: @active_record_fallback)
|
105
112
|
rows = result.rows.map { |row| row_class.new row }
|
106
113
|
@eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
|
107
114
|
yield_measurements!
|
@@ -222,5 +229,30 @@ module OccamsRecord
|
|
222
229
|
use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders,
|
223
230
|
)
|
224
231
|
end
|
232
|
+
|
233
|
+
#
|
234
|
+
# Returns the specified column(s) as an array of values.
|
235
|
+
#
|
236
|
+
# If more than one column is given, the result will be an array of arrays.
|
237
|
+
#
|
238
|
+
# @param cols [Array] one or more column names as Symbols or Strings. Also accepts SQL functions, e.g. "LENGTH(name)".
|
239
|
+
# @return [Array]
|
240
|
+
#
|
241
|
+
def pluck(*cols)
|
242
|
+
sql = (block_given? ? yield(scope).to_sql : scope).unscope(:select).select(*cols).to_sql
|
243
|
+
return [] if sql.blank? # return early in case ActiveRecord::QueryMethods#none was used
|
244
|
+
|
245
|
+
@query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
|
246
|
+
result =
|
247
|
+
if measure?
|
248
|
+
record_start_time!
|
249
|
+
measure!(model.table_name, sql) {
|
250
|
+
model.connection.exec_query sql
|
251
|
+
}
|
252
|
+
else
|
253
|
+
model.connection.exec_query sql
|
254
|
+
end
|
255
|
+
pluck_results(result, model: @model)
|
256
|
+
end
|
225
257
|
end
|
226
258
|
end
|