sunstone 6.1.3 → 7.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.
@@ -27,19 +27,8 @@ module ActiveRecord
27
27
  return @definitions[table_name]
28
28
  end
29
29
 
30
- response = @connection.get("/#{table_name}/schema")
31
-
32
- version = Gem::Version.create(response['StandardAPI-Version'] || '5.0.0.4')
33
-
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'))
39
- JSON.parse(response.body)
40
- else
41
- { 'columns' => JSON.parse(response.body), 'limit' => nil }
42
- end
30
+ response = with_raw_connection { |conn| conn.get("/#{table_name}/schema") }
31
+ @definitions[table_name] = JSON.parse(response.body)
43
32
  rescue ::Sunstone::Exception::NotFound
44
33
  raise ActiveRecord::StatementInvalid, "Table \"#{table_name}\" does not exist"
45
34
  end
@@ -50,8 +39,8 @@ module ActiveRecord
50
39
  # - format_type includes the column size constraint, e.g. varchar(50)
51
40
  # - ::regclass is a function that gives the id for a table name
52
41
  def column_definitions(table_name) # :nodoc:
53
- # First check for attributes and then for the deprecated columns field
54
- # TODO: Remove after 0.3
42
+ # TODO: settle on schema, I think we've switched to attributes, so
43
+ # columns can be removed soon?
55
44
  definition(table_name)['attributes'] || definition(table_name)['columns']
56
45
  end
57
46
 
@@ -62,7 +51,7 @@ module ActiveRecord
62
51
  end
63
52
 
64
53
  def tables
65
- JSON.parse(@connection.get('/tables').body)
54
+ JSON.parse(with_raw_connection { |conn| conn.get('/tables').body })
66
55
  end
67
56
 
68
57
  def views
@@ -75,7 +64,7 @@ module ActiveRecord
75
64
  end
76
65
 
77
66
  def lookup_cast_type(options)
78
- type_map.lookup(options['type'], options.symbolize_keys)
67
+ @type_map.lookup(options['type'], options.symbolize_keys)
79
68
  end
80
69
 
81
70
  def fetch_type_metadata(options)
@@ -93,6 +82,35 @@ module ActiveRecord
93
82
  def column_name_for_operation(operation, node) # :nodoc:
94
83
  visitor.accept(node, collector).first[operation.to_sym]
95
84
  end
85
+
86
+ # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT.
87
+ # PostgreSQL, MySQL, and Oracle override this for custom DISTINCT syntax - they
88
+ # require the order columns appear in the SELECT.
89
+ #
90
+ # columns_for_distinct("posts.id", ["posts.created_at desc"])
91
+ #
92
+ def columns_for_distinct(columns, orders) # :nodoc:
93
+ columns
94
+ end
95
+
96
+ def distinct_relation_for_primary_key(relation) # :nodoc:
97
+ values = columns_for_distinct(
98
+ relation.table[relation.primary_key],
99
+ relation.order_values
100
+ )
101
+
102
+ limited = relation.reselect(values).distinct!
103
+ limited_ids = select_rows(limited.arel, "SQL").map(&:last)
104
+
105
+ if limited_ids.empty?
106
+ relation.none!
107
+ else
108
+ relation.where!(relation.primary_key => limited_ids)
109
+ end
110
+
111
+ relation.limit_value = relation.offset_value = nil
112
+ relation
113
+ end
96
114
 
97
115
  # TODO: def encoding
98
116
 
@@ -13,11 +13,11 @@ module ActiveRecord
13
13
  #
14
14
  # +value+ The raw input, as provided from the database.
15
15
  def deserialize(value)
16
- value.nil? ? nil : Base64.strict_decode64(value)
16
+ value.nil? ? nil : Base64.strict_decode64(value)
17
17
  end
18
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
19
+ # Casts a value from the ruby type to a type that the database knows
20
+ # how to understand. The returned value from this method should be a
21
21
  # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
22
22
  # +nil+.
23
23
  def serialize(value)
@@ -17,27 +17,14 @@ require 'active_record/connection_adapters/sunstone/type/json'
17
17
  module ActiveRecord
