activerecord-cockroachdb-adapter 6.0.0beta2 → 6.1.0.pre.beta.3

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
@@ -9,7 +9,7 @@ module ActiveRecord
9
9
  # transactions will be retried until they pass or the max retry limit is
10
10
  # exceeded.
11
11
  def within_new_transaction(isolation: nil, joinable: true, attempts: 0)
12
- super
12
+ super(isolation: isolation, joinable: joinable)
13
13
  rescue ActiveRecord::StatementInvalid => error
14
14
  raise unless retryable? error
15
15
  raise if attempts >= @connection.max_transaction_retries
@@ -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,36 +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
- conn = PG.connect(conn_params)
28
- ConnectionAdapters::CockroachDBAdapter.new(conn, logger, conn_params, config)
29
- rescue ::PG::Error, ActiveRecord::ActiveRecordError => error
30
- 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])
31
49
  raise ActiveRecord::NoDatabaseError
32
50
  else
33
- raise
51
+ raise ActiveRecord::ConnectionNotEstablished, error.message
34
52
  end
35
53
  end
36
54
  end
@@ -42,11 +60,49 @@ module ActiveRecord
42
60
  ADAPTER_NAME = "CockroachDB".freeze
43
61
  DEFAULT_PRIMARY_KEY = "rowid"
44
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
+
45
80
  include CockroachDB::SchemaStatements
46
81
  include CockroachDB::ReferentialIntegrity
47
82
  include CockroachDB::DatabaseStatements
48
83
  include CockroachDB::Quoting
49
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
+
50
106
  def debugging?
51
107
  !!ENV["DEBUG_COCKROACHDB_ADAPTER"]
52
108
  end
@@ -117,6 +173,10 @@ module ActiveRecord
117
173
  @crdb_version >= 202
118
174
  end
119
175
 
176
+ def supports_partitioned_indexes?
177
+ false
178
+ end
179
+
120
180
  # This is hardcoded to 63 (as previously was in ActiveRecord 5.0) to aid in
121
181
  # migration from PostgreSQL to CockroachDB. In practice, this limitation
122
182
  # is arbitrary since CockroachDB supports index name lengths and table alias
@@ -160,17 +220,46 @@ module ActiveRecord
160
220
  private
161
221
 
162
222
  def initialize_type_map(m = type_map)
223
+ %w(
224
+ geography
225
+ geometry
226
+ geometry_collection
227
+ line_string
228
+ multi_line_string
229
+ multi_point
230
+ multi_polygon
231
+ st_point
232
+ st_polygon
233
+ ).each do |geo_type|
234
+ m.register_type(geo_type) do |oid, _, sql_type|
235
+ CockroachDB::OID::Spatial.new(oid, sql_type)
236
+ end
237
+ end
238
+
239
+ # Belongs after other types are defined because of issues described
240
+ # in this https://github.com/rails/rails/pull/38571
241
+ # Once that PR is merged, we can call super at the top.
163
242
  super(m)
164
- # NOTE(joey): PostgreSQL intervals have a precision.
165
- # CockroachDB intervals do not, so overide the type
166
- # definition. Returning a ArgumentError may not be correct.
167
- # This needs to be tested.
168
- m.register_type "interval" do |_, _, sql_type|
243
+
244
+ # Override numeric type. This is almost identical to the default,
245
+ # except that the conditional based on the fmod is changed.
246
+ m.register_type "numeric" do |_, fmod, sql_type|
169
247
  precision = extract_precision(sql_type)
170
- if precision
171
- raise(ArgumentError, "CockroachDB does not support precision on intervals, but got precision: #{precision}")
248
+ scale = extract_scale(sql_type)
249
+
250
+ # TODO(#178) this should never use DecimalWithoutScale since scale
251
+ # is assumed to be 0 if it is not explicitly defined.
252
+ #
253
+ # If fmod is -1, that means that precision is defined but not
254
+ # scale, or neither is defined.
255
+ if fmod && fmod == -1
256
+ # Below comment is from ActiveRecord
257
+ # FIXME: Remove this class, and the second argument to
258
+ # lookups on PG
259
+ Type::DecimalWithoutScale.new(precision: precision)
260
+ else
261
+ OID::Decimal.new(precision: precision, scale: scale)
172
262
  end
173
- OID::SpecializedString.new(:interval, precision: precision)
174
263
  end
175
264
  end
176
265
 
@@ -281,6 +370,175 @@ module ActiveRecord
281
370
  return "{}"
282
371
  end
283
372
 
