sunstone 6.0.0.4 → 6.1.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.
@@ -4,7 +4,7 @@ module Arel
4
4
 
5
5
  attr_accessor :eager_load
6
6
 
7
- def initialize cores = [SelectCore.new]
7
+ def initialize(cores = [SelectCore.new])
8
8
  super()
9
9
  @cores = cores
10
10
  @orders = []
@@ -2,8 +2,7 @@ module ActiveRecord
2
2
  module ConnectionAdapters
3
3
  # Sunstone-specific extensions to column definitions in a table.
4
4
  class SunstoneColumn < Column #:nodoc:
5
- delegate :array, to: :sql_type_metadata
6
- alias :array? :array
5
+ attr_reader :array
7
6
 
8
7
  def initialize(name, sql_type_metadata, options={})
9
8
  @name = name.freeze
@@ -14,6 +13,7 @@ module ActiveRecord
14
13
  @collation = nil
15
14
  @table_name = nil
16
15
  @primary_key = (options['primary_key'] == true)
16
+ @array = options['array']
17
17
  end
18
18
 
19
19
  def primary_key?
@@ -47,15 +47,15 @@ module ActiveRecord
47
47
  # can be used to query the database repeatedly.
48
48
  def cacheable_query(klass, arel) # :nodoc:
49
49
  if prepared_statements
50
- sql, binds = visitor.accept(arel.ast, collector).value
50
+ sql, binds = visitor.compile(arel.ast, collector)
51
51
  query = klass.query(sql)
52
52
  elsif self.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter)
53
53
  collector = SunstonePartialQueryCollector.new(self.collector)
54
- parts, binds = visitor.accept(arel.ast, collector).value
54
+ parts, binds = visitor.compile(arel.ast, collector)
55
55
  query = StatementCache::PartialQuery.new(parts, true)
56
56
  else
57
- collector = PartialQueryCollector.new
58
- parts, binds = visitor.accept(arel.ast, collector).value
57
+ collector = klass.partial_query_collector
58
+ parts, binds = visitor.compile(arel.ast, collector)
59
59
  query = klass.partial_query(parts)
60
60
  end
61
61
  [query, binds]
@@ -142,7 +142,7 @@ module ActiveRecord
142
142
 
143
143
  if sars[0].instance_variable_defined?(:@sunstone_calculation) && sars[0].instance_variable_get(:@sunstone_calculation)
144
144
  # this is a count, min, max.... yea i know..
145
- ActiveRecord::Result.new(['all'], [result], {:all => type_map.lookup('integer')})
145
+ ActiveRecord::Result.new(['all'], [result], {:all => type_map.lookup('integer', {})})
146
146
  elsif result.is_a?(Array)
147
147
  ActiveRecord::Result.new(result[0] ? result[0].keys : [], result.map{|r| r.values})
148
148
  else
@@ -31,7 +31,11 @@ module ActiveRecord
31
31
 
32
32
  version = Gem::Version.create(response['StandardAPI-Version'] || '5.0.0.4')
33
33
 
34
- @definitions[table_name] = if (version >= Gem::Version.create('5.0.0.5'))
34
+ @definitions[table_name] = if (version >= Gem::Version.create('6.0.0.29'))
35
+ schema = JSON.parse(response.body)
36
+ schema['columns'] = schema.delete('attributes')
37
+ schema
38
+ elsif (version >= Gem::Version.create('5.0.0.5'))
35
39
  JSON.parse(response.body)
36
40
  else
37
41
  { 'columns' => JSON.parse(response.body), 'limit' => nil }
@@ -46,7 +50,9 @@ module ActiveRecord
46
50
  # - format_type includes the column size constraint, e.g. varchar(50)
47
51
  # - ::regclass is a function that gives the id for a table name
48
52
  def column_definitions(table_name) # :nodoc:
49
- definition(table_name)['columns']
53
+ # First check for attributes and then for the deprecated columns field
54
+ # TODO: Remove after 0.3
55
+ definition(table_name)['attributes'] || definition(table_name)['columns']
50
56
  end
51
57
 
52
58
  # Returns the limit definition of the table (the maximum limit that can
@@ -58,28 +64,32 @@ module ActiveRecord
58
64
  def tables
59
65
  JSON.parse(@connection.get('/tables').body)
60
66
  end
61
-
67
+
62
68
  def views
63
69
  []
64
70
  end
65
-
71
+
66
72
  def new_column(name, options)
67
73
  sql_type_metadata = fetch_type_metadata(options)
68
74
  SunstoneColumn.new(name, sql_type_metadata, options)
