sunstone 7.2.0 → 8.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d280487f58777eebd36b07eb69f9492b77370ea3def7b05321bb2d4fd8e55626
4
- data.tar.gz: a34f616483aca20b1207f12538e08fafb19b16ac01db6c19977b902cd1cd1537
3
+ metadata.gz: 9e9d652c2cfc067ca7a6921350122369134b44a200a262d93da2f9df98c90d5c
4
+ data.tar.gz: b13d474440401eb82768422ece2047e9b0e1f1cf470264e4a2d786dcdf5c13eb
5
5
  SHA512:
6
- metadata.gz: 0211de6c05fded268d04e00af2e0f16e9f4ee0650307288ea890c343e1b20f0a2fcf924631e2c0c3e72b602af7e964c8663a6df51f22cce9bd85798b26d12604
7
- data.tar.gz: 62e9b986925a43ca27c172499bca956ea20e6b8ca237d94dbcb1b07f52772ad1ffc9d5a0fe68d9ef6560e9a3df5ccc7bc27a8a78b56aad4fcccc44604f2a9060
6
+ metadata.gz: 5da172f2cfae9b86d5eaa2bdf37bce3b99d385171c505c4047116354c45a7231860ac7dc21dd4e0375ed72c5cdfa2e8bd93a1b289d23289ef002f02e6d547581
7
+ data.tar.gz: 2a7b1fb0a8061847a605499ec8d7f31aa3364802a7bce7fc40bb269959769db4d4e7fe1cee40df47c3071301e16c8d20d40b898756a3f3037224b84f81a7a590
@@ -1,73 +1,53 @@
1
1
  # The last ref that this code was synced with Rails
2
2
  # ref: 9269f634d471ad6ca46752421eabd3e1c26220b5
3
- module ActiveRecord
4
- module Associations
5
- class CollectionAssociation
6
-
7
- def replace(other_array)
8
- other_array.each { |val| raise_on_type_mismatch!(val) }
9
- original_target = skip_strict_loading { load_target }.dup
10
-
11
- if owner.new_record?
12
- replace_records(other_array, original_target)
13
- elsif owner.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter) && owner.instance_variable_defined?(:@sunstone_updating) && owner.instance_variable_get(:@sunstone_updating)
14
- replace_common_records_in_memory(other_array, original_target)
15
-
16
- # Remove from target
17
- records_for_removal = (original_target - other_array)
18
- if !records_for_removal.empty?
19
- self.instance_variable_set(:@sunstone_changed, true)
20
- records_for_removal.each { |record| callback(:before_remove, record) }
21
- records_for_removal.each { |record| target.delete(record) }
22
- records_for_removal.each { |record| callback(:after_remove, record) }
23
- end
24
-
25
- # Add to target
26
- records_for_addition = (other_array - original_target)
27
- if !records_for_addition.empty?
28
- self.instance_variable_set(:@sunstone_changed, true)
29
- (other_array - original_target).each do |record|
30
- add_to_target(record)
31
- end
32
- end
33
-
34
- other_array
35
- else
36
- replace_common_records_in_memory(other_array, original_target)
37
- if other_array != original_target
38
- transaction { replace_records(other_array, original_target) }
39
- else
40
- other_array
41
- end
42
- end
3
+ class ActiveRecord::Associations::CollectionAssociation
4
+
5
+ def replace(other_array)
6
+ other_array.each { |val| raise_on_type_mismatch!(val) }
7
+ original_target = skip_strict_loading { load_target }.dup
8
+
9
+ if owner.new_record?
10
+ replace_records(other_array, original_target)
11
+ elsif owner.sunstone? && owner.instance_variable_defined?(:@sunstone_updating) && owner.instance_variable_get(:@sunstone_updating)
12
+ replace_common_records_in_memory(other_array, original_target)
13
+
14
+ # Remove from target
15
+ records_for_removal = (original_target - other_array)
16
+ if !records_for_removal.empty?
17
+ self.instance_variable_set(:@sunstone_changed, true)
18
+ records_for_removal.each { |record| callback(:before_remove, record) }
19
+ records_for_removal.each { |record| target.delete(record) }
20
+ records_for_removal.each { |record| callback(:after_remove, record) }
43
21
  end
44
22
 
45
- def insert_record(record, validate = true, raise = false, &block)
46
- if record.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter) && owner.instance_variable_defined?(:@sunstone_updating) && owner.instance_variable_get(:@sunstone_updating)
47
- true
48
- elsif raise
49
- record.save!(validate: validate, &block)
50
- else
51
- record.save(validate: validate, &block)
23
+ # Add to target
24
+ records_for_addition = (other_array - original_target)
25
+ if !records_for_addition.empty?
26
+ self.instance_variable_set(:@sunstone_changed, true)
27
+ (other_array - original_target).each do |record|
28
+ add_to_target(record)
52
29
  end
53
30
  end
54
31
 
55
- end
56
-
57
- class HasManyThroughAssociation
58
-
59
- private
60
- def save_through_record(record)
61
- return if record.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
62
- association = build_through_record(record)
63
- if association.changed?
64
- association.save!
65
- end
66
- ensure
67
- @through_records.delete(record.object_id)
32
+ other_array
33
+ else
34
+ replace_common_records_in_memory(other_array, original_target)
35
+ if other_array != original_target
36
+ transaction { replace_records(other_array, original_target) }
37
+ else
38
+ other_array
68
39
  end
69
-
70
40
  end
41
+ end
71
42
 
43
+ def insert_record(record, validate = true, raise = false, &block)
44
+ if record.sunstone? && owner.instance_variable_defined?(:@sunstone_updating) && owner.instance_variable_get(:@sunstone_updating)
45
+ true
46
+ elsif raise
47
+ record.save!(validate: validate, &block)
48
+ else
49
+ record.save(validate: validate, &block)
50
+ end
72
51
  end