373
+ # override
374
+ # This method makes a query to gather information about columns
375
+ # in a table. It returns an array of arrays (one for each col) and
376
+ # passes each to the SchemaStatements#new_column_from_field method
377
+ # as the field parameter. This data is then used to format the column
378
+ # objects for the model and sent to the OID for data casting.
379
+ #
380
+ # Sometimes there are differences between how data is formatted
381
+ # in Postgres and CockroachDB, so additional queries for certain types
382
+ # may be necessary to properly form the column definition.
383
+ #
384
+ # @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
385
+ def column_definitions(table_name)
386
+ fields = super
387
+
388
+ # Use regex comparison because if a type is an array it will
389
+ # have [] appended to the end of it.
390
+ target_types = [
391
+ /geometry/,
392
+ /geography/,
393
+ /interval/,
394
+ /numeric/
395
+ ]
396
+ re = Regexp.union(target_types)
397
+ fields.map do |field|
398
+ dtype = field[1]
399
+ if re.match(dtype)
400
+ crdb_column_definition(field, table_name)
401
+ else
402
+ field
403
+ end
404
+ end
405
+ end
406
+
407
+ # Use the crdb_sql_type instead of the sql_type returned by
408
+ # column_definitions. This will include limit,
409
+ # precision, and scale information in the type.
410
+ # Ex. geometry -> geometry(point, 4326)
411
+ def crdb_column_definition(field, table_name)
412
+ col_name = field[0]
413
+ data_type = \
414
+ query(<<~SQL, "SCHEMA")
415
+ SELECT c.crdb_sql_type
416
+ FROM information_schema.columns c
417
+ WHERE c.table_name = #{quote(table_name)}
418
+ AND c.column_name = #{quote(col_name)}
419
+ SQL
420
+ field[1] = data_type[0][0].downcase
421
+ field
422
+ end
423
+
424
+ # override
425
+ # This method is used to determine if a
426
+ # FEATURE_NOT_SUPPORTED error from the PG gem should
427
+ # be an ActiveRecord::PreparedStatementCacheExpired
428
+ # error.
429
+ #
430
+ # ActiveRecord handles this by checking that the sql state matches the
431
+ # FEATURE_NOT_SUPPORTED code and that the source function
432
+ # is "RevalidateCachedQuery" since that is the only function
433
+ # in postgres that will create this error.
434
+ #
435
+ # That method will not work for CockroachDB because the error
436
+ # originates from the "runExecBuilder" function, so we need
437
+ # to modify the original to match the CockroachDB behavior.
438
+ def is_cached_plan_failure?(e)
439
+ pgerror = e.cause
440
+
441
+ pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE) == FEATURE_NOT_SUPPORTED &&
442
+ pgerror.result.result_error_field(PG::PG_DIAG_SOURCE_FUNCTION) == "runExecBuilder"
443
+ rescue
444
+ false
445
+ end
446
+
447
+ # override
448
+ # This method loads info about data types from the database to
449
+ # populate the TypeMap.
450
+ #
451
+ # Currently, querying from the pg_type catalog can be slow due to geo-partitioning
452
+ # so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
453
+ def load_additional_types(oids = nil)
454
+ if @config[:use_follower_reads_for_type_introspection]
455
+ initializer = OID::TypeMapInitializer.new(type_map)
456
+
457
+ query = <<~SQL
458
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
459
+ FROM pg_type as t
460
+ LEFT JOIN pg_range as r ON oid = rngtypid AS OF SYSTEM TIME '-10s'
461
+ SQL
462
+
463
+ if oids
464
+ query += "WHERE t.oid IN (%s)" % oids.join(", ")
465
+ else
466
+ query += initializer.query_conditions_for_initial_load
467
+ end
468
+
469
+ execute_and_clear(query, "SCHEMA", []) do |records|
470
+ initializer.run(records)
471
+ end
472
+ else
473
+ super
474
+ end
475
+ rescue ActiveRecord::StatementInvalid => e
476
+ raise e unless e.cause.is_a? PG::InvalidCatalogName
477
+ # use original if database is younger than 10s
478
+ super
479
+ end
480
+
481
+ # override
482
+ # This method maps data types to their proper decoder.
483
+ #
484
+ # Currently, querying from the pg_type catalog can be slow due to geo-partitioning
485
+ # so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
486
+ def add_pg_decoders
487
+ if @config[:use_follower_reads_for_type_introspection]
488
+ @default_timezone = nil
489
+ @timestamp_decoder = nil
490
+
491
+ coders_by_name = {
492
+ "int2" => PG::TextDecoder::Integer,
493
+ "int4" => PG::TextDecoder::Integer,
494
+ "int8" => PG::TextDecoder::Integer,
495
+ "oid" => PG::TextDecoder::Integer,
496
+ "float4" => PG::TextDecoder::Float,
497
+ "float8" => PG::TextDecoder::Float,
498
+ "numeric" => PG::TextDecoder::Numeric,
499
+ "bool" => PG::TextDecoder::Boolean,
500
+ "timestamp" => PG::TextDecoder::TimestampUtc,
501
+ "timestamptz" => PG::TextDecoder::TimestampWithTimeZone,
502
+ }
503
+
504
+ known_coder_types = coders_by_name.keys.map { |n| quote(n) }
505
+ query = <<~SQL % known_coder_types.join(", ")
506
+ SELECT t.oid, t.typname
507
+ FROM pg_type as t AS OF SYSTEM TIME '-10s'
508
+ WHERE t.typname IN (%s)
509
+ SQL
510
+
511
+ coders = execute_and_clear(query, "SCHEMA", []) do |result|
512
+ result
513
+ .map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
514
+ .compact
515
+ end
516
+
517
+ map = PG::TypeMapByOid.new
518
+ coders.each { |coder| map.add_coder(coder) }
519
+ @connection.type_map_for_results = map
520
+
521
+ @type_map_for_results = PG::TypeMapByOid.new
522
+ @type_map_for_results.default_type_map = map
523
+ @type_map_for_results.add_coder(PG::TextDecoder::Bytea.new(oid: 17, name: "bytea"))
524
+ @type_map_for_results.add_coder(MoneyDecoder.new(oid: 790, name: "money"))
525
+
526
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
527
+ @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
528
+ update_typemap_for_default_timezone
529
+ else
530
+ super
531
+ end
532
+ rescue ActiveRecord::StatementInvalid => e
533
+ raise e unless e.cause.is_a? PG::InvalidCatalogName
534
+ # use original if database is younger than 10s
535
+ super
536
+ end
537
+
538
+ def arel_visitor
539
+ Arel::Visitors::CockroachDB.new(self)
540
+ end
541
+
284
542
  # end private
285
543
  end
286
544
  end