sunstone 6.1.3 → 7.1.0.1

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