18
18
  module ConnectionHandling # :nodoc:
19
19
 
20
- VALID_SUNSTONE_CONN_PARAMS = [:url, :host, :port, :api_key, :use_ssl, :user_agent, :ca_cert]
21
-
20
+ def sunstone_adapter_class
21
+ ConnectionAdapters::SunstoneAPIAdapter
22
+ end
23
+
22
24
  # Establishes a connection to the database that's used by all Active Record
23
25
  # objects
24
26
  def sunstone_connection(config)
25
- conn_params = config.symbolize_keys
26
- conn_params.delete_if { |_, v| v.nil? }
27
-
28
- if conn_params[:url]
29
- uri = URI.parse(conn_params.delete(:url))
30
- conn_params[:api_key] ||= (uri.user ? CGI.unescape(uri.user) : nil)
31
- conn_params[:host] ||= uri.host
32
- conn_params[:port] ||= uri.port
33
- conn_params[:use_ssl] ||= (uri.scheme == 'https')
34
- end
35
-
36
- # Forward only valid config params to Sunstone::Connection
37
- conn_params.slice!(*VALID_SUNSTONE_CONN_PARAMS)
38
-
39
- client = ::Sunstone::Connection.new(conn_params)
40
- ConnectionAdapters::SunstoneAPIAdapter.new(client, logger, conn_params, config)
27
+ sunstone_adapter_class.new(config)
41
28
  end
42
29
  end
43
30
 
@@ -54,6 +41,7 @@ module ActiveRecord
54
41
  # <encoding></tt> call on the connection.
55
42
  class SunstoneAPIAdapter < AbstractAdapter
56
43
  ADAPTER_NAME = 'Sunstone'.freeze
44
+ VALID_SUNSTONE_CONN_PARAMS = [:url, :host, :port, :api_key, :use_ssl, :user_agent, :ca_cert]
57
45
 
58
46
  NATIVE_DATABASE_TYPES = {
59
47
  string: { name: "string" },
@@ -61,6 +49,12 @@ module ActiveRecord
61
49
  json: { name: "json" },
62
50
  boolean: { name: "boolean" }
63
51
  }
52
+
53
+ class << self
54
+ def new_client(conn_params)
55
+ ::Sunstone::Connection.new(conn_params)
56
+ end
57
+ end
64
58
 
65
59
  # include PostgreSQL::Quoting
66
60
  # include PostgreSQL::ReferentialIntegrity
@@ -72,35 +66,66 @@ module ActiveRecord
72
66
  def supports_statement_cache?
73
67
  false
74
68
  end
69
+
70
+ def default_prepared_statements
71
+ false
72
+ end
75
73
 
76
- def clear_cache!
74
+ def clear_cache!(new_connection: false)
77
75
  # TODO move @definitions to using @schema_cache
78
76
  @definitions = {}
79
77
  end
80
78
 
81
79
  # Initializes and connects a SunstoneAPI adapter.
82
- def initialize(connection, logger, connection_parameters, config)
83
- super(connection, logger, config.reverse_merge(prepared_statements: false))
80
+ def initialize(...)
81
+ super
84
82
 
85
- @prepared_statement_status = Concurrent::ThreadLocalVar.new(false)
86
- @connection_parameters = connection_parameters
83
+ conn_params = @config.compact
84
+ if conn_params[:url]
85
+ uri = URI.parse(conn_params.delete(:url))
86
+ conn_params[:api_key] ||= (uri.user ? CGI.unescape(uri.user) : nil)
87
+ conn_params[:host] ||= uri.host
88
+ conn_params[:port] ||= uri.port
89
+ conn_params[:use_ssl] ||= (uri.scheme == 'https')
90
+ end
91
+
92
+ # Forward only valid config params to Sunstone::Connection
93
+ conn_params.slice!(*VALID_SUNSTONE_CONN_PARAMS)
94
+
95
+ @connection_parameters = conn_params
96
+
97
+ @max_identifier_length = nil
98
+ @type_map = nil
99
+ @raw_connection = nil
100
+ end
87
101
 