69
75
  end
70
-
76
+
77
+ def lookup_cast_type(options)
78
+ type_map.lookup(options['type'], options.symbolize_keys)
79
+ end
80
+
71
81
  def fetch_type_metadata(options)
72
- cast_type = lookup_cast_type(options['type'])
82
+ cast_type = lookup_cast_type(options)
73
83
  simple_type = SqlTypeMetadata.new(
74
84
  sql_type: options['type'],
75
85
  type: cast_type.type,
76
86
  limit: cast_type.limit,
77
87
  precision: cast_type.precision,
78
- scale: cast_type.scale,
88
+ scale: cast_type.scale
79
89
  )
80
90
  SunstoneSQLTypeMetadata.new(simple_type, options)
81
91
  end
82
-
92
+
83
93
  def column_name_for_operation(operation, node) # :nodoc:
84
94
  visitor.accept(node, collector).first[operation.to_sym]
85
95
  end
@@ -0,0 +1,34 @@
1
+ require 'base64'
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Sunstone
6
+ module Type
7
+ class Binary < ActiveRecord::Type::Binary
8
+
9
+ # Converts a value from database input to the appropriate ruby type. The
10
+ # return value of this method will be returned from
11
+ # ActiveRecord::AttributeMethods::Read#read_attribute. The default
12
+ # implementation just calls Value#cast.
13
+ #
14
+ # +value+ The raw input, as provided from the database.
15
+ def deserialize(value)
16
+ value.nil? ? nil : Base64.strict_decode64(value)
17
+ end
18
+
19
+ # Casts a value from the ruby type to a type that the database knows how
20
+ # to understand. The returned value from this method should be a
21
+ # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
22
+ # +nil+.
23
+ def serialize(value)
24
+ if limit && value.bytesize > limit
25
+ raise ActiveModel::RangeError, "value is out of range for #{self.class} with limit #{limit} bytes"
26
+ end
27
+ Base64.strict_encode64(value)
28
+ end
29
+
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,6 +1,7 @@
1
1
  require 'active_record/connection_adapters/abstract_adapter'
2
2
 
3
- #require 'active_record/connection_adapters/statement_pool'
3
+ require 'arel/nodes/relation'
4
+ require 'arel/visitors/to_sql_extensions'
4
5
 
5
6
  require 'active_record/connection_adapters/sunstone/database_statements'
6
7
  require 'active_record/connection_adapters/sunstone/schema_statements'
@@ -9,6 +10,7 @@ require 'active_record/connection_adapters/sunstone/column'
9
10
 
10
11
  require 'active_record/connection_adapters/sunstone/type/date_time'
11
12
  require 'active_record/connection_adapters/sunstone/type/array'
13
+ require 'active_record/connection_adapters/sunstone/type/binary'
12
14
  require 'active_record/connection_adapters/sunstone/type/uuid'
13
15
  require 'active_record/connection_adapters/sunstone/type/json'
14
16
 
@@ -30,7 +32,7 @@ module ActiveRecord
30
32
  conn_params[:port] ||= uri.port
31
33
  conn_params[:use_ssl] ||= (uri.scheme == 'https')
32
34
  end
33
-
35
+
34
36
  # Forward only valid config params to Sunstone::Connection
35
37
  conn_params.slice!(*VALID_SUNSTONE_CONN_PARAMS)
36
38
 
@@ -78,9 +80,9 @@ module ActiveRecord
78
80
 
79
81
  # Initializes and connects a SunstoneAPI adapter.
80
82
  def initialize(connection, logger, connection_parameters, config)
81
- super(connection, logger, config)
83
+ super(connection, logger, config.reverse_merge(prepared_statements: false))
82
84
 
83
- @prepared_statements = false
85
+ @prepared_statement_status = Concurrent::ThreadLocalVar.new(false)
84
86
  @connection_parameters = connection_parameters
85
87
 
86
88
  # @type_map = Type::HashLookupTypeMap.new
@@ -95,12 +97,12 @@ module ActiveRecord
95
97
  super
96
98
  @connection.reconnect!
97
99
  end
98
-
100
+
99
101
  def disconnect!
100
102
  super
101
103
  @connection.disconnect!
102
104
  end
103
-
105
+
104
106
  # Executes the delete statement and returns the number of rows affected.
105
107
  def delete(arel, name = nil, binds = [])
106
108
  r = exec_delete(arel, name, binds)
@@ -122,27 +124,28 @@ module ActiveRecord
122
124
  def update_table_definition(table_name, base) #:nodoc:
