sunstone 5.1.0.4 → 5.2.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.
Files changed (33) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +5 -13
  3. data/ext/active_record/associations.rb +2 -7
  4. data/ext/active_record/associations/collection_association.rb +25 -18
  5. data/ext/active_record/attribute_methods.rb +19 -35
  6. data/ext/active_record/finder_methods.rb +138 -90
  7. data/ext/active_record/persistence.rb +16 -11
  8. data/ext/active_record/relation.rb +7 -47
  9. data/ext/active_record/relation/calculations.rb +5 -3
  10. data/ext/active_record/relation/query_methods.rb +9 -0
  11. data/ext/active_record/statement_cache.rb +4 -6
  12. data/ext/active_record/transactions.rb +9 -19
  13. data/ext/arel/nodes/select_statement.rb +26 -12
  14. data/lib/active_record/connection_adapters/sunstone/database_statements.rb +76 -25
  15. data/lib/active_record/connection_adapters/sunstone/type/json.rb +1 -1
  16. data/lib/active_record/connection_adapters/sunstone_adapter.rb +2 -3
  17. data/lib/arel/collectors/sunstone.rb +21 -19
  18. data/lib/arel/visitors/sunstone.rb +4 -8
  19. data/lib/sunstone.rb +2 -3
  20. data/lib/sunstone/connection.rb +2 -2
  21. data/lib/sunstone/exception.rb +8 -1
  22. data/lib/sunstone/version.rb +1 -1
  23. data/sunstone.gemspec +3 -3
  24. data/test/active_record/associations/has_many_test.rb +30 -2
  25. data/test/active_record/eager_loading_test.rb +10 -0
  26. data/test/active_record/persistance_test.rb +7 -7
  27. data/test/active_record/query/count_test.rb +13 -0
  28. data/test/active_record/query_test.rb +6 -6
  29. data/test/sunstone/connection/send_request_test.rb +1 -1
  30. data/test/test_helper.rb +1 -1
  31. metadata +9 -10
  32. data/ext/active_record/associations/association.rb +0 -16
  33. data/ext/active_record/batches.rb +0 -12
@@ -14,12 +14,14 @@ module ActiveRecord
14
14
 
15
15
  private
16
16
 
17
- def create_or_update(*args)
17
+ def create_or_update(*args, &block)
18
+ _raise_readonly_record_error if readonly?
19
+ return false if destroyed?
20
+
18
21
  @updating = new_record? ? :creating : :updating
19
22
  Thread.current[:sunstone_updating_model] = self
20
23
 
21
- raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
22
- result = new_record? ? _create_record : _update_record(*args)
24
+ result = new_record? ? _create_record(&block) : _update_record(*args, &block)
23
25
 
24
26
  if self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter) && result != 0
25
27
  row_hash = result.rows.first
@@ -61,9 +63,10 @@ module ActiveRecord
61
63
  # Creates a record with values matching those of the instance attributes
62
64
  # and returns its id.
63
65
  def _create_record(attribute_names = self.attribute_names)
64
- attributes_values = arel_attributes_with_values_for_create(attribute_names)
65
-
66
- new_id = self.class.unscoped.insert attributes_values
66
+ attribute_names &= self.class.column_names
67
+ attributes_values = attributes_with_values_for_create(attribute_names)
68
+
69
+ new_id = self.class._insert_record(attributes_values)
67
70
 
68
71
  @new_record = false
69
72
 
@@ -76,18 +79,20 @@ module ActiveRecord
76
79
  end
77
80
 
78
81
  def _update_record(attribute_names = self.attribute_names)
79
- attributes_values = arel_attributes_with_values_for_update(attribute_names)
82
+ attribute_names &= self.class.column_names
83
+ attributes_values = attributes_with_values_for_update(attribute_names)
84
+
80
85
  if attributes_values.empty?
81
- rows_affected = 0
86
+ affected_rows = 0
82
87
  @_trigger_update_callback = true
83
88
  else
