sunstone 5.1.0.4 → 5.2.1

Sign up to get free protection for your applications and to get access to all the features.
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