123
125
  SunstoneAPI::Table.new(table_name, base)
124
126
  end
125
-
127
+
126
128
  def arel_visitor
127
129
  Arel::Visitors::Sunstone.new
128
130
  end
129
-
131
+
130
132
  def collector
131
133
  Arel::Collectors::Sunstone.new
132
134
  end
133
-
135
+
134
136
  def server_config
135
137
  JSON.parse(@connection.get("/configuration").body)
136
138
  end
137
-
139
+
138
140
  def lookup_cast_type_from_column(column) # :nodoc:
139
- if column.array
140
- Sunstone::Type::Array.new(type_map.lookup(column.sql_type))
141
- else
142
- type_map.lookup(column.sql_type)
143
- end
141
+ cast_type = type_map.lookup(column.sql_type, {
142
+ limit: column.limit,
143
+ precision: column.precision,
144
+ scale: column.scale
145
+ })
146
+ column.array ? Sunstone::Type::Array.new(cast_type) : cast_type
144
147
  end
145
-
148
+
146
149
  def transaction(requires_new: nil, isolation: nil, joinable: true)
147
150
  Thread.current[:sunstone_transaction_count] ||= 0
148
151
  Thread.current[:sunstone_request_sent] = nil if Thread.current[:sunstone_transaction_count] == 0
@@ -159,7 +162,7 @@ module ActiveRecord
159
162
  rescue ActiveRecord::Rollback
160
163
  # rollbacks are silently swallowed
161
164
  end
162
-
165
+
163
166
  def supports_json?
164
167
  true
165
168
  end
@@ -176,33 +179,43 @@ module ActiveRecord
176
179
  exec_insert(arel, name, binds, pk, sequence_name)
177
180
  end
178
181
  alias create insert
179
-
182
+
180
183
  # Should be the defuat insert, but rails escapes if for SQL so we'll just
181
184
  # catch the string "DEFATUL VALUES" in the visitor
182
185
  # def empty_insert_statement_value
183
186
  # {}
184
187
  # end
185
-
188
+
186
189
  private
187
190
 
188
191
  def initialize_type_map(m) # :nodoc:
189
192
  m.register_type 'boolean', Type::Boolean.new
190
- m.register_type 'string', Type::String.new
191
- m.register_type 'integer', Type::Integer.new
192
- m.register_type 'decimal', Type::Decimal.new
193
+ m.register_type 'string' do |_, options|
194
+ Type::String.new(**options.slice(:limit))
195
+ end
196
+ m.register_type 'integer' do |_, options|
197
+ Type::Integer.new(**options.slice(:limit))
198
+ end
199
+ m.register_type 'decimal' do |_, options|
200
+ Type::Decimal.new(**options.slice(:precision, :scale))
201
+ end
202
+ m.register_type 'binary' do |_, options|
203
+ Sunstone::Type::Binary.new(**options.slice(:limit))
204
+ end
205
+
193
206
  m.register_type 'datetime', Sunstone::Type::DateTime.new
194
207
  m.register_type 'json', Sunstone::Type::Json.new
195
208
  m.register_type 'uuid', Sunstone::Type::Uuid.new
196
-
209
+
197
210
  if defined?(Sunstone::Type::EWKB)
198
211
  m.register_type 'ewkb', Sunstone::Type::EWKB.new
199
212
  end
200
213
  end
201
214
 
202
- def create_table_definition(name, temporary, options, as = nil) # :nodoc:
203
- SunstoneAPI::TableDefinition.new native_database_types, name, temporary, options, as
215
+ def create_table_definition(name, **options)
216
+ SunstoneAPI::TableDefinition.new(self, name, **options)
204
217
  end
205
-
218
+
206
219
  ActiveRecord::Type.add_modifier({ array: true }, Sunstone::Type::Array, adapter: :sunstone)
207
220
  # ActiveRecord::Type.add_modifier({ range: true }, OID::Range, adapter: :postgresql)
208
221
  end
@@ -1,19 +1,13 @@
1
1
  require 'arel/visitors/visitor'
2
- class Arel::Visitors::Dot
3
- def visit_Arel_Nodes_Casted o
4
- # collector << quoted(o.val, o.attribute).to_s
5
- visit_String o.val
6
- end
7
- end
8
2
 
9
3
  module Arel
10
4
  module Visitors
11
5
  class Sunstone < Arel::Visitors::Visitor
12
6
 
13
- def compile node, &block
14
- accept(node, Arel::Collectors::SQLString.new, &block).value
7
+ def compile(node, collector = Arel::Collectors::Sunstone.new)
8
+ accept(node, collector).value
15
9
  end