84
- rows_affected = self.class.unscoped._update_record(attributes_values, id, id_in_database)
85
- @_trigger_update_callback = (rows_affected.is_a?(ActiveRecord::Result) ? rows_affected.rows.size : rows_affected) > 0
89
+ affected_rows = self.class._update_record( attributes_values, self.class.primary_key => id_in_database )
90
+ @_trigger_update_callback = affected_rows == 1
86
91
  end
87
92
 
88
93
  yield(self) if block_given?
89
94
 
90
- rows_affected
95
+ affected_rows
91
96
  end
92
97
 
93
98
  #!!!! TODO: I am duplicated from finder_methods.....
@@ -1,59 +1,19 @@
1
1
  module ActiveRecord
2
2
  class Relation
3
3
 
4
- def to_sql
5
- @to_sql ||= begin
6
- relation = self
7
-
8
- if eager_loading?
9
- find_with_associations { |rel| relation = rel }
10
- end
11
-
12
- conn = klass.connection
13
- conn.unprepared_statement {
14
- conn.to_sql(relation.arel, relation.bound_attributes)
15
- }
16
- end
17
- end
18
-
19
4
  def to_sar
20
5
  @to_sar ||= begin
21
- relation = self
22
-
23
6
  if eager_loading?
24
- find_with_associations { |rel| relation = rel }
7
+ apply_join_dependency do |relation, join_dependency|
8
+ relation = join_dependency.apply_column_aliases(relation)
9
+ relation.to_sar
10
+ end
11
+ else
12
+ conn = klass.connection
13
+ conn.unprepared_statement { conn.to_sar(arel) }
25
14
  end
26
-
27
- conn = klass.connection
28
- conn.unprepared_statement {
29
- conn.to_sar(relation.arel, relation.bound_attributes)
30
- }
31
15
  end
32
16
  end
33
17
 
34
- def _update_record(values, id, id_was) # :nodoc:
35
- substitutes, binds = substitute_values values
36
-
37
- scope = @klass.unscoped
38
-
39
- if @klass.finder_needs_type_condition?
40
- scope.unscope!(where: @klass.inheritance_column)
41
- end
42
-
43
- relation = scope.where(@klass.primary_key => (id_was || id))
44
- bvs = binds + relation.bound_attributes
45
- um = relation
46
- .arel
47
- .compile_update(substitutes, @klass.primary_key)
48
- um.table @klass.arel_table
49
-
50
- @klass.connection.update(
51
- um,
52
- 'SQL',
53
- bvs,
54
- )
55
- end
56
-
57
-
58
18
  end
59
19
  end
@@ -7,19 +7,21 @@ module ActiveRecord
7
7
  end
8
8
 
9
9
  if has_include?(column_names.first)
10
- construct_relation_for_association_calculations.pluck(*column_names)
10
+ relation = apply_join_dependency
11
+ relation.pluck(*column_names)
11
12
  elsif klass.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
12
13
  load
