activerecord-cockroachdb-adapter 6.0.0beta1 → 6.1.0.pre.beta.2

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.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord # :nodoc:
4
+ module ConnectionAdapters # :nodoc:
5
+ module CockroachDB # :nodoc:
6
+ def self.initial_setup
7
+ ::ActiveRecord::SchemaDumper.ignore_tables |= %w[
8
+ geography_columns
9
+ geometry_columns
10
+ layer
11
+ raster_columns
12
+ raster_overviews
13
+ spatial_ref_sys
14
+ topology
15
+ ]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module CockroachDB
4
+ class SpatialColumnInfo
5
+ def initialize(adapter, table_name)
6
+ @adapter = adapter
7
+ @table_name = table_name
8
+ end
9
+
10
+ def all
11
+ info = @adapter.query(
12
+ "SELECT f_geometry_column,coord_dimension,srid,type FROM geometry_columns WHERE f_table_name='#{@table_name}'"
13
+ )
14
+ result = {}
15
+ info.each do |row|
16
+ name = row[0]
17
+ type = row[3]
18
+ dimension = row[1].to_i
19
+ has_m = !!(type =~ /m$/i)
20
+ type.sub!(/m$/, '')
21
+ has_z = dimension > 3 || dimension == 3 && !has_m
22
+ result[name] = {
23
+ dimension: dimension,
24
+ has_m: has_m,
25
+ has_z: has_z,
26
+ name: name,
27
+ srid: row[2].to_i,
28
+ type: type
29
+ }
30
+ end
31
+ result
32
+ end
33
+
34
+ # do not query the database for non-spatial columns/tables
35
+ def get(column_name, type)
36
+ return unless CockroachDBAdapter.spatial_column_options(type.to_sym)
37
+
38
+ @spatial_column_info ||= all
39
+ @spatial_column_info[column_name]
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord # :nodoc:
4
+ module ConnectionAdapters # :nodoc:
5
+ module CockroachDB # :nodoc:
6
+ class TableDefinition < PostgreSQL::TableDefinition # :nodoc:
7
+ include ColumnMethods
8
+
9
+ # Support for spatial columns in tables
10
+ # super: https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
11
+ def new_column_definition(name, type, **options)
12
+ if (info = CockroachDBAdapter.spatial_column_options(type.to_sym))
13
+ if (limit = options.delete(:limit)) && limit.is_a?(::Hash)
14
+ options.merge!(limit)
15
+ end
16
+
17
+ geo_type = ColumnDefinitionUtils.geo_type(options[:type] || type || info[:type])
18
+ base_type = info[:type] || (options[:geographic] ? :geography : :geometry)
19
+
20
+ options[:limit] = ColumnDefinitionUtils.limit_from_options(geo_type, options)
21
+ options[:spatial_type] = geo_type
22
+ column = super(name, base_type, **options)
23
+ else
24
+ column = super(name, type, **options)
25
+ end
26
+
27
+ column
28
+ end
29
+ end
30
+
31
+ module ColumnDefinitionUtils
32
+ class << self
33
+ def geo_type(type = 'GEOMETRY')
34
+ g_type = type.to_s.delete('_').upcase
35
+ return 'POINT' if g_type == 'STPOINT'
36
+ return 'POLYGON' if g_type == 'STPOLYGON'
37
+
38
+ g_type
39
+ end
40
+
41
+ def limit_from_options(type, options = {})
42
+ spatial_type = geo_type(type)
43
+ spatial_type << 'Z' if options[:has_z]
44
+ spatial_type << 'M' if options[:has_m]
45
+ spatial_type << ",#{options[:srid] || default_srid(options)}"
46
+ spatial_type
47
+ end
48
+
49
+ def default_srid(options)
50
+ options[:geographic] ? 4326 : CockroachDBAdapter::DEFAULT_SRID
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -8,16 +8,22 @@ module ActiveRecord
8
8
  # transactions that fail due to serialization errors. Failed
9
9
  # transactions will be retried until they pass or the max retry limit is
10
10
  # exceeded.
11
- def within_new_transaction(options = {})
12
- attempts = options.fetch(:attempts, 0)
13
- super
14
- rescue ActiveRecord::SerializationFailure => error
11
+ def within_new_transaction(isolation: nil, joinable: true, attempts: 0)
12
+ super(isolation: isolation, joinable: joinable)
13
+ rescue ActiveRecord::StatementInvalid => error
14
+ raise unless retryable? error
15
15
  raise if attempts >= @connection.max_transaction_retries
16
16
 
17
17
  attempts += 1
18
18
  sleep_seconds = (2 ** attempts + rand) / 10
19
19
  sleep(sleep_seconds)