16
-
10
+
17
11
  def preparable
18
12
  false
19
13
  end
@@ -67,28 +61,6 @@ module Arel
67
61
  collector
68
62
  end
69
63
 
70
- def visit_Arel_Nodes_Overlaps o, collector
71
- key = visit(o.left, collector)
72
- value = { overlaps: visit(o.right, collector) }
73
- if key.is_a?(Hash)
74
- add_to_bottom_of_hash_or_array(key, value)
75
- key
76
- else
77
- {key => value}
78
- end
79
- end
80
-
81
- def visit_Arel_Nodes_NotOverlaps o, collector
82
- key = visit(o.left, collector)
83
- value = { not_overlaps: visit(o.right, collector) }
84
- if key.is_a?(Hash)
85
- add_to_bottom_of_hash_or_array(key, value)
86
- key
87
- else
88
- {key => value}
89
- end
90
- end
91
-
92
64
  def visit_Arel_Nodes_InsertStatement o, collector
93
65
  collector.request_type = Net::HTTP::Post
94
66
  collector.table = o.relation.name
@@ -255,7 +227,7 @@ module Arel
255
227
  #
256
228
  def visit_Arel_Nodes_Casted o, collector
257
229
  # collector << quoted(o.val, o.attribute).to_s
258
- o.val
230
+ o.value
259
231
  end
260
232
 
261
233
  def visit_Arel_Nodes_Quoted o, collector
@@ -947,9 +919,12 @@ module Arel
947
919
  value = if o.relation.is_a?(Arel::Attributes::Relation)
948
920
  { o.name => visit_Arel_Attributes_Relation(o.relation, collector, false) }
949
921
  else
950
- visit(o.relation, collector)
922
+ if o.relation.is_a?(Arel::Attributes::Attribute)
923
+ { o.name => o.relation.name }
924
+ else
925
+ visit(o.relation, collector)
926
+ end
951
927
  end
952
- # value = value.to_s.split('.').last if !value.is_a?(Hash)
953
928
 
954
929
  if o.collection
955
930
  ary = []
@@ -972,9 +947,9 @@ module Arel
972
947
  end
973
948
  end
974
949
 
975
- def visit_Arel_Attributes_EmptyRelation o, collector, top=true
976
- o.for_write ? "#{o.name}_attributes" : o.name
977
- end
950
+ # def visit_Arel_Attributes_EmptyRelation o, collector, top=true
951
+ # o.for_write ? "#{o.name}_attributes" : o.name
952
+ # end
978
953
 
979
954
  def visit_Arel_Attributes_Attribute o, collector
980
955
  join_name = o.relation.table_alias || o.relation.name