13
14
  return records.pluck(*column_names.map{|n| n.sub(/^#{klass.table_name}\./, "")})
14
15
  else
16
+ enforce_raw_sql_whitelist(column_names)
15
17
  relation = spawn
16
18
  relation.select_values = column_names.map { |cn|
17
19
  @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn
18
20
  }
19
- result = klass.connection.select_all(relation.arel, nil, bound_attributes)
21
+ result = skip_query_cache_if_necessary { klass.connection.select_all(relation.arel, nil) }
20
22
  result.cast_values(klass.attribute_types)
21
23
  end
22
24
  end
23
-
25
+
24
26
  end
25
27
  end
@@ -0,0 +1,9 @@
1
+ module ActiveRecord
2
+ module QueryMethods
3
+ private
4
+ def assert_mutability!
5
+ raise ImmutableRelation if @loaded
6
+ raise ImmutableRelation if defined?(@arel) && !klass.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter) && @arel
7
+ end
8
+ end
9
+ end
@@ -5,9 +5,7 @@ module ActiveRecord
5
5
  def initialize(values, sunstone=false)
6
6
  @values = values
7
7
  @indexes = if sunstone
8
- values.value.find_all { |thing|
9
- Arel::Nodes::BindParam === thing
10
- }
8
+
11
9
  else
12
10
  values.each_with_index.find_all { |thing,i|
13
11
  Arel::Nodes::BindParam === thing
@@ -16,11 +14,11 @@ module ActiveRecord
16
14
  end
17
15
 
18
16
  def sql_for(binds, connection)
19
- casted_binds = binds.map(&:value_for_database)
20
-
21
17
  if connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
22
- @values.compile(binds)
18
+ binds.map!(&:value_for_database)
19
+ @values
23
20
  else
21
+ casted_binds = binds.map(&:value_for_database)
24
22
  val = @values.dup
25
23
  @indexes.each { |i| val[i] = connection.quote(casted_binds.shift) }
26
24
  val.join
@@ -34,29 +34,19 @@ module ActiveRecord
34
34
  # end
35
35
 
36
36
  def with_transaction_returning_status
37
- if self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter) && instance_variable_defined?(:@updating) && @updating
38
- begin
39
- status = yield
40
- rescue ActiveRecord::Rollback
41
- clear_transaction_record_state
42
- status = nil
43
- end
44
- return status
45
- end
46
-
47
37
  status = nil
48
- self.class.transaction do
49
- add_to_transaction
50
- begin
38
+
39
+ if self.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter) && instance_variable_defined?(:@updating) && @updating
40
+ status = yield
41
+ status
42
+ else
43
+ self.class.transaction do
44
+ add_to_transaction
51
45
  status = yield
52
- rescue ActiveRecord::Rollback
53
- clear_transaction_record_state
54
- status = nil
46
+ raise ActiveRecord::Rollback unless status
55
47
  end
56
-
57
- raise ActiveRecord::Rollback unless status
48
+ status
58
49
  end
59
- status
60
50
  ensure
61
51
  if @transaction_state && @transaction_state.committed?
62
52
  clear_transaction_record_state
@@ -4,24 +4,38 @@ module Arel
4
4
 
5
5
  attr_accessor :eager_load
6
6
 
7
- def initialize_with_eager_load cores = [SelectCore.new]
8
- initialize_without_eager_load
9
- @eager_load = nil
7
+ def initialize cores = [SelectCore.new]
8
+ super()
9
+ @cores = cores
10
+ @orders = []
11
+ @limit = nil
12
+ @lock = nil
13
+ @offset = nil
14
+ @with = nil
15
+ @eager_load = nil
10
16
  end
11
-
12
- alias_method :initialize_without_eager_load, :initialize
13
- alias_method :initialize, :initialize_with_eager_load
14
-
17
+
18
+ def initialize_copy other
19
+ super
20
+ @cores = @cores.map { |x| x.clone }
21
+ @orders = @orders.map { |x| x.clone }
22
+ @eager_load = @eager_load&.map { |x| x.clone }
23
+ end
24
+
15
25
  def hash
16
26
  [@cores, @orders, @limit, @lock, @offset, @with, @eager_load].hash
17
27
  end
18
28
 
19
- def eql_with_eager_load? other
20
- eql_without_eager_load?(other) && self.eager_load == other.eager_load
29
+ def eql? other
30
+ self.class == other.class &&
31
+ self.cores == other.cores &&
32
+ self.orders == other.orders &&
33
+ self.limit == other.limit &&
34
+ self.lock == other.lock &&
35
+ self.offset == other.offset &&
36
+ self.with == other.with &&
37
+ self.eager_load == other.eager_load
21
38
  end
22
- alias_method :eql_without_eager_load?, :eql?
23
- alias_method :eql?, :eql_with_eager_load?
24
- alias_method :==, :eql?
25
39
 
26
40
  end
27
41
  end
@@ -7,49 +7,97 @@ module ActiveRecord
7
7
 
8
8
  def to_sql(arel, binds = [])
9
9
  if arel.respond_to?(:ast)
10
- collected = Arel::Visitors::ToSql.new(self).accept(arel.ast, prepared_statements ? AbstractAdapter::SQLString.new : AbstractAdapter::BindCollector.new)
11
- collected.compile(binds, self).freeze
10
+ unless binds.empty?
11
+ raise "Passing bind parameters with an arel AST is forbidden. " \
12
+ "The values must be stored on the AST directly"
13
+ end
14
+ Arel::Visitors::ToSql.new(self).accept(arel.ast, Arel::Collectors::SubstituteBinds.new(self, Arel::Collectors::SQLString.new)).value
12
15
  else
13
16
  arel.dup.freeze
14
17
  end
15
18
  end
16
19
 
17
20
  # Converts an arel AST to a Sunstone API Request
18
- def to_sar(arel, bvs = [])
19
- if arel.respond_to?(:ast)
20
- collected = visitor.accept(arel.ast, collector)
21
- collected.compile(bvs, self)
21
+ def to_sar(arel_or_sar_string, binds = nil)
22
+ if arel_or_sar_string.respond_to?(:ast)
23
+ sar = visitor.accept(arel_or_sar_string.ast, collector)
24
+ binds = sar.binds if binds.nil?
22
25
  else
23
- arel
26
+ sar = arel_or_sar_string
24
27
  end
28
+ sar.compile(binds)
25
29
  end
26
30
 
31
+ def to_sar_and_binds(arel_or_sar_string, binds = []) # :nodoc:
32
+ if arel_or_sar_string.respond_to?(:ast)
33
+ unless binds.empty?
34
+ raise "Passing bind parameters with an arel AST is forbidden. " \
35
+ "The values must be stored on the AST directly"
36
+ end
37
+ sar = visitor.accept(arel_or_sar_string.ast, collector)
38
+ # puts ['a', sar.freeze, sar.binds].map(&:inspect)
39
+ [sar.freeze, sar.binds]
40
+ else
41
+ # puts ['b',arel_or_sar_string.dup.freeze, binds].map(&:inspect)
42
+ [arel_or_sar_string.dup.freeze, binds]
43
+ end
44
+ end
45
+
46
+ # This is used in the StatementCache object. It returns an object that
47
+ # can be used to query the database repeatedly.
27
48
  def cacheable_query(klass, arel) # :nodoc:
28
- collected = visitor.accept(arel.ast, collector)
29
49
  if prepared_statements
30
- klass.query(collected.value)
50
+ sql, binds = visitor.accept(arel.ast, collector).value
51
+ query = klass.query(sql)
52
+ elsif self.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
53
+ collector = SunstonePartialQueryCollector.new(self.collector)
54
+ parts, binds = visitor.accept(arel.ast, collector).value
55
+ query = StatementCache::PartialQuery.new(parts, true)
31
56
  else
32
- if self.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
33
- StatementCache::PartialQuery.new(collected, true)
34
- else
35
- StatementCache::PartialQuery.new(collected.value, false)
36
- end
57
+ collector = PartialQueryCollector.new
58
+ parts, binds = visitor.accept(arel.ast, collector).value
59
+ query = klass.partial_query(parts)
60
+ end
61
+ [query, binds]
62
+ end
63
+
64
+ class SunstonePartialQueryCollector
65
+ delegate_missing_to :@collector
66
+
67
+ def initialize(collector)
68
+ @collector = collector
69
+ @binds = []
70
+ end
71
+
72
+ def add_bind(obj)
73
+ @binds << obj
74
+ end
75
+
76
+ def value
77
+ [@collector, @binds]
37
78
  end
38
79
  end
39
80
 
40
81
  # Returns an ActiveRecord::Result instance.
41
82
  def select_all(arel, name = nil, binds = [], preparable: nil)
42
- arel, binds = binds_from_relation arel, binds
43
- select(arel, name, binds)
83
+ arel = arel_from_relation(arel)
84
+ sar, binds = to_sar_and_binds(arel, binds)
85
+ select(sar, name, binds)
44
86
  end
45
87
 
46
88
  def exec_query(arel, name = 'SAR', binds = [], prepare: false)
47
89
  sars = []
48
- multiple_requests = arel.is_a?(Arel::SelectManager)
49
-
90
+ multiple_requests = arel.is_a?(Arel::Collectors::Sunstone)
91
+ type_casted_binds = binds#type_casted_binds(binds)
92
+
50
93
  if multiple_requests
51
- allowed_limit = limit_definition(arel.source.left.name)
52
- requested_limit = binds.find { |x| x.name == 'LIMIT' }&.value
94
+ allowed_limit = limit_definition(arel.table)
95
+ limit_bind_index = nil#binds.find_index { |x| x.name == 'LIMIT' }
96
+ requested_limit = if limit_bind_index
97
+ type_casted_binds[limit_bind_index]
98
+ else
99
+ arel.limit&.value&.value_for_database
100
+ end
53
101
 
54
102
  if allowed_limit.nil?
55
103
  multiple_requests = false
@@ -61,7 +109,7 @@ module ActiveRecord
61
109
  end
62
110
 
63
111
  send_request = lambda { |req_arel|
64
- sar = to_sar(req_arel, binds)
112
+ sar = to_sar(req_arel, type_casted_binds)
65
113
  sars.push(sar)
66
114
  log_mess = sar.path.split('?', 2)
67
115
  log("#{sar.method} #{log_mess[0]} #{(log_mess[1] && !log_mess[1].empty?) ? MessagePack.unpack(CGI.unescape(log_mess[1])) : '' }", name) do
@@ -75,12 +123,11 @@ module ActiveRecord
75
123
  }
76
124
 
77
125
  result = if multiple_requests
78
- bind = binds.find { |x| x.name == 'LIMIT' }
79
- binds.delete(bind)
126
+ binds.delete_at(limit_bind_index) if limit_bind_index
80
127
 
81
128
  limit, offset, results = allowed_limit, 0, []
82
129
  while requested_limit ? offset < requested_limit : true
83
- split_arel = arel.clone
130
+ split_arel = arel.dup
84
131
  split_arel.limit = limit
85
132
  split_arel.offset = offset
86
133
  request_results = send_request.call(split_arel)
@@ -104,8 +151,12 @@ module ActiveRecord
104
151
  end
105
152
 
106
153
  def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
107
- value = exec_insert(arel, name, binds, pk, sequence_name)
154
+ sar, binds = to_sar_and_binds(arel, binds)
155
+ value = exec_insert(sar, name, binds, pk, sequence_name)
108
156
  id_value || last_inserted_id(value)
157
+
158
+ # value = exec_insert(arel, name, binds, pk, sequence_name)
159
+ # id_value || last_inserted_id(value)
109
160
  end
110
161
 
111
162
  def update(arel, name = nil, binds = [])
@@ -2,7 +2,7 @@ module ActiveRecord
2
2
  module ConnectionAdapters
3
3
  module Sunstone
4
4
  module Type
5
- class Json < ActiveRecord::Type::Internal::AbstractJson
5
+ class Json < ActiveRecord::Type::Json
6
6
 
7
7
 
8
8
  def deserialize(value)
@@ -130,7 +130,7 @@ module ActiveRecord
130
130
  def collector
131
131
  Arel::Collectors::Sunstone.new
132
132
  end
133
-
133
+
134
134
  def server_config
135
135
  JSON.parse(@connection.get("/configuration").body)
136
136
  end
@@ -173,8 +173,7 @@ module ActiveRecord
173
173
  # If the next id was calculated in advance (as in Oracle), it should be
174
174
  # passed in as +id_value+.
175
175
  def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
176
- sql, binds, pk, sequence_name = sql_for_insert(arel, pk, id_value, sequence_name, binds)
177
- exec_insert(sql, name, binds, pk, sequence_name)
176
+ exec_insert(arel, name, binds, pk, sequence_name)
178
177
  end
179
178
  alias create insert
180
179