20
- within_new_transaction(options.merge(attempts: attempts)) { yield }
20
+ within_new_transaction(isolation: isolation, joinable: joinable, attempts: attempts) { yield }
21
+ end
22
+
23
+ def retryable?(error)
24
+ return true if error.is_a? ActiveRecord::SerializationFailure
25
+ return retryable? error.cause if error.cause
26
+ false
21
27
  end
22
28
  end
23
29
  end
@@ -1,12 +1,10 @@
1
1
  module ActiveRecord
2
2
  module Type
3
3
  class << self
4
- private
5
-
6
4
  # Return :postgresql instead of :cockroachdb for current_adapter_name so
7
5
  # we can continue using the ActiveRecord::Types defined in
8
6
  # PostgreSQLAdapter.
9
- def current_adapter_name
7
+ def adapter_name_from(_model)
10
8
  :postgresql
11
9
  end
12
10
  end
@@ -1,39 +1,54 @@
1
- require 'active_record/connection_adapters/postgresql_adapter'
1
+ require "rgeo/active_record"
2
+
3
+ require "active_record/connection_adapters/postgresql_adapter"
4
+ require "active_record/connection_adapters/cockroachdb/column_methods"
2
5
  require "active_record/connection_adapters/cockroachdb/schema_statements"
3
6
  require "active_record/connection_adapters/cockroachdb/referential_integrity"
4
7
  require "active_record/connection_adapters/cockroachdb/transaction_manager"
5
- require "active_record/connection_adapters/cockroachdb/column"
6
8
  require "active_record/connection_adapters/cockroachdb/database_statements"
9
+ require "active_record/connection_adapters/cockroachdb/table_definition"
7
10
  require "active_record/connection_adapters/cockroachdb/quoting"
8
11
  require "active_record/connection_adapters/cockroachdb/type"
9
12
  require "active_record/connection_adapters/cockroachdb/attribute_methods"
13
+ require "active_record/connection_adapters/cockroachdb/column"
14
+ require "active_record/connection_adapters/cockroachdb/spatial_column_info"
15
+ require "active_record/connection_adapters/cockroachdb/setup"
16
+ require "active_record/connection_adapters/cockroachdb/oid/type_map_initializer"
17
+ require "active_record/connection_adapters/cockroachdb/oid/spatial"
18
+ require "active_record/connection_adapters/cockroachdb/oid/interval"
19
+ require "active_record/connection_adapters/cockroachdb/arel_tosql"
20
+
21
+ # Run to ignore spatial tables that will break schemna dumper.
22
+ # Defined in ./setup.rb
23
+ ActiveRecord::ConnectionAdapters::CockroachDB.initial_setup
10
24
 
11
25
  module ActiveRecord
12
26
  module ConnectionHandling
13
27
  def cockroachdb_connection(config)
14
28
  # This is copied from the PostgreSQL adapter.
15
- conn_params = config.symbolize_keys
16
-
17
- conn_params.delete_if { |_, v| v.nil? }
29
+ conn_params = config.symbolize_keys.compact
18
30
 
19
31
  # Map ActiveRecords param names to PGs.
20
32
  conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
21
33
  conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
22
34
 
23
35
  # Forward only valid config params to PG::Connection.connect.
24
- valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:sslmode, :application_name]
36
+ valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
25
37
  conn_params.slice!(*valid_conn_param_keys)
26
38
 
27
- # The postgres drivers don't allow the creation of an unconnected
28
- # PG::Connection object, so just pass a nil connection object for the
29
- # time being.
30
- conn = PG.connect(conn_params)
31
- ConnectionAdapters::CockroachDBAdapter.new(conn, logger, conn_params, config)
32
- rescue ::PG::Error => error
33
- if error.message.include?("does not exist")
39
+ ConnectionAdapters::CockroachDBAdapter.new(
40
+ ConnectionAdapters::CockroachDBAdapter.new_client(conn_params),
41
+ logger,
42
+ conn_params,
43
+ config
44
+ )
45
+ # This rescue flow appears in new_client, but it is needed here as well
46
+ # since Cockroach will sometimes not raise until a query is made.
47
+ rescue ActiveRecord::StatementInvalid => error
48
+ if conn_params && conn_params[:dbname] && error.cause.message.include?(conn_params[:dbname])
34
49
  raise ActiveRecord::NoDatabaseError
35
50
  else
36
- raise
51
+ raise ActiveRecord::ConnectionNotEstablished, error.message
37
52
  end
38
53
  end
39
54
  end
@@ -45,11 +60,49 @@ module ActiveRecord
45
60
  ADAPTER_NAME = "CockroachDB".freeze
46
61
  DEFAULT_PRIMARY_KEY = "rowid"
47
62
 