@@ -35,7 +35,20 @@ require File.expand_path(File.join(__FILE__, '../../ext/active_support/core_ext/
35
35
 
36
36
  require File.expand_path(File.join(__FILE__, '../../ext/arel/select_manager'))
37
37
  require File.expand_path(File.join(__FILE__, '../../ext/arel/nodes/eager_load'))
38
- require File.expand_path(File.join(__FILE__, '../../ext/arel/attributes/relation'))
39
38
  require File.expand_path(File.join(__FILE__, '../../ext/arel/attributes/empty_relation'))
40
39
  require File.expand_path(File.join(__FILE__, '../../ext/arel/nodes/select_statement'))
41
- require File.expand_path(File.join(__FILE__, '../../ext/active_record/finder_methods'))
40
+ require File.expand_path(File.join(__FILE__, '../../ext/active_record/finder_methods'))
41
+
42
+ if ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR == 1
43
+ # Patch to allow Rails 6.1 pass url to adapter, all other versions work
44
+ class ActiveRecord::DatabaseConfigurations::UrlConfig
45
+ private
46
+ def build_url_hash
47
+ if url.nil? || %w(jdbc: http: https:).any? { |protocol| url.start_with?(protocol) }
48
+ { url: url }
49
+ else
50
+ ConnectionUrlResolver.new(url).to_hash
51
+ end
52
+ end
53
+ end
54
+ end
@@ -348,7 +348,7 @@ module Sunstone
348
348
  headers = Thread.current[:sunstone_headers]&.clone || {}
349
349
  headers['Accept'] = 'application/json'
350
350
  headers['User-Agent'] = user_agent
351
- headers['Api-Version'] = '0.1.0'
351
+ headers['Api-Version'] = '0.2.0'
352
352
  headers['Connection'] = 'keep-alive'
353
353
 
354
354
  request_api_key = Thread.current[:sunstone_api_key] || api_key
@@ -1,3 +1,3 @@
1
1
  module Sunstone
2
- VERSION = '6.0.0.4'
2
+ VERSION = '6.1.0.1'
3
3
  end
@@ -30,10 +30,11 @@ Gem::Specification.new do |s|
30
30
  s.add_development_dependency 'rgeo'
31
31
  s.add_development_dependency 'simplecov'
32
32
  s.add_development_dependency 'byebug'
33
- s.add_development_dependency 'activesupport', '>= 6.0.0.rc1'
33
+ s.add_development_dependency 'activesupport', '>= 6.1.0'
34
34
 
35
35
  # Runtime
36
36
  s.add_runtime_dependency 'msgpack'
37
37
  s.add_runtime_dependency 'cookie_store'
38
- s.add_runtime_dependency 'activerecord', '>= 6.0.0.rc1'
38
+ s.add_runtime_dependency 'activerecord', '>= 6.1.0'
39
+ s.add_runtime_dependency 'arel-extensions', '>= 6.1.0'
39
40
  end
@@ -11,9 +11,10 @@ class ActiveRecord::PersistanceTest < ActiveSupport::TestCase
11
11
  create_table "fleets" do |t|
12
12
  t.string "name", limit: 255
13
13
  end
14
-
14
+
15
15
  create_table "sailors" do |t|
16
16
  t.string "name", limit: 255
17
+ t.text "assignment"
17
18
  end
18
19
 
19
20
  create_table "sailors_ships", id: false do |t|
@@ -22,7 +23,7 @@ class ActiveRecord::PersistanceTest < ActiveSupport::TestCase
22
23
  end
23
24
 
24
25
  end
25
-
26
+
26
27
  class Fleet < ActiveRecord::Base
27
28
  has_many :ships
28
29
  end
@@ -36,16 +37,19 @@ class ActiveRecord::PersistanceTest < ActiveSupport::TestCase
36
37
  class Sailor < ActiveRecord::Base
37
38
  has_and_belongs_to_many :ships
38
39
  end
39
-
40
+
40
41
  class TestModelA < ActiveRecord::Base
41
42
  end
42
-
43
+
43
44
  class TestModelB < ActiveRecord::Base
44
45
  before_save do
45
46
  TestModelA.create
46
47
  end
47
48
  end
48
49
 
50
+ class TestModelC < ActiveRecord::Base
51
+ end
52
+
49
53
  test '#create with errors' do
50
54
  req_stub = webmock(:post, "/fleets").with(
51
55
  body: { fleet: {} }.to_json
@@ -71,7 +75,7 @@ class ActiveRecord::PersistanceTest < ActiveSupport::TestCase
71
75
  assert_requested req_stub
72
76
  end
73
77
 
74
- test '#save w/o changes' do
78
+ test '#save w/o changes does not trigger a request' do
75
79
  webmock(:get, '/fleets', where: {id: 1}, limit: 1).to_return(
76
80
  body: [{id: 1, name: 'Armada Duo'}].to_json
77
81
  )
@@ -82,9 +86,30 @@ class ActiveRecord::PersistanceTest < ActiveSupport::TestCase
82
86
  assert fleet.save
83
87
  assert_equal 1, fleet.id
84
88
  assert_equal 'Armada Duo', fleet.name
89
+
90
+
91
+ webmock(:get, '/sailors', where: {id: 1}, limit: 1).to_return(
92
+ body: [{id: 1, name: 'Nandor', assignment: 'stay alert'}].to_json
93
+ )
94
+
95
+ fleet = Sailor.find(1)
96
+ fleet.save
97
+
98
+ assert fleet.save
99
+ assert_equal 1, fleet.id
100
+ assert_equal 'stay alert', fleet.assignment
85
101
  end
86
102
 
87
103
  test '#save attempts another request while in transaction' do
104
+ webmock(:get, '/test_model_cs/schema').to_return(
105
+ body: {
106
+ attributes: {
107
+ id: {type: 'integer', primary_key: true, null: false, array: false},
108
+ name: {type: 'string', primary_key: false, null: true, array: false}
109
+ }
110
+ }.to_json,
111
+ headers: { 'StandardAPI-Version' => '6.0.0.29' }
112
+ )
88
113
  webmock(:get, '/test_model_bs/schema').to_return(
89
114
  body: {
90
115
  columns: {
@@ -156,4 +181,4 @@ class ActiveRecord::PersistanceTest < ActiveSupport::TestCase
156
181
  assert_requested req_stub
157
182
  end
158
183
 
159
- end
184
+ end