activerecord-cockroachdb-adapter 6.0.0beta1 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,25 @@
1
+ require "rgeo/active_record"
2
+
1
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/arel_tosql"
19
+
20
+ # Run to ignore spatial tables that will break schemna dumper.
21
+ # Defined in ./setup.rb
22
+ ActiveRecord::ConnectionAdapters::CockroachDB.initial_setup
10
23
 
11
24
  module ActiveRecord
12
25
  module ConnectionHandling
@@ -24,12 +37,9 @@ module ActiveRecord
24
37
  valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:sslmode, :application_name]
25
38
  conn_params.slice!(*valid_conn_param_keys)
26
39
 
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
40
  conn = PG.connect(conn_params)
31
41
  ConnectionAdapters::CockroachDBAdapter.new(conn, logger, conn_params, config)
32
- rescue ::PG::Error => error
42
+ rescue ::PG::Error, ActiveRecord::ActiveRecordError => error
33
43
  if error.message.include?("does not exist")
34
44
  raise ActiveRecord::NoDatabaseError
35
45
  else
@@ -41,15 +51,127 @@ end
41
51
 
42
52
  module ActiveRecord
43
53
  module ConnectionAdapters
54
+ module CockroachDBConnectionPool
55
+ def initialize(spec)
56
+ super(spec)
57
+ disable_telemetry = spec.config[:disable_cockroachdb_telemetry]
58
+ adapter = spec.config[:adapter]
59
+ return if disable_telemetry || adapter != "cockroachdb"
60
+
61
+ with_connection do |conn|
62
+ if conn.active?
63
+ begin
64
+ query = "SELECT crdb_internal.increment_feature_counter('ActiveRecord %d.%d')"
65
+ conn.execute(query % [ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR])
66
+ rescue ActiveRecord::StatementInvalid
67
+ # The increment_feature_counter built-in is not supported on this
68
+ # CockroachDB version. Ignore.
69
+ rescue StandardError => e
70
+ conn.logger.warn "Unexpected error when incrementing feature counter: #{e}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ ConnectionPool.prepend(CockroachDBConnectionPool)
77
+
44
78
  class CockroachDBAdapter < PostgreSQLAdapter
45
79
  ADAPTER_NAME = "CockroachDB".freeze
46
80
  DEFAULT_PRIMARY_KEY = "rowid"
47
81
 
82
+ SPATIAL_COLUMN_OPTIONS =
83
+ {
84
+ geography: { geographic: true },
85
+ geometry: {},
86
+ geometry_collection: {},
87
+ line_string: {},
88
+ multi_line_string: {},
89
+ multi_point: {},
90
+ multi_polygon: {},
91
+ spatial: {},
92
+ st_point: {},
93
+ st_polygon: {},
94
+ }
95
+
96
+ # http://postgis.17.x6.nabble.com/Default-SRID-td5001115.html
97
+ DEFAULT_SRID = 0
98
+
48
99
  include CockroachDB::SchemaStatements
49
100
  include CockroachDB::ReferentialIntegrity
50
101
  include CockroachDB::DatabaseStatements
51
102
  include CockroachDB::Quoting
52
103
 
104
+ # override
105
+ # This method makes a sql query to gather information about columns
106
+ # in a table. It returns an array of arrays (one for each col) and
107
+ # passes each to the SchemaStatements#new_column_from_field method
108
+ # as the field parameter. This data is then used to format the column
109
+ # objects for the model and sent to the OID for data casting.
110
+ #
111
+ # The issue with the default method is that the sql_type field is
112
+ # retrieved with the `format_type` function, but this is implemented
113
+ # differently in CockroachDB than PostGIS, so geometry/geography
114
+ # types are missing information which makes parsing them impossible.
115
+ # Below is an example of what `format_type` returns for a geometry
116
+ # column.
117
+ #
118
+ # column_type: geometry(POINT, 4326)
119
+ # Expected: geometry(POINT, 4326)
120
+ # Actual: geometry
121
+ #
122
+ # The solution is to make the default query with super, then
123
+ # iterate through the columns and if it is a spatial type,
124
+ # access the proper column_type with the information_schema.columns
125
+ # table.
126
+ #
127
+ # @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
128
+ def column_definitions(table_name)
129
+ fields = super
130
+ # iterate through and identify all spatial fields based on format_type
131
+ # being geometry or geography, then query for the information_schema.column
132
+ # column_type because that contains the necessary information.
133
+ fields.map do |field|
134
+ dtype = field[1]
135
+ if dtype == 'geometry' || dtype == 'geography'
136
+ col_name = field[0]
137
+ data_type = \
138
+ query(<<~SQL, "SCHEMA")
139
+ SELECT c.data_type
140
+ FROM information_schema.columns c
141
+ WHERE c.table_name = #{quote(table_name)}
142
+ AND c.column_name = #{quote(col_name)}
143
+ SQL
144
+ field[1] = data_type[0][0]
145
+ end
146
+ field
147
+ end
148
+ end
149
+
150
+ def arel_visitor
151
+ Arel::Visitors::CockroachDB.new(self)
152
+ end
153
+
154
+ def self.spatial_column_options(key)
155
+ SPATIAL_COLUMN_OPTIONS[key]
156
+ end
157
+
158
+ def postgis_lib_version
159
+ @postgis_lib_version ||= select_value("SELECT PostGIS_Lib_Version()")
160
+ end
161
+
162
+ def default_srid
163
+ DEFAULT_SRID
164
+ end
165
+
166
+ def srs_database_columns
167
+ {
168
+ auth_name_column: "auth_name",
169
+ auth_srid_column: "auth_srid",
170
+ proj4text_column: "proj4text",
171
+ srtext_column: "srtext",
172
+ }
173
+ end
174
+
53
175
  def debugging?