63
+ SPATIAL_COLUMN_OPTIONS =
64
+ {
65
+ geography: { geographic: true },
66
+ geometry: {},
67
+ geometry_collection: {},
68
+ line_string: {},
69
+ multi_line_string: {},
70
+ multi_point: {},
71
+ multi_polygon: {},
72
+ spatial: {},
73
+ st_point: {},
74
+ st_polygon: {},
75
+ }
76
+
77
+ # http://postgis.17.x6.nabble.com/Default-SRID-td5001115.html
78
+ DEFAULT_SRID = 0
79
+
48
80
  include CockroachDB::SchemaStatements
49
81
  include CockroachDB::ReferentialIntegrity
50
82
  include CockroachDB::DatabaseStatements
51
83
  include CockroachDB::Quoting
52
84
 
85
+ def self.spatial_column_options(key)
86
+ SPATIAL_COLUMN_OPTIONS[key]
87
+ end
88
+
89
+ def postgis_lib_version
90
+ @postgis_lib_version ||= select_value("SELECT PostGIS_Lib_Version()")
91
+ end
92
+
93
+ def default_srid
94
+ DEFAULT_SRID
95
+ end
96
+
97
+ def srs_database_columns
98
+ {
99
+ auth_name_column: "auth_name",
100
+ auth_srid_column: "auth_srid",
101
+ proj4text_column: "proj4text",
102
+ srtext_column: "srtext",
103
+ }
104
+ end
105
+
53
106
  def debugging?
54
107
  !!ENV["DEBUG_COCKROACHDB_ADAPTER"]
55
108
  end
@@ -154,20 +207,55 @@ module ActiveRecord
154
207
  @crdb_version = version_num
155
208
  end
156
209
 
210
+ def self.database_exists?(config)
211
+ !!ActiveRecord::Base.cockroachdb_connection(config)
212
+ rescue ActiveRecord::NoDatabaseError
213
+ false
214
+ end
215
+
157
216
  private
158
217
 
159
218
  def initialize_type_map(m = type_map)
219
+ %w(
220
+ geography
221
+ geometry
222
+ geometry_collection
223
+ line_string
224
+ multi_line_string
225
+ multi_point
226
+ multi_polygon
227
+ st_point
228
+ st_polygon
229
+ ).each do |geo_type|
230
+ m.register_type(geo_type) do |oid, _, sql_type|
231
+ CockroachDB::OID::Spatial.new(oid, sql_type)
232
+ end
233
+ end
234
+
235
+ # Belongs after other types are defined because of issues described
236
+ # in this https://github.com/rails/rails/pull/38571
237
+ # Once that PR is merged, we can call super at the top.
160
238
  super(m)
161
- # NOTE(joey): PostgreSQL intervals have a precision.
162
- # CockroachDB intervals do not, so overide the type
163
- # definition. Returning a ArgumentError may not be correct.
164
- # This needs to be tested.
165
- m.register_type "interval" do |_, _, sql_type|
239
+
240
+ # Override numeric type. This is almost identical to the default,
241
+ # except that the conditional based on the fmod is changed.
242
+ m.register_type "numeric" do |_, fmod, sql_type|
166
243
  precision = extract_precision(sql_type)
167
- if precision
168
- raise(ArgumentError, "CockroachDB does not support precision on intervals, but got precision: #{precision}")
244
+ scale = extract_scale(sql_type)
245
+
246
+ # TODO(#178) this should never use DecimalWithoutScale since scale
247
+ # is assumed to be 0 if it is not explicitly defined.
248
+ #
249
+ # If fmod is -1, that means that precision is defined but not
250
+ # scale, or neither is defined.
251
+ if fmod && fmod == -1
252
+ # Below comment is from ActiveRecord
253
+ # FIXME: Remove this class, and the second argument to
254
+ # lookups on PG
255
+ Type::DecimalWithoutScale.new(precision: precision)
256
+ else
257
+ OID::Decimal.new(precision: precision, scale: scale)
169
258
  end
170
- OID::SpecializedString.new(:interval, precision: precision)
171
259
  end
172
260
  end
173
261
 
@@ -278,6 +366,84 @@ module ActiveRecord
278
366
  return "{}"
279
367
  end
280
368
 
