sunstone 8.0.0 → 8.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd0eae0dbdee21a95ca29317302b67b50647f7d6654af15553425f55d1d11aed
4
- data.tar.gz: b0cdc960489411360b1ee4f2cb4cdcce93b7df2464dabdafa2bc26955be002b3
3
+ metadata.gz: 9e9d652c2cfc067ca7a6921350122369134b44a200a262d93da2f9df98c90d5c
4
+ data.tar.gz: b13d474440401eb82768422ece2047e9b0e1f1cf470264e4a2d786dcdf5c13eb
5
5
  SHA512:
6
- metadata.gz: ce768f425153db1d5a2242f845bc3de221a4d0d0d8a30a1428c128d2bb1e4353ef986020283fdd1d17e7652937c38d03eb1f42478acbc694da32135f335cab22
7
- data.tar.gz: ece020a27e0554bad07f0e831919e8969cebdd6970adc344fb4f018b3a45032588dcd7c4eb512d9fcb8ff4acfc09ef27d9fe6124fa2b9db0d5eeff659885d800
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
@@ -4,27 +4,6 @@
4
4
  module ActiveRecord
5
5
  module Calculations
6
6
 
7
- # Prior to Rails 8 we didn't need this method becuase it would
8
- # return the first value if there was just one - so we'll just
9
- # do the same as prevously because it doesn't have to be joined
10
- def select_for_count
11
- if select_values.empty?
12
- :all
13
- else
14
- with_connection do |conn|
15
- # Rails compiles this to a string, but we don't have string we
16
- # have a hash
17
- if model.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
18
- sv = arel_columns(select_values)
19
- sv.one? ? sv.first : sv
20
- else
21
- sv = arel_columns(select_values).map { |column| conn.visitor.compile(column) }
22
- sv.one? ? sv.first : sv.join(", ")
23
- end
24
- end
25
- end
26
- end
27
-
28
7
  def pluck(*column_names)
29
8
  if @none
30
9
  if @async
@@ -46,7 +25,7 @@ module ActiveRecord
46
25
  if has_include?(column_names.first)
47
26
  relation = apply_join_dependency
48
27
  relation.pluck(*column_names)
49
- elsif model.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
28
+ elsif model.sunstone?
50
29
  load