54
176
  !!ENV["DEBUG_COCKROACHDB_ADAPTER"]
55
177
  end
@@ -120,6 +242,10 @@ module ActiveRecord
120
242
  @crdb_version >= 202
121
243
  end
122
244
 
245
+ def supports_partitioned_indexes?
246
+ false
247
+ end
248
+
123
249
  # This is hardcoded to 63 (as previously was in ActiveRecord 5.0) to aid in
124
250
  # migration from PostgreSQL to CockroachDB. In practice, this limitation
125
251
  # is arbitrary since CockroachDB supports index name lengths and table alias
@@ -137,6 +263,7 @@ module ActiveRecord
137
263
 
138
264
  def initialize(connection, logger, conn_params, config)
139
265
  super(connection, logger, conn_params, config)
266
+
140
267
  crdb_version_string = query_value("SHOW crdb_version")
141
268
  if crdb_version_string.include? "v1."
142
269
  version_num = 1
@@ -154,10 +281,31 @@ module ActiveRecord
154
281
  @crdb_version = version_num
155
282
  end
156
283
 
284
+ def self.database_exists?(config)
285
+ !!ActiveRecord::Base.cockroachdb_connection(config)
286
+ rescue ActiveRecord::NoDatabaseError
287
+ false
288
+ end
289
+
157
290
  private
158
291
 
159
292
  def initialize_type_map(m = type_map)
160
- super(m)
293
+ %w(
294
+ geography
295
+ geometry
296
+ geometry_collection
297
+ line_string
298
+ multi_line_string
299
+ multi_point
300
+ multi_polygon
301
+ st_point
302
+ st_polygon
303
+ ).each do |geo_type|
304
+ m.register_type(geo_type) do |oid, _, sql_type|
305
+ CockroachDB::OID::Spatial.new(oid, sql_type)
306
+ end
307
+ end
308
+
161
309
  # NOTE(joey): PostgreSQL intervals have a precision.
162
310
  # CockroachDB intervals do not, so overide the type
163
311
  # definition. Returning a ArgumentError may not be correct.
@@ -169,6 +317,8 @@ module ActiveRecord
169
317
  end
170
318
  OID::SpecializedString.new(:interval, precision: precision)
171
319
  end
320
+
321
+ super(m)
172
322
  end
173
323
 
174
324
  # Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -278,6 +428,100 @@ module ActiveRecord
278
428
  return "{}"
279
429
  end
280
430
 