369
+ # override
370
+ # This method makes a query to gather information about columns
371
+ # in a table. It returns an array of arrays (one for each col) and
372
+ # passes each to the SchemaStatements#new_column_from_field method
373
+ # as the field parameter. This data is then used to format the column
374
+ # objects for the model and sent to the OID for data casting.
375
+ #
376
+ # Sometimes there are differences between how data is formatted
377
+ # in Postgres and CockroachDB, so additional queries for certain types
378
+ # may be necessary to properly form the column definition.
379
+ #
380
+ # @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
381
+ def column_definitions(table_name)
382
+ fields = super
383
+
384
+ # Use regex comparison because if a type is an array it will
385
+ # have [] appended to the end of it.
386
+ target_types = [
387
+ /geometry/,
388
+ /geography/,
389
+ /interval/,
390
+ /numeric/
391
+ ]
392
+ re = Regexp.union(target_types)
393
+ fields.map do |field|
394
+ dtype = field[1]
395
+ if re.match(dtype)
396
+ crdb_column_definition(field, table_name)
397
+ else
398
+ field
399
+ end
400
+ end
401
+ end
402
+
403
+ # Use the crdb_sql_type instead of the sql_type returned by
404
+ # column_definitions. This will include limit,
405
+ # precision, and scale information in the type.
406
+ # Ex. geometry -> geometry(point, 4326)
407
+ def crdb_column_definition(field, table_name)
408
+ col_name = field[0]
409
+ data_type = \
410
+ query(<<~SQL, "SCHEMA")
411
+ SELECT c.crdb_sql_type
412
+ FROM information_schema.columns c
413
+ WHERE c.table_name = #{quote(table_name)}
414
+ AND c.column_name = #{quote(col_name)}
415
+ SQL
416
+ field[1] = data_type[0][0].downcase
417
+ field
418
+ end
419
+
420
+ # override
421
+ # This method is used to determine if a
422
+ # FEATURE_NOT_SUPPORTED error from the PG gem should
423
+ # be an ActiveRecord::PreparedStatementCacheExpired
424
+ # error.
425
+ #
426
+ # ActiveRecord handles this by checking that the sql state matches the
427
+ # FEATURE_NOT_SUPPORTED code and that the source function
428
+ # is "RevalidateCachedQuery" since that is the only function
429
+ # in postgres that will create this error.
430
+ #
431
+ # That method will not work for CockroachDB because the error
432
+ # originates from the "runExecBuilder" function, so we need
433
+ # to modify the original to match the CockroachDB behavior.
434
+ def is_cached_plan_failure?(e)
435
+ pgerror = e.cause
436
+
437
+ pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE) == FEATURE_NOT_SUPPORTED &&
438
+ pgerror.result.result_error_field(PG::PG_DIAG_SOURCE_FUNCTION) == "runExecBuilder"
439
+ rescue
440
+ false
441
+ end
442
+
443
+ def arel_visitor
444
+ Arel::Visitors::CockroachDB.new(self)
445
+ end
446
+
281
447
  # end private
282
448
  end
283
449
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-cockroachdb-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0beta1
4
+ version: 6.1.0.pre.beta.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cockroach Labs
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-17 00:00:00.000000000 Z
11
+ date: 2021-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,28 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 6.0.3
19
+ version: '6.1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 6.0.3
26
+ version: '6.1'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: pg
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.20'
33
+ version: '1.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rgeo-activerecord
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 7.0.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: '0.20'
54
+ version: 7.0.0
41
55
  description: Allows the use of CockroachDB as a backend for ActiveRecord and Rails
42
56
  apps.
43
57
  email:
@@ -48,6 +62,7 @@ extra_rdoc_files: []
48
62
  files:
49
63
  - ".gitignore"
50
64
  - ".gitmodules"
65
+ - CHANGELOG.md
51
66
  - CONTRIBUTING.md
52
67
  - Gemfile
53
68
  - LICENSE
@@ -61,13 +76,21 @@ files:
61
76
  - build/local-test.sh
62
77
  - build/teamcity-test.sh
63
78
  - docker.sh
79
+ - lib/active_record/connection_adapters/cockroachdb/arel_tosql.rb
64
80
  - lib/active_record/connection_adapters/cockroachdb/attribute_methods.rb
65
81
  - lib/active_record/connection_adapters/cockroachdb/column.rb
82
+ - lib/active_record/connection_adapters/cockroachdb/column_methods.rb
66
83
  - lib/active_record/connection_adapters/cockroachdb/database_statements.rb
67
84
  - lib/active_record/connection_adapters/cockroachdb/database_tasks.rb
85
+ - lib/active_record/connection_adapters/cockroachdb/oid/interval.rb
86
+ - lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb
87
+ - lib/active_record/connection_adapters/cockroachdb/oid/type_map_initializer.rb
68
88
  - lib/active_record/connection_adapters/cockroachdb/quoting.rb
69
89
  - lib/active_record/connection_adapters/cockroachdb/referential_integrity.rb
70
90
  - lib/active_record/connection_adapters/cockroachdb/schema_statements.rb
91
+ - lib/active_record/connection_adapters/cockroachdb/setup.rb
92
+ - lib/active_record/connection_adapters/cockroachdb/spatial_column_info.rb
93
+ - lib/active_record/connection_adapters/cockroachdb/table_definition.rb
71
94
  - lib/active_record/connection_adapters/cockroachdb/transaction_manager.rb
72
95
  - lib/active_record/connection_adapters/cockroachdb/type.rb
73
96
  - lib/active_record/connection_adapters/cockroachdb_adapter.rb