73
- end
52
+
53
+ end
@@ -0,0 +1,16 @@
1
+ class ActiveRecord::Associations::CollectionAssociation::HasManyThroughAssociation
2
+
3
+ private
4
+
5
+ def save_through_record(record)
6
+ return if record.sunstone?
7
+
8
+ association = build_through_record(record)
9
+ if association.changed?
10
+ association.save!
11
+ end
12
+ ensure
13
+ @through_records.delete(record.object_id)
14
+ end
15
+
16
+ end
@@ -26,7 +26,7 @@ module ActiveRecord
26
26
  include Module.new {
27
27
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
28
28
  def destroy_associations
29
- if !self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
29
+ if !self.sunstone?
30
30
  association(:#{middle_reflection.name}).delete_all(:delete_all)
31
31
  association(:#{name}).reset
32
32
  end
@@ -11,7 +11,7 @@ module ActiveRecord
11
11
  def attributes_with_values(attribute_names)
12
12
  attrs = attribute_names.index_with { |name| @attributes[name] }
13
13
 
14
- if self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
14
+ if self.sunstone?
15
15
  self.class.reflect_on_all_associations.each do |reflection|
16
16
  if reflection.belongs_to?
17
17
  if association(reflection.name).loaded? && association(reflection.name).target == Thread.current[:sunstone_updating_model]
@@ -0,0 +1,11 @@
1
+ class ActiveRecord::Base
2
+
3
+ def self.sunstone?
4
+ connection_pool.db_config.adapter_class == ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter
5
+ end
6
+
7
+ def sunstone?
8
+ self.class.sunstone?
9
+ end
10
+
11
+ end
@@ -6,7 +6,7 @@ module ActiveRecord
6
6
  private
7
7
 
8
8
  def create_or_update(**) #:nodoc:
9
- if self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
9
+ if self.sunstone?
10
10
  @_already_called ||= {}
11
11
  self.class.reflect_on_all_associations.each do |r|
12
12
  @_already_called[:"autosave_associated_records_for_#{r.name}"] = true
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Locking
5
+ module Optimistic
6
+
7
+ private
8
+
9
+ def _update_row(attribute_values, attempted_action = "update")
10
+ return super unless locking_enabled?
11
+
12
+ begin
13
+ locking_column = self.class.locking_column
14
+ lock_attribute_was = @attributes[locking_column]
15
+
16
+ update_constraints = _query_constraints_hash
17
+
18
+ self[locking_column] += 1
19
+
20
+ attribute_values = if attribute_values.is_a?(Hash)
21
+ attribute_values.merge(attributes_with_values([locking_column]))
22
+ else
23
+ attribute_values = attribute_values.dup if attribute_values.frozen?
24
+ attribute_values << locking_column
25
+ attributes_with_values(attribute_values)
26
+ end
27
+
28
+ # Suntone returns the row(s) not a int of afftecd_rows
29
+ result = self.class._update_record(
30
+ attribute_values,
31
+ update_constraints
32
+ )
33
+ affected_rows = sunstone? ? result.rows.size : result
34
+
35
+ if affected_rows != 1
36
+ raise ActiveRecord::StaleObjectError.new(self, attempted_action)
37
+ end
38
+
39
+ affected_rows
40
+
41
+ # If something went wrong, revert the locking_column value.
42
+ rescue Exception
43
+ @attributes[locking_column] = lock_attribute_was
44
+ raise
45
+ end
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -59,7 +59,7 @@ module ActiveRecord
59
59
 
60
60
  result = new_record? ? _create_record(&block) : _update_record(&block)
61
61
 
62
- if self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter) && result != 0
62
+ if self.sunstone? && result != 0 && !result[0].nil?
63
63
  row_hash = result[0]
64
64
 
65
65
  seen = Hash.new { |h, parent_klass|
@@ -70,7 +70,7 @@ module ActiveRecord
70
70
 
71
71
  model_cache = Hash.new { |h,klass| h[klass] = {} }
72
72
  parents = model_cache[self.class.base_class]
73
-
73
+
74
74
  row_hash.each do |key, value|
75
75
  if self.class.column_names.include?(key.to_s)
76
76
  _write_attribute(key, value)
@@ -105,7 +105,7 @@ module ActiveRecord
105
105
  attribute_names = attributes_for_create(attribute_names)
106
106
  attribute_values = attributes_with_values(attribute_names)
107
107
  returning_values = nil
108
-
108
+
109
109
  self.class.with_connection do |connection|
110
110
  returning_columns = self.class._returning_columns_for_insert(connection)
111
111
 
@@ -115,25 +115,41 @@ module ActiveRecord
115
115
  returning_columns
116
116
  )
117
117
 
118
- if !self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
118
+ if !self.sunstone?
119
119
  returning_columns.zip(returning_values).each do |column, value|
120
- _write_attribute(column, value) if !_read_attribute(column)
120
+ _write_attribute(column, type_for_attribute(column).deserialize(value)) if !_read_attribute(column)
121
121
  end if returning_values
122
122
  end
123
123
  end
124
124
 
125
125
  @new_record = false
126
126
  @previously_new_record = true
127
-
127
+
128
128
  yield(self) if block_given?
129
-
130
- if self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
129
+
130
+ if self.sunstone?
131
131
  returning_values
132
132
  else
133
133
  id
134
134
  end
135
135
  end
136
136
 
137
+ def _touch_row(attribute_names, time)
138
+ time ||= current_time_from_proper_timezone
139
+
140
+ attribute_names.each do |attr_name|
141
+ _write_attribute(attr_name, time)
142
+ end
143
+
144
+ _update_row(attributes_with_values(attribute_names), "touch")
145
+ end
146
+
147
+ # Sunstone passes the values, not just the names. This is because if a
148
+ # sub resource has changed it'll send the whole shabang
149
+ def _update_row(attribute_values, attempted_action = "update")
150
+ self.class._update_record(attribute_values, _query_constraints_hash)
151
+ end
152
+
137
153
  def _update_record(attribute_names = self.attribute_names)
138
154
  attribute_names = attributes_for_update(attribute_names)
139
155
  attribute_values = attributes_with_values(attribute_names)
@@ -142,8 +158,10 @@ module ActiveRecord
142
158
  affected_rows = 0
143
159
  @_trigger_update_callback = true
144
160
  else
145
- affected_rows = self.class._update_record(attribute_values, _query_constraints_hash)
146
- @_trigger_update_callback = affected_rows == 1
161
+ affected_rows = _update_row(attribute_values)
162
+
163
+ # Suntone returns the row(s) not a int of afftecd_rows
164
+ @_trigger_update_callback = (sunstone? ? affected_rows.rows.size : affected_rows) == 1
147
165
  end
148
166
 
149
167
  @previously_new_record = false
@@ -179,7 +197,7 @@ module ActiveRecord
179
197
 
180
198
  end
181
199
  end
182
-
200
+
183
201
  #!!!! TODO: I am duplicated from finder_methods.....
184
202
  def construct_association(parent, reflection, attributes, seen, model_cache)
185
203
  return if attributes.nil?
@@ -231,6 +249,6 @@ module ActiveRecord
231
249
  other.set_inverse_instance(model)
232
250
  model
233
251
  end
234
-
252
+
235
253
  end
236
254
  end
@@ -25,20 +25,20 @@ module ActiveRecord
25
25
  if has_include?(column_names.first)
26
26
  relation = apply_join_dependency
27
27
  relation.pluck(*column_names)
28
- elsif klass.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
28
+ elsif model.sunstone?
29
29
  load
30
- return records.pluck(*column_names.map{|n| n.to_s.sub(/^#{klass.table_name}\./, "")})
30
+ return records.pluck(*column_names.map{|n| n.to_s.sub(/^#{model.table_name}\./, "")})
31
31
  else
32
- klass.disallow_raw_sql!(flattened_args(column_names))
33
- columns = arel_columns(column_names)
32
+ model.disallow_raw_sql!(flattened_args(column_names))
34
33
  relation = spawn
34
+ columns = relation.arel_columns(column_names)
35
35
  relation.select_values = columns
36
36
  result = skip_query_cache_if_necessary do
37
37
  if where_clause.contradiction?
38
38
  ActiveRecord::Result.empty(async: @async)
39
39
  else
40
- klass.with_connection do |c|
41
- c.select_all(relation.arel, "#{klass.name} Pluck", async: @async)
40
+ model.with_connection do |c|
41
+ c.select_all(relation.arel, "#{model.name} Pluck", async: @async)
42
42
  end
43
43
  end
44
44
  end
@@ -48,5 +48,22 @@ module ActiveRecord
48
48
  end
49
49
  end
50
50
 
51
+ private
52
+
53
+ # Prior to Rails 8 we didn't need this method becuase it would
54
+ # return the first value if there was just one - so we'll just
55
+ # do the same as prevously because it doesn't have to be joined
56
+ def select_for_count
57
+ if select_values.empty?
58
+ :all
59
+ elsif model.sunstone?
60
+ select_values.one? ? select_values.first : select_values
61
+ else
62
+ with_connection do |conn|
63
+ arel_columns(select_values).map { |column| conn.visitor.compile(column) }.join(", ")
64
+ end
65
+ end
66
+ end
67
+
51
68
  end
52
69
  end
@@ -0,0 +1,162 @@
1
+ # The last ref that this code was synced with Rails
2
+ # ref: 9269f634d471ad6ca46752421eabd3e1c26220b5
3
+ module ActiveRecord::FinderMethods
4
+ class SunstoneJoinDependency
5
+ def initialize(klass)
6
+ @klass = klass
7
+ end
8
+
9
+ def reflections
10
+ []
11
+ end
12
+
13
+ def apply_column_aliases(relation)
14
+ relation
15
+ end
16
+
17
+ def instantiate(result_set, strict_loading_value, &block)
18
+ seen = Hash.new { |i, object_id|
19
+ i[object_id] = Hash.new { |j, child_class|
20
+ j[child_class] = {}
21
+ }
22
+ }
23
+
24
+ model_cache = Hash.new { |h, klass| h[klass] = {} }
25
+ parents = model_cache[@klass]
26
+
27
+ message_bus = ActiveSupport::Notifications.instrumenter
28
+
29
+ payload = {
30
+ record_count: result_set.length,
31
+ class_name: @klass.name
32
+ }
33
+
34
+ message_bus.instrument("instantiation.active_record", payload) do
35
+ result_set.each { |row_hash|
36
+ parent_key = @klass.primary_key ? row_hash[@klass.primary_key] : row_hash
37
+ parent = parents[parent_key] ||= @klass.instantiate(row_hash.select{|k,v| @klass.column_names.include?(k.to_s) }, &block)
38
+ construct(parent, row_hash.select{|k,v| !@klass.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
39
+ }
40
+ end
41
+
42
+ parents.values
43
+ end
44
+
45
+ def construct(parent, relations, seen, model_cache, strict_loading_value)
46
+ relations.each do |key, attributes|
47
+ reflection = parent.class.reflect_on_association(key)
48
+ next unless reflection
49
+
50
+ if reflection.collection?
51
+ other = parent.association(reflection.name)
52
+ other.loaded!
53
+ else
54
+ if parent.association_cached?(reflection.name)
55
+ model = parent.association(reflection.name).target
56
+ construct(model, attributes.select{|k,v| !model.class.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
57
+ end
58
+ end
59
+
60
+ if !reflection.collection?
61
+ construct_association(parent, reflection, attributes, seen, model_cache, strict_loading_value)
62
+ else
63
+ attributes.each do |row|
64
+ construct_association(parent, reflection, row, seen, model_cache, strict_loading_value)
65
+ end
66
+ end
67
+
68
+ end
69
+ end
70
+
71
+ def construct_association(parent, reflection, attributes, seen, model_cache, strict_loading_value)
72
+ return if attributes.nil?
73
+
74
+ klass = if reflection.polymorphic?
75
+ parent.send(reflection.foreign_type).constantize.base_class
76
+ else
77
+ reflection.klass
78
+ end
79
+ id = attributes[klass.primary_key]
80
+ model = seen[parent.object_id][klass][id]
81
+
82
+ if model
83
+ construct(model, attributes.select{|k,v| !klass.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
84
+
85
+ other = parent.association(reflection.name)
86
+
87
+ if reflection.collection?
88
+ other.target.push(model)
89
+ else
90
+ other.target = model
91
+ end
92
+
93
+ other.set_inverse_instance(model)
94
+ else
95
+ model = construct_model(parent, reflection, id, attributes.select{|k,v| klass.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
96
+ seen[parent.object_id][model.class.base_class][id] = model
97
+ construct(model, attributes.select{|k,v| !klass.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
98
+ end
99
+ end
100
+
101
+
102
+ def construct_model(record, reflection, id, attributes, seen, model_cache, strict_loading_value)
103
+ klass = if reflection.polymorphic?
104
+ record.send(reflection.foreign_type).constantize
105
+ else
106
+ reflection.klass
107
+ end
108
+
109
+ model = model_cache[klass][id] ||= klass.instantiate(attributes)
110
+ other = record.association(reflection.name)
111
+
112
+ if reflection.collection?
113
+ other.target.push(model)
114
+ else
115
+ other.target = model
116
+ end
117
+
118
+ other.set_inverse_instance(model)
119
+ model
120
+ end
121
+
122
+ end
123
+
124
+ private
125
+
126
+ def apply_join_dependency(eager_loading: group_values.empty?)
127
+ if model.sunstone?
128
+ join_dependency = SunstoneJoinDependency.new(base_class)
129
+ relation = except(:includes, :eager_load, :preload)
130
+ relation.arel.eager_load = Arel::Nodes::EagerLoad.new(eager_load_values)
131
+ else
132
+ join_dependency = construct_join_dependency(
133
+ eager_load_values | includes_values, Arel::Nodes::OuterJoin
134
+ )
135
+ relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
136
+
137
+ if eager_loading && has_limit_or_offset? && !(
138
+ using_limitable_reflections?(join_dependency.reflections) &&
139
+ using_limitable_reflections?(
140
+ construct_join_dependency(
141
+ select_association_list(joins_values).concat(
142
+ select_association_list(left_outer_joins_values)
143
+ ), nil
144
+ ).reflections
145
+ )
146
+ )
147
+ relation = skip_query_cache_if_necessary do
148
+ model.with_connection do |c|
149
+ c.distinct_relation_for_primary_key(relation)
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ if block_given?
156
+ yield relation, join_dependency
157
+ else
158
+ relation
159
+ end
160
+ end
161
+
162
+ end
@@ -0,0 +1,78 @@
1
+ class ActiveRecord::PredicateBuilder # :nodoc:
2
+
3
+ def expand_from_hash(attributes, &block)
4
+ return ["1=0"] if attributes.empty?
5
+
6
+ attributes.flat_map do |key, value|
7
+ if key.is_a?(Array) && key.size == 1
8
+ key = key.first
9
+ value = value.flatten
10
+ end
11
+
12
+ if key.is_a?(Array)
13
+ queries = Array(value).map do |ids_set|
14
+ raise ArgumentError, "Expected corresponding value for #{key} to be an Array" unless ids_set.is_a?(Array)
15
+ expand_from_hash(key.zip(ids_set).to_h)
16
+ end
17
+ grouping_queries(queries)
18
+ elsif value.is_a?(Hash) && !table.has_column?(key)
19
+ ka = table.associated_table(key, &block)
20
+ .predicate_builder.expand_from_hash(value.stringify_keys)
21
+
22
+ if self.table.instance_variable_get(:@klass).sunstone?
23
+ ka.each { |k|
24
+ if k.left.is_a?(Arel::Attributes::Attribute) || k.left.is_a?(Arel::Attributes::Relation)
25
+ k.left = Arel::Attributes::Relation.new(k.left, key)
26
+ end
27
+ }
28
+ end
29
+ ka
30
+ elsif table.associated_with?(key)
31
+ # Find the foreign key when using queries such as:
32
+ # Post.where(author: author)
33
+ #
34
+ # For polymorphic relationships, find the foreign key and type:
35
+ # PriceEstimate.where(estimate_of: treasure)
36
+ associated_table = table.associated_table(key)
37
+ if associated_table.polymorphic_association?
38
+ value = [value] unless value.is_a?(Array)
39
+ klass = PolymorphicArrayValue
40
+ elsif associated_table.through_association?
41
+ next associated_table.predicate_builder.expand_from_hash(
42
+ associated_table.primary_key => value
43
+ )
44
+ end
45
+
46
+ klass ||= AssociationQueryValue
47
+ queries = klass.new(associated_table, value).queries.map! do |query|
48
+ # If the query produced is identical to attributes don't go any deeper.
49
+ # Prevents stack level too deep errors when association and foreign_key are identical.
50
+ query == attributes ? self[key, value] : expand_from_hash(query)
51
+ end
52
+
53
+ grouping_queries(queries)
54
+ elsif table.aggregated_with?(key)
55
+ mapping = table.reflect_on_aggregation(key).mapping
56
+ values = value.nil? ? [nil] : Array.wrap(value)
57
+ if mapping.length == 1 || values.empty?
58
+ column_name, aggr_attr = mapping.first
59
+ values = values.map do |object|
60
+ object.respond_to?(aggr_attr) ? object.public_send(aggr_attr) : object
61
+ end
62
+ self[column_name, values]
63
+ else
64
+ queries = values.map do |object|
65
+ mapping.map do |field_attr, aggregate_attr|
66
+ self[field_attr, object.try!(aggregate_attr)]
67
+ end
68
+ end
69
+
70
+ grouping_queries(queries)
71
+ end
72
+ else
73
+ self[key, value]
74
+ end
75
+ end
76
+ end
77
+
78
+ end
@@ -4,7 +4,7 @@ module ActiveRecord
4
4
 
5
5
  def assert_modifiable!
6
6
  raise UnmodifiableRelation if @loaded
7
- raise UnmodifiableRelation if @arel && !klass.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
7
+ raise UnmodifiableRelation if @arel && !model.sunstone?
8
8
  end
9
9
 
10
10
  end
@@ -38,7 +38,7 @@ module ActiveRecord
38
38
  status = nil
39
39
  # connection = self.class.connection
40
40
 
41
- if connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter) && instance_variable_defined?(:@sunstone_updating) && @sunstone_updating
41
+ if sunstone? && instance_variable_defined?(:@sunstone_updating) && @sunstone_updating
42
42
  status = yield
43
43
  else
44
44
  ensure_finalize = !connection.transaction_open?
@@ -30,6 +30,7 @@ module ActiveRecord
30
30
  end
31
31
 
32
32
  def to_sar_and_binds(arel_or_sar_string, binds = [], preparable = nil, allow_retry = false)
33
+ # Arel::TreeManager -> Arel::Node
33
34
  if arel_or_sar_string.respond_to?(:ast)
34
35
  arel_or_sar_string = arel_or_sar_string.ast
35
36
  end
@@ -40,10 +41,13 @@ module ActiveRecord
40
41
  "The values must be stored on the AST directly"
41
42
  end
42
43
 
43
- sar = visitor.accept(arel_or_sar_string, collector)
44
+ col = collector()
45
+ col.retryable = true
46
+ sar = visitor.compile(arel_or_sar_string, col)
44
47
  [sar.freeze, sar.binds, false, allow_retry]
45
48
  else
46
- [arel_or_sar_string.dup.freeze, binds, false, allow_retry]
49
+ arel_or_sar_string = arel_or_sar_string.dup.freeze unless arel_or_sar_string.frozen?
50
+ [arel_or_sar_string, binds, false, allow_retry]
47
51
  end
48
52
  end
49
53
 
@@ -114,9 +118,14 @@ module ActiveRecord
114
118
  sar, binds = sar_for_insert(arel, pk, binds, returning)
115
119
  internal_exec_query(sar, name, binds)
116
120
  end
121
+
122
+ def exec_delete(arel, name = nil, binds = [])
123
+ x = internal_execute(arel, name, binds)
124
+ x.nil? ? 1 : x
125
+ end
117
126
 
118
- def internal_exec_query(arel, name = 'SAR', binds = [], prepare: false, async: false, allow_retry: false)
119
- sars = []
127
+ # Lowest level way to execute a query. Doesn't check for illegal writes, doesn't annotate queries, yields a native result object.
128
+ def raw_execute(arel, name = nil, binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, batch: false)
120
129
  multiple_requests = arel.is_a?(Arel::Collectors::Sunstone)
121
130
  type_casted_binds = binds#type_casted_binds(binds)
122
131
 
@@ -137,50 +146,73 @@ module ActiveRecord
137
146
  multiple_requests = true
138
147
  end
139
148
  end
140
-
141
- send_request = lambda { |req_arel|
149
+
150
+ send_request = lambda { |conn, req_arel, batch|
142
151
  sar = to_sar(req_arel, type_casted_binds)
143
- sars.push(sar)
144
152
  log_mess = sar.path.split('?', 2)
145
- log("#{sar.method} #{log_mess[0]} #{(log_mess[1] && !log_mess[1].empty?) ? MessagePack.unpack(CGI.unescape(log_mess[1])) : '' }", name) do
146
- with_raw_connection do |conn|
147
- response = conn.send_request(sar)
148
- if response.is_a?(Net::HTTPNoContent)
149
- nil
153
+ log("#{sar.method} #{log_mess[0]} #{(log_mess[1] && !log_mess[1].empty?) ? MessagePack.unpack(CGI.unescape(log_mess[1])) : '' }", name) do |notification_payload|
154
+ result = perform_query(conn, sar, prepare:, notification_payload:, batch: batch)
155
+ result.instance_variable_set(:@sunstone_calculation, true) if result && sar.instance_variable_get(:@sunstone_calculation)
156
+ result
157
+ end
158
+ }
159
+
160
+ result = with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
161
+ if multiple_requests
162
+ binds.delete_at(limit_bind_index) if limit_bind_index
163
+
164
+ limit, offset, results = allowed_limit, 0, nil
165
+ last_affected_rows = 0
166
+ while requested_limit ? offset < requested_limit : true
167
+ split_arel = arel.dup
168
+ split_arel.limit = limit
169
+ split_arel.offset = offset
170
+ request_results = send_request.call(conn, split_arel, true)
171
+ last_affected_rows += @last_affected_rows
172
+ if results
173
+ results.push(*request_results)
150
174
  else
151
- JSON.parse(response.body)
175
+ results = request_results
152
176
  end
177
+ break if request_results.size < limit
178
+ offset = offset + limit
153
179
  end
180
+ @last_affected_rows = last_affected_rows
181
+ results
182
+ else
183
+ send_request.call(conn, arel, true)
154
184
  end
155
- }
156
-
157
- result = if multiple_requests
158
- binds.delete_at(limit_bind_index) if limit_bind_index
159
-
160
- limit, offset, results = allowed_limit, 0, []
161
- while requested_limit ? offset < requested_limit : true
162
- split_arel = arel.dup
163
- split_arel.limit = limit
164
- split_arel.offset = offset
165
- request_results = send_request.call(split_arel)
166
- results = results + request_results
167
- break if request_results.size < limit
168
- offset = offset + limit
169
- end
170
- results
171
- else
172
- send_request.call(arel)
173
185
  end
174
-
175
- if sars[0].instance_variable_defined?(:@sunstone_calculation) && sars[0].instance_variable_get(:@sunstone_calculation)
186
+
187
+ result
188
+ end
189
+
190
+ def perform_query(raw_connection, sar, prepare:, notification_payload:, batch: false)
191
+ response = raw_connection.send_request(sar)
192
+ result = response.is_a?(Net::HTTPNoContent) ? nil : JSON.parse(response.body)
193
+
194
+ verified!
195
+ # handle_warnings(result)
196
+ @last_affected_rows = response['Affected-Rows'] || result&.count || 0
197
+ notification_payload[:row_count] = @last_affected_rows
198
+ result
199
+ end
200
+
201
+ # Receive a native adapter result object and returns an ActiveRecord::Result object.
202
+ def cast_result(raw_result)
203
+ if raw_result.instance_variable_defined?(:@sunstone_calculation) && raw_result.instance_variable_get(:@sunstone_calculation)
176
204
  # this is a count, min, max.... yea i know..
177
- ActiveRecord::Result.new(['all'], [result], {:all => @type_map.lookup('integer', {})})
178
- elsif result.is_a?(Array)
179
- ActiveRecord::Result.new(result[0] ? result[0].keys : [], result.map{|r| r.values})
205
+ ActiveRecord::Result.new(['all'], [raw_result], {:all => @type_map.lookup('integer', {})})
206
+ elsif raw_result.is_a?(Array)
207
+ ActiveRecord::Result.new(raw_result[0] ? raw_result[0].keys : [], raw_result.map{|r| r.values})
180
208
  else
181
- ActiveRecord::Result.new(result.keys, [result.values])
209
+ ActiveRecord::Result.new(raw_result.keys, [raw_result.values])
182
210
  end
183
211
  end
212
+
213
+ def affected_rows(raw_result)
214
+ @last_affected_rows
215
+ end
184
216
 
185
217
  def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [], returning: nil)
186
218
  sar, binds = to_sar_and_binds(arel, binds)
@@ -190,13 +222,18 @@ module ActiveRecord
190
222
 
191
223
  id_value || last_inserted_id(value)
192
224
  end
193
-
225
+ alias create insert
226
+
227
+ # Executes the update statement and returns the number of rows affected.
194
228
  def update(arel, name = nil, binds = [])
195
- exec_update(arel, name, binds)
229
+ sar, binds = to_sar_and_binds(arel, binds)
230
+ internal_exec_query(sar, name, binds)
196
231
  end
197
232
 
233
+ # Executes the delete statement and returns the number of rows affected.
198
234
  def delete(arel, name = nil, binds = [])
199
- exec_delete(arel, name, binds)
235
+ sql, binds = to_sar_and_binds(arel, binds)
236
+ exec_delete(sql, name, binds)
200
237
  end
201
238
 
202
239
  def last_inserted_id(result)
@@ -39,6 +39,8 @@ module ActiveRecord
39
39
  # - format_type includes the column size constraint, e.g. varchar(50)
40
40
  # - ::regclass is a function that gives the id for a table name
41
41
  def column_definitions(table_name) # :nodoc:
42
+ puts table_name.inspect
43
+ puts definition(table_name).inspect
42
44
  # TODO: settle on schema, I think we've switched to attributes, so
43
45
  # columns can be removed soon?
44
46
  definition(table_name)['attributes'] || definition(table_name)['columns']
@@ -92,18 +92,30 @@ module ActiveRecord
92
92
  end
93
93
 
94
94
  def active?
95
- @raw_connection&.active?
95
+ @lock.synchronize do
96
+ @raw_connection&.active?
97
+ end
96
98
  end
97
99
 
98
- def reconnect
99
- super
100
- @raw_connection&.reconnect!
100
+ # Connects to a StandardAPI server and sets up the adapter depending
101
+ # on the connected server's characteristics.
102
+ def connect
103
+ @raw_connection = self.class.new_client(@connection_parameters)
101
104
  end
102
105
 
106
+ def reconnect
107
+ @lock.synchronize do
108
+ @raw_connection&.reconnect!
109
+ connect unless @raw_connection
110
+ end
111
+ end
112
+
103
113
  def disconnect!
104
- super
105
- @raw_connection&.disconnect!
106
- @raw_connection = nil
114
+ @lock.synchronize do
115
+ super
116
+ @raw_connection&.disconnect!
117
+ @raw_connection = nil
118
+ end
107
119
  end
108
120
 
109
121
  def discard! # :nodoc:
@@ -111,12 +123,6 @@ module ActiveRecord
111
123
  @raw_connection = nil
112
124
  end
113
125
 
114
- # Executes the delete statement and returns the number of rows affected.
115
- def delete(arel, name = nil, binds = [])
116
- r = exec_delete(arel, name, binds)
117
- r.rows.first.to_i
118
- end
119
-
120
126
  def native_database_types #:nodoc:
121
127
  NATIVE_DATABASE_TYPES
122
128
  end
@@ -189,17 +195,6 @@ module ActiveRecord
189
195
  end
190
196
  alias create insert
191
197
 
192
- # Connects to a StandardAPI server and sets up the adapter depending
193
- # on the connected server's characteristics.
194
- def connect
195
- @raw_connection = self.class.new_client(@connection_parameters)
196
- end
197
-
198
- def reconnect
199
- @raw_connection&.reconnect!
200
- connect unless @raw_connection
201
- end
202
-
203
198
  # Configures the encoding, verbosity, schema search path, and time zone of the connection.
204
199
  # This is called by #connect and should not be called manually.
205
200
  def configure_connection
@@ -136,16 +136,24 @@ module Arel
136
136
 
137
137
  case operation
138
138
  when :count
139
- path += "/#{operation}"
139
+ path << "/#{operation}"
140
140
  when :calculate
141
- path += "/calculate"
141
+ path << "/calculate"
142
142
  params[:select] = columns
143
143
  when :update, :delete
144
- path += "/#{params[:where]['id']}"
145
- params.delete(:where)
144
+ if params[:where].is_a?(Array)
145
+ path << "/#{params[:where][0]['id']}"
146
+ params[:where][0].delete('id')
147
+ params[:where].shift if params[:where][0].empty?
148
+ else
149
+ path << "/#{params[:where]['id']}"
150
+ params[:where].delete('id')
151
+ params.delete(:where) if params[:where].empty?
152
+ end
153
+ params[:where] = params[:where].first if params[:where]&.one?
146
154
  end
147
155
 
148
- if params.size > 0 && request_type == Net::HTTP::Get
156
+ if params.size > 0
149
157
  newpath = path + "?#{CGI.escape(MessagePack.pack(params))}"
150
158
  if newpath.length > MAX_URI_LENGTH
151
159
  request_type_override = Net::HTTP::Post
@@ -160,7 +160,6 @@ module Arel
160
160
  collector.table = o.relation.name
161
161
  collector.operation = :update
162
162
 
163
- # collector.id = o.wheres.first.children.first.right
164
163
  if !o.wheres.empty?
165
164
  collector.where = o.wheres.map { |x| visit(x, collector) }.inject([]) { |c, w|
166
165
  w.is_a?(Array) ? c += w : c << w
@@ -171,8 +170,6 @@ module Arel
171
170
  raise 'Upsupported'
172
171
  end
173
172
 
174
- collector.where = collector.where.first
175
-
176
173
  if o.values
177
174
  collector.updates = {}
178
175
 
@@ -128,6 +128,17 @@ module Sunstone
128
128
  # end
129
129
  # end
130
130
  def send_request(request, body=nil, &block)
131
+ path_and_query = request.path.split('?', 2)
132
+ message = "SENDING: #{request.method} #{path_and_query[0]}"
133
+ if path_and_query[1]
134
+ if request['Query-Encoding'] == 'application/msgpack'
135
+ message << " " << MessagePack.unpack(CGI.unescape(path_and_query[1])).inspect
136
+ else
137
+ message << " " << CGI.unescape(path_and_query[1])
138
+ end
139
+ end
140
+ puts message
141
+
131
142
  if request.method != 'GET' && Thread.current[:sunstone_transaction_count]
132
143
  if Thread.current[:sunstone_transaction_count] == 1 && !Thread.current[:sunstone_request_sent]
133
144
  Thread.current[:sunstone_request_sent] = request
@@ -1,3 +1,3 @@
1
1
  module Sunstone
2
- VERSION = '7.2.0'
2
+ VERSION = '8.0.1'
3
3
  end
data/lib/sunstone.rb CHANGED
@@ -7,6 +7,7 @@ require 'msgpack'
7
7
  require 'cookie_store' # optional
8
8
 
9
9
  require "active_record"
10
+ require "active_record/locking/optimistic"
10
11
 
11
12
  # Adapter
12
13
  require File.expand_path(File.join(__FILE__, '../sunstone/version'))
@@ -20,16 +21,21 @@ require File.expand_path(File.join(__FILE__, '../arel/visitors/sunstone'))
20
21
  require File.expand_path(File.join(__FILE__, '../arel/collectors/sunstone'))
21
22
 
22
23
  # ActiveRecord Extensions
24
+ require File.expand_path(File.join(__FILE__, '../../ext/active_record/base'))
23
25
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/statement_cache'))
24
26
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/associations'))
25
27
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/relation'))
26
28
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/relation/calculations'))
27
29
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/relation/query_methods'))
30
+ require File.expand_path(File.join(__FILE__, '../../ext/active_record/relation/predicate_builder'))
31
+ require File.expand_path(File.join(__FILE__, '../../ext/active_record/relation/finder_methods'))
28
32
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/persistence'))
29
33
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/callbacks'))
30
34
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/attribute_methods'))
31
35
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/transactions'))
32
36
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/associations/collection_association'))
37
+ require File.expand_path(File.join(__FILE__, '../../ext/active_record/associations/has_many_through_association'))
38
+ require File.expand_path(File.join(__FILE__, '../../ext/active_record/locking/optimistic'))
33
39
 
34
40
  require File.expand_path(File.join(__FILE__, '../../ext/active_support/core_ext/object/to_query'))
35
41
 
@@ -37,7 +43,7 @@ require File.expand_path(File.join(__FILE__, '../../ext/arel/select_manager'))
37
43
  require File.expand_path(File.join(__FILE__, '../../ext/arel/nodes/eager_load'))
38
44
  require File.expand_path(File.join(__FILE__, '../../ext/arel/attributes/empty_relation'))
39
45
  require File.expand_path(File.join(__FILE__, '../../ext/arel/nodes/select_statement'))
40
- require File.expand_path(File.join(__FILE__, '../../ext/active_record/finder_methods'))
46
+
41
47
 
42
48
 
43
49
  ActiveRecord::ConnectionAdapters.register("sunstone", "ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter", "active_record/connection_adapters/sunstone_adapter")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sunstone
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.0
4
+ version: 8.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Bracy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-13 00:00:00.000000000 Z
11
+ date: 2025-01-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -226,14 +226,14 @@ dependencies:
226
226
  requirements:
227
227
  - - ">="
228
228
  - !ruby/object:Gem::Version
229
- version: 7.2.0
229
+ version: 8.0.1
230
230
  type: :runtime
231
231
  prerelease: false
232
232
  version_requirements: !ruby/object:Gem::Requirement
233
233
  requirements:
234
234
  - - ">="
235
235
  - !ruby/object:Gem::Version
236
- version: 7.2.0
236
+ version: 8.0.1
237
237
  - !ruby/object:Gem::Dependency
238
238
  name: arel-extensions
239
239
  requirement: !ruby/object:Gem::Requirement
@@ -272,12 +272,16 @@ files:
272
272
  - LICENSE
273
273
  - ext/active_record/associations.rb
274
274
  - ext/active_record/associations/collection_association.rb
275
+ - ext/active_record/associations/has_many_through_association.rb
275
276
  - ext/active_record/attribute_methods.rb
277
+ - ext/active_record/base.rb
276
278
  - ext/active_record/callbacks.rb
277
- - ext/active_record/finder_methods.rb
279
+ - ext/active_record/locking/optimistic.rb
278
280
  - ext/active_record/persistence.rb
279
281
  - ext/active_record/relation.rb
280
282
  - ext/active_record/relation/calculations.rb
283
+ - ext/active_record/relation/finder_methods.rb
284
+ - ext/active_record/relation/predicate_builder.rb
281
285
  - ext/active_record/relation/query_methods.rb
282
286
  - ext/active_record/statement_cache.rb
283
287
  - ext/active_record/transactions.rb
@@ -324,7 +328,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
324
328
  - !ruby/object:Gem::Version
325
329
  version: '0'
326
330
  requirements: []
327
- rubygems_version: 3.5.11
331
+ rubygems_version: 3.5.21
328
332
  signing_key:
329
333
  specification_version: 4
330
334
  summary: A library for interacting with REST APIs
@@ -1,241 +0,0 @@
1
- # The last ref that this code was synced with Rails
2
- # ref: 9269f634d471ad6ca46752421eabd3e1c26220b5
3
-
4
- module ActiveRecord
5
- class PredicateBuilder # :nodoc:
6
-
7
- def expand_from_hash(attributes, &block)
8
- return ["1=0"] if attributes.empty?
9
-
10
- attributes.flat_map do |key, value|
11
- if key.is_a?(Array)
12
- queries = Array(value).map do |ids_set|
13
- raise ArgumentError, "Expected corresponding value for #{key} to be an Array" unless ids_set.is_a?(Array)
14
- expand_from_hash(key.zip(ids_set).to_h)
15
- end
16
- grouping_queries(queries)
17
- elsif value.is_a?(Hash) && !table.has_column?(key)
18
- ka = table.associated_table(key, &block)
19
- .predicate_builder.expand_from_hash(value.stringify_keys)
20
-
21
- if self.table.instance_variable_get(:@klass).connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
22
- ka.each { |k|
23
- if k.left.is_a?(Arel::Attributes::Attribute) || k.left.is_a?(Arel::Attributes::Relation)
24
- k.left = Arel::Attributes::Relation.new(k.left, key)
25
- end
26
- }
27
- end
28
- ka
29
- elsif table.associated_with?(key)
30
- # Find the foreign key when using queries such as:
31
- # Post.where(author: author)
32
- #
33
- # For polymorphic relationships, find the foreign key and type:
34
- # PriceEstimate.where(estimate_of: treasure)
35
- associated_table = table.associated_table(key)
36
- if associated_table.polymorphic_association?
37
- value = [value] unless value.is_a?(Array)
38
- klass = PolymorphicArrayValue
39
- elsif associated_table.through_association?
40
- next associated_table.predicate_builder.expand_from_hash(
41
- associated_table.primary_key => value
42
- )
43
- end
44
-
45
- klass ||= AssociationQueryValue
46
- queries = klass.new(associated_table, value).queries.map! do |query|
47
- # If the query produced is identical to attributes don't go any deeper.
48
- # Prevents stack level too deep errors when association and foreign_key are identical.
49
- query == attributes ? self[key, value] : expand_from_hash(query)
50
- end
51
-
52
- grouping_queries(queries)
53
- elsif table.aggregated_with?(key)
54
- mapping = table.reflect_on_aggregation(key).mapping
55
- values = value.nil? ? [nil] : Array.wrap(value)
56
- if mapping.length == 1 || values.empty?
57
- column_name, aggr_attr = mapping.first
58
- values = values.map do |object|
59
- object.respond_to?(aggr_attr) ? object.public_send(aggr_attr) : object
60
- end
61
- self[column_name, values]
62
- else
63
- queries = values.map do |object|
64
- mapping.map do |field_attr, aggregate_attr|
65
- self[field_attr, object.try!(aggregate_attr)]
66
- end
67
- end
68
-
69
- grouping_queries(queries)
70
- end
71
- else
72
- self[key, value]
73
- end
74
- end
75
- end
76
-
77
- end
78
- end
79
-
80
- module ActiveRecord
81
- module FinderMethods
82
-
83
- class SunstoneJoinDependency
84
- def initialize(klass)
85
- @klass = klass
86
- end
87
-
88
- def reflections
89
- []
90
- end
91
-
92
- def apply_column_aliases(relation)
93
- relation
94
- end
95
-
96
- def instantiate(result_set, strict_loading_value, &block)
97
- seen = Hash.new { |i, object_id|
98
- i[object_id] = Hash.new { |j, child_class|
99
- j[child_class] = {}
100
- }
101
- }
102
-
103
- model_cache = Hash.new { |h, klass| h[klass] = {} }
104
- parents = model_cache[@klass]
105
-
106
- message_bus = ActiveSupport::Notifications.instrumenter
107
-
108
- payload = {
109
- record_count: result_set.length,
110
- class_name: @klass.name
111
- }
112
-
113
- message_bus.instrument("instantiation.active_record", payload) do
114
- result_set.each { |row_hash|
115
- parent_key = @klass.primary_key ? row_hash[@klass.primary_key] : row_hash
116
- parent = parents[parent_key] ||= @klass.instantiate(row_hash.select{|k,v| @klass.column_names.include?(k.to_s) }, &block)
117
- construct(parent, row_hash.select{|k,v| !@klass.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
118
- }
119
- end
120
-
121
- parents.values
122
- end
123
-
124
- def construct(parent, relations, seen, model_cache, strict_loading_value)
125
- relations.each do |key, attributes|
126
- reflection = parent.class.reflect_on_association(key)
127
- next unless reflection
128
-
129
- if reflection.collection?
130
- other = parent.association(reflection.name)
131
- other.loaded!
132
- else
133
- if parent.association_cached?(reflection.name)
134
- model = parent.association(reflection.name).target
135
- construct(model, attributes.select{|k,v| !model.class.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
136
- end
137
- end
138
-
139
- if !reflection.collection?
140
- construct_association(parent, reflection, attributes, seen, model_cache, strict_loading_value)
141
- else
142
- attributes.each do |row|
143
- construct_association(parent, reflection, row, seen, model_cache, strict_loading_value)
144
- end
145
- end
146
-
147
- end
148
- end
149
-
150
- def construct_association(parent, reflection, attributes, seen, model_cache, strict_loading_value)
151
- return if attributes.nil?
152
-
153
- klass = if reflection.polymorphic?
154
- parent.send(reflection.foreign_type).constantize.base_class
155
- else
156
- reflection.klass
157
- end
158
- id = attributes[klass.primary_key]
159
- model = seen[parent.object_id][klass][id]
160
-
161
- if model
162
- construct(model, attributes.select{|k,v| !klass.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
163
-
164
- other = parent.association(reflection.name)
165
-
166
- if reflection.collection?
167
- other.target.push(model)
168
- else
169
- other.target = model
170
- end
171
-
172
- other.set_inverse_instance(model)
173
- else
174
- model = construct_model(parent, reflection, id, attributes.select{|k,v| klass.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
175
- seen[parent.object_id][model.class.base_class][id] = model
176
- construct(model, attributes.select{|k,v| !klass.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
177
- end
178
- end
179
-
180
-
181
- def construct_model(record, reflection, id, attributes, seen, model_cache, strict_loading_value)
182
- klass = if reflection.polymorphic?
183
- record.send(reflection.foreign_type).constantize
184
- else
185
- reflection.klass
186
- end
187
-
188
- model = model_cache[klass][id] ||= klass.instantiate(attributes)
189
- other = record.association(reflection.name)
190
-
191
- if reflection.collection?
192
- other.target.push(model)
193
- else
194
- other.target = model
195
- end
196
-
197
- other.set_inverse_instance(model)
198
- model
199
- end
200
-
201
- end
202
-
203
- def apply_join_dependency(eager_loading: group_values.empty?)
204
- if connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
205
- join_dependency = SunstoneJoinDependency.new(base_class)
206
- relation = except(:includes, :eager_load, :preload)
207
- relation.arel.eager_load = Arel::Nodes::EagerLoad.new(eager_load_values)
208
- else
209
- join_dependency = construct_join_dependency(
210
- eager_load_values | includes_values, Arel::Nodes::OuterJoin
211
- )
212
- relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
213
- end
214
-
215
- if eager_loading && has_limit_or_offset? && !(
216
- using_limitable_reflections?(join_dependency.reflections) &&
217
- using_limitable_reflections?(
218
- construct_join_dependency(
219
- select_association_list(joins_values).concat(
220
- select_association_list(left_outer_joins_values)
221
- ), nil
222
- ).reflections
223
- )
224
- )
225
- if !connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
226
- relation = skip_query_cache_if_necessary do
227
- klass.connection.distinct_relation_for_primary_key(relation)
228
- end
229
- end
230
- end
231
-
232
- if block_given?
233
- yield relation, join_dependency
234
- else
235
- relation
236
- end
237
- end
238
-
239
- end
240
-
241
- end