51
30
  return records.pluck(*column_names.map{|n| n.to_s.sub(/^#{model.table_name}\./, "")})
52
31
  else
@@ -69,5 +48,22 @@ module ActiveRecord
69
48
  end
70
49
  end
71
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
+
72
68
  end
73
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 && !model.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?
@@ -118,6 +118,11 @@ module ActiveRecord
118
118
  sar, binds = sar_for_insert(arel, pk, binds, returning)
119
119
  internal_exec_query(sar, name, binds)
120
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
121
126
 
122
127
  # Lowest level way to execute a query. Doesn't check for illegal writes, doesn't annotate queries, yields a native result object.
123
128
  def raw_execute(arel, name = nil, binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, batch: false)
@@ -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']
@@ -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 = '8.0.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: 8.0.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: 2025-01-24 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
@@ -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
@@ -1,246 +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) && key.size == 1
12
- key = key.first
13
- value = value.flatten
14
- end
15
-
16
- if key.is_a?(Array)
17
- queries = Array(value).map do |ids_set|
18
- raise ArgumentError, "Expected corresponding value for #{key} to be an Array" unless ids_set.is_a?(Array)
19
- expand_from_hash(key.zip(ids_set).to_h)
20
- end
21
- grouping_queries(queries)
22
- elsif value.is_a?(Hash) && !table.has_column?(key)
23
- ka = table.associated_table(key, &block)
24
- .predicate_builder.expand_from_hash(value.stringify_keys)
25
-
26
- if self.table.instance_variable_get(:@klass).connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
27
- ka.each { |k|
28
- if k.left.is_a?(Arel::Attributes::Attribute) || k.left.is_a?(Arel::Attributes::Relation)
29
- k.left = Arel::Attributes::Relation.new(k.left, key)
30
- end
31
- }
32
- end
33
- ka
34
- elsif table.associated_with?(key)
35
- # Find the foreign key when using queries such as:
36
- # Post.where(author: author)
37
- #
38
- # For polymorphic relationships, find the foreign key and type:
39
- # PriceEstimate.where(estimate_of: treasure)
40
- associated_table = table.associated_table(key)
41
- if associated_table.polymorphic_association?
42
- value = [value] unless value.is_a?(Array)
43
- klass = PolymorphicArrayValue
44
- elsif associated_table.through_association?
45
- next associated_table.predicate_builder.expand_from_hash(
46
- associated_table.primary_key => value
47
- )
48
- end
49
-
50
- klass ||= AssociationQueryValue
51
- queries = klass.new(associated_table, value).queries.map! do |query|
52
- # If the query produced is identical to attributes don't go any deeper.
53
- # Prevents stack level too deep errors when association and foreign_key are identical.
54
- query == attributes ? self[key, value] : expand_from_hash(query)
55
- end
56
-
57
- grouping_queries(queries)
58
- elsif table.aggregated_with?(key)
59
- mapping = table.reflect_on_aggregation(key).mapping
60
- values = value.nil? ? [nil] : Array.wrap(value)
61
- if mapping.length == 1 || values.empty?
62
- column_name, aggr_attr = mapping.first
63
- values = values.map do |object|
64
- object.respond_to?(aggr_attr) ? object.public_send(aggr_attr) : object
65
- end
66
- self[column_name, values]
67
- else
68
- queries = values.map do |object|
69
- mapping.map do |field_attr, aggregate_attr|
70
- self[field_attr, object.try!(aggregate_attr)]
71
- end
72
- end
73
-
74
- grouping_queries(queries)
75
- end
76
- else
77
- self[key, value]
78
- end
79
- end
80
- end
81
-
82
- end
83
- end
84
-
85
- module ActiveRecord
86
- module FinderMethods
87
-
88
- class SunstoneJoinDependency
89
- def initialize(klass)
90
- @klass = klass
91
- end
92
-
93
- def reflections
94
- []
95
- end
96
-
97
- def apply_column_aliases(relation)
98
- relation
99
- end
100
-
101
- def instantiate(result_set, strict_loading_value, &block)
102
- seen = Hash.new { |i, object_id|
103
- i[object_id] = Hash.new { |j, child_class|
104
- j[child_class] = {}
105
- }
106
- }
107
-
108
- model_cache = Hash.new { |h, klass| h[klass] = {} }
109
- parents = model_cache[@klass]
110
-
111
- message_bus = ActiveSupport::Notifications.instrumenter
112
-
113
- payload = {
114
- record_count: result_set.length,
115
- class_name: @klass.name
116
- }
117
-
118
- message_bus.instrument("instantiation.active_record", payload) do
119
- result_set.each { |row_hash|
120
- parent_key = @klass.primary_key ? row_hash[@klass.primary_key] : row_hash
121
- parent = parents[parent_key] ||= @klass.instantiate(row_hash.select{|k,v| @klass.column_names.include?(k.to_s) }, &block)
122
- construct(parent, row_hash.select{|k,v| !@klass.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
123
- }
124
- end
125
-
126
- parents.values
127
- end
128
-
129
- def construct(parent, relations, seen, model_cache, strict_loading_value)
130
- relations.each do |key, attributes|
131
- reflection = parent.class.reflect_on_association(key)
132
- next unless reflection
133
-
134
- if reflection.collection?
135
- other = parent.association(reflection.name)
136
- other.loaded!
137
- else
138
- if parent.association_cached?(reflection.name)
139
- model = parent.association(reflection.name).target
140
- construct(model, attributes.select{|k,v| !model.class.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
141
- end
142
- end
143
-
144
- if !reflection.collection?
145
- construct_association(parent, reflection, attributes, seen, model_cache, strict_loading_value)
146
- else
147
- attributes.each do |row|
148
- construct_association(parent, reflection, row, seen, model_cache, strict_loading_value)
149
- end
150
- end
151
-
152
- end
153
- end
154
-
155
- def construct_association(parent, reflection, attributes, seen, model_cache, strict_loading_value)
156
- return if attributes.nil?
157
-
158
- klass = if reflection.polymorphic?
159
- parent.send(reflection.foreign_type).constantize.base_class
160
- else
161
- reflection.klass
162
- end
163
- id = attributes[klass.primary_key]
164
- model = seen[parent.object_id][klass][id]
165
-
166
- if model
167
- construct(model, attributes.select{|k,v| !klass.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
168
-
169
- other = parent.association(reflection.name)
170
-
171
- if reflection.collection?
172
- other.target.push(model)
173
- else
174
- other.target = model
175
- end
176
-
177
- other.set_inverse_instance(model)
178
- else
179
- model = construct_model(parent, reflection, id, attributes.select{|k,v| klass.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
180
- seen[parent.object_id][model.class.base_class][id] = model
181
- construct(model, attributes.select{|k,v| !klass.column_names.include?(k.to_s) }, seen, model_cache, strict_loading_value)
182
- end
183
- end
184
-
185
-
186
- def construct_model(record, reflection, id, attributes, seen, model_cache, strict_loading_value)
187
- klass = if reflection.polymorphic?
188
- record.send(reflection.foreign_type).constantize
189
- else
190
- reflection.klass
191
- end
192
-
193
- model = model_cache[klass][id] ||= klass.instantiate(attributes)
194
- other = record.association(reflection.name)
195
-
196
- if reflection.collection?
197
- other.target.push(model)
198
- else
199
- other.target = model
200
- end
201
-
202
- other.set_inverse_instance(model)
203
- model
204
- end
205
-
206
- end
207
-
208
- def apply_join_dependency(eager_loading: group_values.empty?)
209
- if connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
210
- join_dependency = SunstoneJoinDependency.new(base_class)
211
- relation = except(:includes, :eager_load, :preload)
212
- relation.arel.eager_load = Arel::Nodes::EagerLoad.new(eager_load_values)
213
- else
214
- join_dependency = construct_join_dependency(
215
- eager_load_values | includes_values, Arel::Nodes::OuterJoin
216
- )
217
- relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
218
- end
219
-
220
- if eager_loading && has_limit_or_offset? && !(
221
- using_limitable_reflections?(join_dependency.reflections) &&
222
- using_limitable_reflections?(
223
- construct_join_dependency(
224
- select_association_list(joins_values).concat(
225
- select_association_list(left_outer_joins_values)
226
- ), nil
227
- ).reflections
228
- )
229
- )
230
- if !connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
231
- relation = skip_query_cache_if_necessary do
232
- klass.connection.distinct_relation_for_primary_key(relation)
233
- end
234
- end
235
- end
236
-
237
- if block_given?
238
- yield relation, join_dependency
239
- else
240
- relation
241
- end
242
- end
243
-
244
- end
245
-
246
- end