88
- # @type_map = Type::HashLookupTypeMap.new
89
- # initialize_type_map(type_map)
102
+ def url(path=nil)
103
+ "http#{@connection_parameters[:use_ssl] ? 's' : ''}://#{@connection_parameters[:host]}#{@connection_parameters[:port] != 80 ? (@connection_parameters[:port] == 443 && @connection_parameters[:use_ssl] ? '' : ":#{@connection_parameters[:port]}") : ''}#{path}"
90
104
  end
91
105
 
92
106
  def active?
93
- @connection.active?
107
+ @raw_connection&.active?
94
108
  end
95
109
 
96
- def reconnect!
110
+ def load_type_map
111
+ @type_map = Type::HashLookupTypeMap.new
112
+ initialize_type_map(@type_map)
113
+ end
114
+
115
+ def reconnect
97
116
  super
98
- @connection.reconnect!
117
+ @raw_connection&.reconnect!
99
118
  end
100
119
 
101
120
  def disconnect!
102
121
  super
103
- @connection.disconnect!
122
+ @raw_connection&.disconnect!
123
+ @raw_connection = nil
124
+ end
125
+
126
+ def discard! # :nodoc:
127
+ super
128
+ @raw_connection = nil
104
129
  end
105
130
 
106
131
  # Executes the delete statement and returns the number of rows affected.
@@ -134,11 +159,17 @@ module ActiveRecord
134
159
  end
135
160
 
136
161
  def server_config
137
- JSON.parse(@connection.get("/configuration").body)
162
+ with_raw_connection do |conn|
163
+ JSON.parse(conn.get("/configuration").body)
164
+ end
165
+ end
166
+
167
+ def return_value_after_insert?(column) # :nodoc:
168
+ column.auto_populated?
138
169
  end
139
170
 
140
171
  def lookup_cast_type_from_column(column) # :nodoc:
