sunstone 8.0.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: 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