431
+ # override
432
+ # This method loads info about data types from the database to
433
+ # populate the TypeMap.
434
+ #
435
+ # Currently, querying from the pg_type catalog can be slow due to geo-partitioning
436
+ # so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
437
+ def load_additional_types(oids = nil)
438
+ if @config[:use_follower_reads_for_type_introspection]
439
+ initializer = OID::TypeMapInitializer.new(type_map)
440
+
441
+ query = <<~SQL
442
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
443
+ FROM pg_type as t
444
+ LEFT JOIN pg_range as r ON oid = rngtypid AS OF SYSTEM TIME '-10s'
445
+ SQL
446
+
447
+ if oids
448
+ query += "WHERE t.oid IN (%s)" % oids.join(", ")
449
+ else
450
+ query += initializer.query_conditions_for_initial_load
451
+ end
452
+
453
+ execute_and_clear(query, "SCHEMA", []) do |records|
454
+ initializer.run(records)
455
+ end
456
+ else
457
+ super
458
+ end
459
+ rescue ActiveRecord::StatementInvalid => e
460
+ raise e unless e.cause.is_a? PG::InvalidCatalogName
461
+ # use original if database is younger than 10s
462
+ super
463
+ end
464
+
465
+ # override
466
+ # This method maps data types to their proper decoder.
467
+ #
468
+ # Currently, querying from the pg_type catalog can be slow due to geo-partitioning
469
+ # so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
470
+ def add_pg_decoders
471
+ if @config[:use_follower_reads_for_type_introspection]
472
+ @default_timezone = nil
473
+ @timestamp_decoder = nil
474
+
475
+ coders_by_name = {
476
+ "int2" => PG::TextDecoder::Integer,
477
+ "int4" => PG::TextDecoder::Integer,
478
+ "int8" => PG::TextDecoder::Integer,
479
+ "oid" => PG::TextDecoder::Integer,
480
+ "float4" => PG::TextDecoder::Float,
481
+ "float8" => PG::TextDecoder::Float,
482
+ "numeric" => PG::TextDecoder::Numeric,
483
+ "bool" => PG::TextDecoder::Boolean,
484
+ "timestamp" => PG::TextDecoder::TimestampUtc,
485
+ "timestamptz" => PG::TextDecoder::TimestampWithTimeZone,
486
+ }
487
+
488
+ known_coder_types = coders_by_name.keys.map { |n| quote(n) }
489
+ query = <<~SQL % known_coder_types.join(", ")
490
+ SELECT t.oid, t.typname
491
+ FROM pg_type as t AS OF SYSTEM TIME '-10s'
492
+ WHERE t.typname IN (%s)
493
+ SQL
494
+
495
+ coders = execute_and_clear(query, "SCHEMA", []) do |result|
496
+ result
497
+ .map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
498
+ .compact
499
+ end
500
+
501
+ map = PG::TypeMapByOid.new
502
+ coders.each { |coder| map.add_coder(coder) }
503
+ @connection.type_map_for_results = map
504
+
505
+ @type_map_for_results = PG::TypeMapByOid.new
506
+ @type_map_for_results.default_type_map = map
507
+ @type_map_for_results.add_coder(PG::TextDecoder::Bytea.new(oid: 17, name: "bytea"))
508
+
509
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
510
+ @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
511
+ update_typemap_for_default_timezone
512
+ else
513
+ super
514
+ end
515
+ rescue ActiveRecord::StatementInvalid => e
516
+ raise e unless e.cause.is_a? PG::InvalidCatalogName
517
+ # use original if database is younger than 10s
518
+ super
519
+ end
520
+
521
+ def arel_visitor
522
+ Arel::Visitors::CockroachDB.new(self)
523
+ end
524
+
281
525
  # end private
282
526
  end
283
527
  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.0.0
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-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.20'
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
+ - - "~>"
53
+ - !ruby/object:Gem::Version
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,20 @@ 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/spatial.rb
86
+ - lib/active_record/connection_adapters/cockroachdb/oid/type_map_initializer.rb
68
87
  - lib/active_record/connection_adapters/cockroachdb/quoting.rb
69
88
  - lib/active_record/connection_adapters/cockroachdb/referential_integrity.rb
70
89
  - lib/active_record/connection_adapters/cockroachdb/schema_statements.rb
90
+ - lib/active_record/connection_adapters/cockroachdb/setup.rb
91
+ - lib/active_record/connection_adapters/cockroachdb/spatial_column_info.rb
92
+ - lib/active_record/connection_adapters/cockroachdb/table_definition.rb
71
93
  - lib/active_record/connection_adapters/cockroachdb/transaction_manager.rb
72
94
  - lib/active_record/connection_adapters/cockroachdb/type.rb
73
95
  - lib/active_record/connection_adapters/cockroachdb_adapter.rb
@@ -88,9 +110,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
88
110
  version: '0'
89
111
  required_rubygems_version: !ruby/object:Gem::Requirement
90
112
  requirements:
91
- - - ">"
113
+ - - ">="
92
114
  - !ruby/object:Gem::Version
93
- version: 1.3.1
115
+ version: '0'
94
116
  requirements: []
95
117
  rubygems_version: 3.1.4
96
118
  signing_key: