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

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