141
- cast_type = type_map.lookup(column.sql_type, {
172
+ cast_type = @type_map.lookup(column.sql_type, {
142
173
  limit: column.limit,
143
174
  precision: column.precision,
144
175
  scale: column.scale
@@ -167,45 +198,63 @@ module ActiveRecord
167
198
  true
168
199
  end
169
200
 
170
- # Executes an INSERT query and returns the new record's ID
171
- #
172
- # +id_value+ will be returned unless the value is nil, in
173
- # which case the database will attempt to calculate the last inserted
174
- # id and return that value.
175
- #
176
- # If the next id was calculated in advance (as in Oracle), it should be
177
- # passed in as +id_value+.
178
- def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
179
- exec_insert(arel, name, binds, pk, sequence_name)
201
+ # Executes an INSERT query and returns a hash of the object and
202
+ # any updated relations. This is different from AR which returns an ID
203
+ def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [], returning: nil)
204
+ exec_insert(arel, name, binds, pk, sequence_name, returning: returning)
180
205
  end
181
206
  alias create insert
182
207
 
183
- # Should be the defuat insert, but rails escapes if for SQL so we'll just
184
- # catch the string "DEFATUL VALUES" in the visitor
185
- # def empty_insert_statement_value
186
- # {}
187
- # end
208
+ # Connects to a StandardAPI server and sets up the adapter depending
209
+ # on the connected server's characteristics.
210
+ def connect
211
+ @raw_connection = self.class.new_client(@connection_parameters)
212
+ end
188
213
 
189
- private
214
+ def reconnect
215
+ @raw_connection&.reconnect!
216
+ connect unless @raw_connection
217
+ end
190
218
 
191
- def initialize_type_map(m) # :nodoc:
192
- m.register_type 'boolean', Type::Boolean.new
193
- m.register_type 'string' do |_, options|
194
- Type::String.new(**options.slice(:limit))
219
+ # Configures the encoding, verbosity, schema search path, and time zone of the connection.
220
+ # This is called by #connect and should not be called manually.
221
+ def configure_connection
222
+ reload_type_map
223
+ end
224
+
225
+ def reload_type_map
226
+ if @type_map
227
+ type_map.clear
228
+ else
229
+ @type_map = Type::HashLookupTypeMap.new
195
230
  end
196
- m.register_type 'integer' do |_, options|
197
- Type::Integer.new(**options.slice(:limit))
231
+
232
+ initialize_type_map
233
+ end
234
+
235
+ private
236
+
237
+ def initialize_type_map(m = nil)
238
+ self.class.initialize_type_map(m || @type_map)
239
+ end
240
+
241
+ def self.initialize_type_map(m) # :nodoc:
242
+ m.register_type 'boolean', Type::Boolean.new
243
+ m.register_type 'binary' do |_, options|
244
+ Sunstone::Type::Binary.new(**options.slice(:limit))
198
245
  end
199
- m.register_type 'decimal' do |_, options|
246
+ m.register_type 'datetime', Sunstone::Type::DateTime.new
247
+ m.register_type 'decimal' do |_, options|
200
248
  Type::Decimal.new(**options.slice(:precision, :scale))
201
249
  end
202
- m.register_type 'binary' do |_, options|
203
- Sunstone::Type::Binary.new(**options.slice(:limit))
250
+ m.register_type 'integer' do |_, options|
251
+ Type::Integer.new(**options.slice(:limit))
204
252
  end
205
-
206
- m.register_type 'datetime', Sunstone::Type::DateTime.new
207
- m.register_type 'json', Sunstone::Type::Json.new
208
- m.register_type 'uuid', Sunstone::Type::Uuid.new
253
+ m.register_type 'json', Sunstone::Type::Json.new
254
+ m.register_type 'string' do |_, options|
255
+ Type::String.new(**options.slice(:limit))
256
+ end
257
+ m.register_type 'uuid', Sunstone::Type::Uuid.new
209
258
 
210
259
  if defined?(Sunstone::Type::EWKB)
211
260
  m.register_type 'ewkb', Sunstone::Type::EWKB.new
@@ -24,7 +24,14 @@ module Arel
24
24
  def substitute_binds hash, bvs
25
25
  if hash.is_a?(Array)
26
26
  hash.map do |v|
27
- if v.is_a?(Arel::Nodes::BindParam)
27
+ if v.is_a?(ActiveRecord::Relation::QueryAttribute)
28
+ new_v = bvs.shift
29
+ if new_v.is_a?(ActiveRecord::Relation::QueryAttribute)
30
+ new_v.value_for_database
31
+ else
32
+ v.type.serialize(new_v)
33
+ end
34
+ elsif v.is_a?(Arel::Nodes::BindParam)
28
35
  bvs.shift#.value_for_database
29
36
  elsif v.is_a?(Hash) || v.is_a?(Array)
30
37
  substitute_binds(v, bvs)
@@ -35,7 +42,14 @@ module Arel
35
42
  elsif hash.is_a?(Hash)
36
43
  newhash = {}
37
44
  hash.each do |k, v|
38
- if v.is_a?(Arel::Nodes::BindParam)
45
+ if v.is_a?(ActiveRecord::Relation::QueryAttribute)
46
+ new_v = bvs.shift
47
+ newhash[k] = if new_v.is_a?(ActiveRecord::Relation::QueryAttribute)
48
+ new_v.value_for_database
49
+ else
50
+ v.type.serialize(new_v)
51
+ end
52
+ elsif v.is_a?(Arel::Nodes::BindParam)
39
53
  newhash[k] = bvs.shift || v.value.value_for_database
40
54
  elsif v.is_a?(Hash)
41
55
  newhash[k] = substitute_binds(v, bvs)
@@ -47,9 +61,16 @@ module Arel
47
61
  end
48
62
  newhash
49
63
  elsif hash.is_a?(Arel::Nodes::BindParam)
50
- bvs.shift || hash.value.value_for_database
64
+ bvs.shift
65
+ elsif hash.is_a?(ActiveRecord::Relation::QueryAttribute)
66
+ new_v = bvs.shift
67
+ if new_v.is_a?(ActiveRecord::Relation::QueryAttribute)
68
+ new_v.value_for_database
69
+ else
70
+ v.type.serialize(new_v)
71
+ end
51
72
  else
52
- bvs.shift || hash.value.value_for_database
73
+ bvs.shift
53
74
  end
54
75
  end
55
76