activerecord-cockroachdb-adapter 6.0.0.pre.beta.4 → 6.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2eca72798ef552202f48b26be2502ef9a9517cf5bd8cda03a0888f67607d6be0
4
- data.tar.gz: e6ca3dc5323251c3e8b77b3e7ea9553cde259557a035fd48f94413e47583ecfe
3
+ metadata.gz: 50e75b02ff521bc6e8988549a1ceaa7bbd8ed3eddb138e3fdd6f0d7f91b50e23
4
+ data.tar.gz: f67eac0c8dce552a8c5270d6c0740281fb9853ccde09ed53ebfc4bac21904cc7
5
5
  SHA512:
6
- metadata.gz: 03d0c5cbd633dec23e4092fac1bd678f583ebb0b15688eaf54f9b334890a3edf8f78382424f9549036c179f3c66c6d7276f05f99d92030a19f5895be4ac87d2b
7
- data.tar.gz: 5d1d43cfe25affb953cc423b60ea7d0eef0f10c6bb33133b06d1e5d18aed30f3b2c8a7d0ba259148a2d91d2060cecc1848a81e6b6b25c3e2bb02b42eb5cb0038
6
+ metadata.gz: 5ee1478689e350aeb60b9397c106c3576582d9ac8d33af80b4e721f5dfb6aa1509c149250f16869db85fe5ff5b3db3542da0575ad95517681d55fa77db815b19
7
+ data.tar.gz: 57ba40b8ca956807626b147592f55b1b193fffbcc5947ea47a11e1830f42a231a08dc0393c3f80fded3035b1897b0fa5579f37da27e97f83809946c692156766
data/CHANGELOG.md CHANGED
@@ -1,19 +1,24 @@
1
1
  # Changelog
2
2
 
3
- ## 6.0.0-beta.4 - 2021-03-06
3
+ ## 6.1.0 - 2021-04-26
4
4
 
5
- - Improved connection performance by refactoring an introspection
6
- that loads types.
7
- - Changed version numbers to semver.
5
+ - Add a telemetry query on start-up. This helps the Cockroach Labs team
6
+ prioritize support for the adapter. It can be disabled by setting the
7
+ `disable_cockroachdb_telemetry` configuration option to false.
8
8
 
9
- ## 6.0.0beta3
9
+ ## 6.1.0-beta.3 - 2021-04-02
10
10
 
11
- - Added support for spatial features.
11
+ - Added a configuration option named `use_follower_reads_for_type_introspection`.
12
+ If true, it improves the speed of type introspection by allowing potentially stale
13
+ type metadata to be read. Defaults to false.
12
14
 
13
- ## 6.0.0beta2
15
+ ## 6.1.0-beta.2 - 2021-03-06
14
16
 
15
- - Updated transaction retry logic to work with Rails 6.
17
+ - Improved connection performance by refactoring an introspection
18
+ that loads types.
19
+ - Changed version numbers to semver.
16
20
 
17
- ## 6.0.0beta1
21
+ ## 6.1.0beta1
18
22
 
19
- - Initial support for Rails 6.
23
+ - Initial support for Rails 6.1.
24
+ - Support for spatial functionality.
data/CONTRIBUTING.md CHANGED
@@ -78,6 +78,26 @@ RAILS_SOURCE="path/to/local_copy" bundle exec rake test
78
78
 
79
79
  `test/config.yml` assumes CockroachDB will be running at localhost:26257 with a root user. Make changes to `test/config.yml` as needed.
80
80
 
81
+ ### Run Tests from a Backup
82
+
83
+ Loading the full test schema every time a test runs can take a while, so for cases where loading the schema sequentially is unimportant, it is possible to use a backup to set up the database. This is significantly faster than the standard method and is provided to run individual tests faster, but should not be used to validate a build.
84
+
85
+ First create the template database.
86
+
87
+ ```bash
88
+ bundle exec rake db:create_test_template
89
+ ```
90
+
91
+ This will create a template database for the current version (ex. `activerecord_test_template611` for version 6.1.1) and create a `BACKUP` in the `nodelocal://self/activerecord-crdb-adapter/#{activerecord_version}` directory.
92
+
93
+ To load from the template, use the `COCKROACH_LOAD_FROM_TEMPLATE` flag.
94
+
95
+ ```bash
96
+ COCKROACH_LOAD_FROM_TEMPLATE=1 TEST_FILES="test/cases/adapters/postgresql/ddl_test.rb" bundle exec rake test
97
+ ```
98
+
99
+ And the `activerecord_unittest` database will use the `RESTORE` command to load the schema from the template database.
100
+
81
101
  # Improvements
82
102
 
83
103
 
data/README.md CHANGED
@@ -22,6 +22,13 @@ development:
22
22
  user: <username>
23
23
  ```
24
24
 
25
+ ## Configuration
26
+
27
+ In addition to the standard adapter settings, CockroachDB also supports the following:
28
+
29
+ - `use_follower_reads_for_type_introspection`: Use follower reads on queries to the `pg_type` catalog when set to `true`. This helps to speed up initialization by reading historical data, but may not find recently created user-defined types.
30
+ - `disable_cockroachdb_telemetry`: Determines if a telemetry call is made to the database when the connection pool is initialized. Setting this to `true` will prevent the call from being made.
31
+
25
32
  ## Working with Spatial Data
26
33
 
27
34
  The adapter uses [RGeo](https://github.com/rgeo/rgeo) and [RGeo-ActiveRecord](https://github.com/rgeo/rgeo-activerecord) to represent geometric and geographic data as Ruby objects and easily interface them with the adapter. The following is a brief introduction to RGeo and tips to help setup your spatial application. More documentation about RGeo can be found in the [YARD Docs](https://rubydoc.info/github/rgeo/rgeo) and [wiki](https://github.com/rgeo/rgeo/wiki).
data/Rakefile CHANGED
@@ -2,10 +2,30 @@ require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
3
  require_relative 'test/support/paths_cockroachdb'
4
4
  require_relative 'test/support/rake_helpers'
5
+ require_relative 'test/support/template_creator'
5
6
 
6
7
  task test: ["test:cockroachdb"]
7
8
  task default: [:test]
8
9
 
10
+ namespace :db do
11
+ task "create_test_template" do
12
+ ENV['DEBUG_COCKROACHDB_ADAPTER'] = "1"
13
+ ENV['COCKROACH_SKIP_LOAD_SCHEMA'] = "1"
14
+ ENV["ARCONN"] = "cockroachdb"
15
+
16
+ TemplateCreator.connect
17
+ require_relative 'test/cases/helper'
18
+
19
+ # TODO: look into this more, but for some reason the blob alias
20
+ # is not defined while running this task.
21
+ ActiveRecord::ConnectionAdapters::CockroachDB::TableDefinition.class_eval do
22
+ alias :blob :binary
23
+ end
24
+
25
+ TemplateCreator.create_test_template
26
+ end
27
+ end
28
+
9
29
  namespace :test do
10
30
  Rake::TestTask.new("cockroachdb") do |t|
11
31
  t.libs = ARTest::CockroachDB.test_load_paths
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "activerecord-cockroachdb-adapter"
7
- spec.version = "6.0.0-beta.4"
7
+ spec.version = "6.1.0"
8
8
  spec.licenses = ["Apache-2.0"]
9
9
  spec.authors = ["Cockroach Labs"]
10
10
  spec.email = ["cockroach-db@googlegroups.com"]
@@ -13,8 +13,8 @@ Gem::Specification.new do |spec|
13
13
  spec.description = "Allows the use of CockroachDB as a backend for ActiveRecord and Rails apps."
14
14
  spec.homepage = "https://github.com/cockroachdb/activerecord-cockroachdb-adapter"
15
15
 
16
- spec.add_dependency "activerecord", "~> 6.0.3"
17
- spec.add_dependency "pg", ">= 0.20"
16
+ spec.add_dependency "activerecord", "~> 6.1"
17
+ spec.add_dependency "pg", "~> 1.2"
18
18
  spec.add_dependency "rgeo-activerecord", "~> 7.0.0"
19
19
 
20
20
  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
@@ -11,6 +11,7 @@ connections:
11
11
  user: root
12
12
  requiressl: disable
13
13
  min_messages: warning
14
+ disable_cockroachdb_telemetry: true
14
15
  arunit_without_prepared_statements:
15
16
  database: activerecord_unittest
16
17
  host: localhost
@@ -19,6 +20,7 @@ connections:
19
20
  requiressl: disable
20
21
  min_messages: warning
21
22
  prepared_statements: false
23
+ disable_cockroachdb_telemetry: true
22
24
  arunit2:
23
25
  database: activerecord_unittest2
24
26
  host: localhost
@@ -26,3 +28,4 @@ connections:
26
28
  user: root
27
29
  requiressl: disable
28
30
  min_messages: warning
31
+ disable_cockroachdb_telemetry: true
@@ -0,0 +1,126 @@
1
+ # frozen-string-literal: true
2
+
3
+ require "active_support/duration"
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module CockroachDB
8
+ module OID
9
+ module Interval # :nodoc:
10
+ DEFAULT_PRECISION = 6 # microseconds
11
+
12
+ def cast_value(value)
13
+ case value
14
+ when ::ActiveSupport::Duration
15
+ value
16
+ when ::String
17
+ begin
18
+ PostgresqlInterval::Parser.parse(value)
19
+ rescue PostgresqlInterval::ParseError
20
+ # Try ISO 8601
21
+ super
22
+ end
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ def serialize(value)
29
+ precision = self.precision || DEFAULT_PRECISION
30
+ case value
31
+ when ::ActiveSupport::Duration
32
+ serialize_duration(value, precision)
33
+ when ::Numeric
34
+ serialize_duration(value.seconds, precision)
35
+ else
36
+ super
37
+ end
38
+ end
39
+
40
+ def type_cast_for_schema(value)
41
+ serialize(value).inspect
42
+ end
43
+
44
+ private
45
+
46
+ # Convert an ActiveSupport::Duration to
47
+ # the postgres interval style
48
+ # ex. 1 year 2 mons 3 days 4 hours 5 minutes 6 seconds
49
+ def serialize_duration(value, precision)
50
+ yrs = value.parts.fetch(:years, 0)
51
+ mons = value.parts.fetch(:months, 0)
52
+ days = value.parts.fetch(:days, 0)
53
+ hrs = value.parts.fetch(:hours, 0)
54
+ mins = value.parts.fetch(:minutes, 0)
55
+ secs = value.parts.fetch(:seconds, 0).round(precision)
56
+
57
+ "#{yrs} years #{mons} mons #{days} days #{hrs} hours #{mins} minutes #{secs} seconds"
58
+ end
59
+ end
60
+
61
+ PostgreSQL::OID::Interval.prepend(Interval)
62
+ end
63
+
64
+ module PostgresqlInterval
65
+ class Parser
66
+ PARTS = ActiveSupport::Duration::PARTS
67
+ PARTS_IN_SECONDS = ActiveSupport::Duration::PARTS_IN_SECONDS
68
+
69
+ # modified regex from https://github.com/jeremyevans/sequel/blob/master/lib/sequel/extensions/pg_interval.rb#L86
70
+ REGEX = /\A([+-]?\d+ years?\s?)?([+-]?\d+ mons?\s?)?([+-]?\d+ days?\s?)?(?:([+-])?(\d{2,10}):(\d\d):(\d\d(\.\d+)?))?\z/
71
+
72
+ def self.parse(string)
73
+ matches = REGEX.match(string)
74
+ raise(ParseError) unless matches
75
+
76
+ # 1 => years, 2 => months, 3 => days, 4 => nil, 5 => hours,
77
+ # 6 => minutes, 7 => seconds with fraction digits, 8 => fractional portion of 7
78
+ duration = 0
79
+ parts = {}
80
+
81
+ if matches[1]
82
+ val = matches[1].to_i
83
+ duration += val * PARTS_IN_SECONDS[:years]
84
+ parts[:years] = val
85
+ end
86
+
87
+ if matches[2]
88
+ val = matches[2].to_i
89
+ duration += val * PARTS_IN_SECONDS[:months]
90
+ parts[:months] = val
91
+ end
92
+
93
+ if matches[3]
94
+ val = matches[3].to_i
95
+ duration += val * PARTS_IN_SECONDS[:days]
96
+ parts[:days] = val
97
+ end
98
+
99
+ if matches[5]
100
+ val = matches[5].to_i
101
+ duration += val * PARTS_IN_SECONDS[:hours]
102
+ parts[:hours] = val
103
+ end
104
+
105
+ if matches[6]
106
+ val = matches[6].to_i
107
+ duration += val * PARTS_IN_SECONDS[:minutes]
108
+ parts[:minutes] = val
109
+ end
110
+
111
+ if matches[7]
112
+ val = matches[7].to_f
113
+ duration += val * PARTS_IN_SECONDS[:seconds]
114
+ parts[:seconds] = val
115
+ end
116
+
117
+ ActiveSupport::Duration.new(duration, parts)
118
+ end
119
+ end
120
+
121
+ class ParseError < StandardError
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -4,7 +4,7 @@ module ActiveRecord
4
4
  module SchemaStatements
5
5
  include ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements
6
6
 
7
- def add_index(table_name, column_name, options = {})
7
+ def add_index(table_name, column_name, **options)
8
8
  super
9
9
  rescue ActiveRecord::StatementInvalid => error
10
10
  if debugging? && error.cause.class == PG::FeatureNotSupported
@@ -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,6 +1,6 @@
1
1
  require "rgeo/active_record"
2
2
 
3
- require 'active_record/connection_adapters/postgresql_adapter'
3
+ require "active_record/connection_adapters/postgresql_adapter"
4
4
  require "active_record/connection_adapters/cockroachdb/column_methods"
5
5
  require "active_record/connection_adapters/cockroachdb/schema_statements"
6
6
  require "active_record/connection_adapters/cockroachdb/referential_integrity"
@@ -15,6 +15,7 @@ require "active_record/connection_adapters/cockroachdb/spatial_column_info"
15
15
  require "active_record/connection_adapters/cockroachdb/setup"
16
16
  require "active_record/connection_adapters/cockroachdb/oid/type_map_initializer"
17
17
  require "active_record/connection_adapters/cockroachdb/oid/spatial"
18
+ require "active_record/connection_adapters/cockroachdb/oid/interval"
18
19
  require "active_record/connection_adapters/cockroachdb/arel_tosql"
19
20
 
20
21
  # Run to ignore spatial tables that will break schemna dumper.
@@ -25,25 +26,29 @@ module ActiveRecord
25
26
  module ConnectionHandling
26
27
  def cockroachdb_connection(config)
27
28
  # This is copied from the PostgreSQL adapter.
28
- conn_params = config.symbolize_keys
29
-
30
- conn_params.delete_if { |_, v| v.nil? }
29
+ conn_params = config.symbolize_keys.compact
31
30
 
32
31
  # Map ActiveRecords param names to PGs.
33
32
  conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
34
33
  conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
35
34
 
36
35
  # Forward only valid config params to PG::Connection.connect.
37
- valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:sslmode, :application_name]
36
+ valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
38
37
  conn_params.slice!(*valid_conn_param_keys)
39
38
 
40
- conn = PG.connect(conn_params)
41
- ConnectionAdapters::CockroachDBAdapter.new(conn, logger, conn_params, config)
42
- rescue ::PG::Error, ActiveRecord::ActiveRecordError => error
43
- 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])
44
49
  raise ActiveRecord::NoDatabaseError
45
50
  else
46
- raise
51
+ raise ActiveRecord::ConnectionNotEstablished, error.message
47
52
  end
48
53
  end
49
54
  end
@@ -51,6 +56,30 @@ end
51
56
 
52
57
  module ActiveRecord
53
58
  module ConnectionAdapters
59
+ module CockroachDBConnectionPool
60
+ def initialize(pool_config)
61
+ super(pool_config)
62
+ disable_telemetry = pool_config.db_config.configuration_hash[:disable_cockroachdb_telemetry]
63
+ adapter = pool_config.db_config.configuration_hash[:adapter]
64
+ return if disable_telemetry || adapter != "cockroachdb"
65
+
66
+ with_connection do |conn|
67
+ if conn.active?
68
+ begin
69
+ query = "SELECT crdb_internal.increment_feature_counter('ActiveRecord %d.%d')"
70
+ conn.execute(query % [ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR])
71
+ rescue ActiveRecord::StatementInvalid
72
+ # The increment_feature_counter built-in is not supported on this
73
+ # CockroachDB version. Ignore.
74
+ rescue StandardError => e
75
+ conn.logger.warn "Unexpected error when incrementing feature counter: #{e}"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ ConnectionPool.prepend(CockroachDBConnectionPool)
82
+
54
83
  class CockroachDBAdapter < PostgreSQLAdapter
55
84
  ADAPTER_NAME = "CockroachDB".freeze
56
85
  DEFAULT_PRIMARY_KEY = "rowid"
@@ -77,56 +106,6 @@ module ActiveRecord
77
106
  include CockroachDB::DatabaseStatements
78
107
  include CockroachDB::Quoting
79
108
 
80
- # override
81
- # This method makes a sql query to gather information about columns
82
- # in a table. It returns an array of arrays (one for each col) and
83
- # passes each to the SchemaStatements#new_column_from_field method
84
- # as the field parameter. This data is then used to format the column
85
- # objects for the model and sent to the OID for data casting.
86
- #
87
- # The issue with the default method is that the sql_type field is
88
- # retrieved with the `format_type` function, but this is implemented
89
- # differently in CockroachDB than PostGIS, so geometry/geography
90
- # types are missing information which makes parsing them impossible.
91
- # Below is an example of what `format_type` returns for a geometry
92
- # column.
93
- #
94
- # column_type: geometry(POINT, 4326)
95
- # Expected: geometry(POINT, 4326)
96
- # Actual: geometry
97
- #
98
- # The solution is to make the default query with super, then
99
- # iterate through the columns and if it is a spatial type,
100
- # access the proper column_type with the information_schema.columns
101
- # table.
102
- #
103
- # @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
104
- def column_definitions(table_name)
105
- fields = super
106
- # iterate through and identify all spatial fields based on format_type
107
- # being geometry or geography, then query for the information_schema.column
108
- # column_type because that contains the necessary information.
109
- fields.map do |field|
110
- dtype = field[1]
111
- if dtype == 'geometry' || dtype == 'geography'
112
- col_name = field[0]
113
- data_type = \
114
- query(<<~SQL, "SCHEMA")
115
- SELECT c.data_type
116
- FROM information_schema.columns c
117
- WHERE c.table_name = #{quote(table_name)}
118
- AND c.column_name = #{quote(col_name)}
119
- SQL
120
- field[1] = data_type[0][0]
121
- end
122
- field
123
- end
124
- end
125
-
126
- def arel_visitor
127
- Arel::Visitors::CockroachDB.new(self)
128
- end
129
-
130
109
  def self.spatial_column_options(key)
131
110
  SPATIAL_COLUMN_OPTIONS[key]
132
111
  end
@@ -218,6 +197,10 @@ module ActiveRecord
218
197
  @crdb_version >= 202
219
198
  end
220
199
 
200
+ def supports_partitioned_indexes?
201
+ false
202
+ end
203
+
221
204
  # This is hardcoded to 63 (as previously was in ActiveRecord 5.0) to aid in
222
205
  # migration from PostgreSQL to CockroachDB. In practice, this limitation
223
206
  # is arbitrary since CockroachDB supports index name lengths and table alias
@@ -235,6 +218,7 @@ module ActiveRecord
235
218
 
236
219
  def initialize(connection, logger, conn_params, config)
237
220
  super(connection, logger, conn_params, config)
221
+
238
222
  crdb_version_string = query_value("SHOW crdb_version")
239
223
  if crdb_version_string.include? "v1."
240
224
  version_num = 1
@@ -277,19 +261,31 @@ module ActiveRecord
277
261
  end
278
262
  end
279
263
 
280
- # NOTE(joey): PostgreSQL intervals have a precision.
281
- # CockroachDB intervals do not, so overide the type
282
- # definition. Returning a ArgumentError may not be correct.
283
- # This needs to be tested.
284
- m.register_type "interval" do |_, _, sql_type|
264
+ # Belongs after other types are defined because of issues described
265
+ # in this https://github.com/rails/rails/pull/38571
266
+ # Once that PR is merged, we can call super at the top.
267
+ super(m)
268
+
269
+ # Override numeric type. This is almost identical to the default,
270
+ # except that the conditional based on the fmod is changed.
271
+ m.register_type "numeric" do |_, fmod, sql_type|
285
272
  precision = extract_precision(sql_type)
286
- if precision
287
- raise(ArgumentError, "CockroachDB does not support precision on intervals, but got precision: #{precision}")
273
+ scale = extract_scale(sql_type)
274
+
275
+ # TODO(#178) this should never use DecimalWithoutScale since scale
276
+ # is assumed to be 0 if it is not explicitly defined.
277
+ #
278
+ # If fmod is -1, that means that precision is defined but not
279
+ # scale, or neither is defined.
280
+ if fmod && fmod == -1
281
+ # Below comment is from ActiveRecord
282
+ # FIXME: Remove this class, and the second argument to
283
+ # lookups on PG
284
+ Type::DecimalWithoutScale.new(precision: precision)
285
+ else
286
+ OID::Decimal.new(precision: precision, scale: scale)
288
287
  end
289
- OID::SpecializedString.new(:interval, precision: precision)
290
288
  end
291
-
292
- super(m)
293
289
  end
294
290
 
295
291
  # Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -399,6 +395,175 @@ module ActiveRecord
399
395
  return "{}"
400
396
  end
401
397
 
398
+ # override
399
+ # This method makes a query to gather information about columns
400
+ # in a table. It returns an array of arrays (one for each col) and
401
+ # passes each to the SchemaStatements#new_column_from_field method
402
+ # as the field parameter. This data is then used to format the column
403
+ # objects for the model and sent to the OID for data casting.
404
+ #
405
+ # Sometimes there are differences between how data is formatted
406
+ # in Postgres and CockroachDB, so additional queries for certain types
407
+ # may be necessary to properly form the column definition.
408
+ #
409
+ # @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
410
+ def column_definitions(table_name)
411
+ fields = super
412
+
413
+ # Use regex comparison because if a type is an array it will
414
+ # have [] appended to the end of it.
415
+ target_types = [
416
+ /geometry/,
417
+ /geography/,
418
+ /interval/,
419
+ /numeric/
420
+ ]
421
+ re = Regexp.union(target_types)
422
+ fields.map do |field|
423
+ dtype = field[1]
424
+ if re.match(dtype)
425
+ crdb_column_definition(field, table_name)
426
+ else
427
+ field
428
+ end
429
+ end
430
+ end
431
+
432
+ # Use the crdb_sql_type instead of the sql_type returned by
433
+ # column_definitions. This will include limit,
434
+ # precision, and scale information in the type.
435
+ # Ex. geometry -> geometry(point, 4326)
436
+ def crdb_column_definition(field, table_name)
437
+ col_name = field[0]
438
+ data_type = \
439
+ query(<<~SQL, "SCHEMA")
440
+ SELECT c.crdb_sql_type
441
+ FROM information_schema.columns c
442
+ WHERE c.table_name = #{quote(table_name)}
443
+ AND c.column_name = #{quote(col_name)}
444
+ SQL
445
+ field[1] = data_type[0][0].downcase
446
+ field
447
+ end
448
+
449
+ # override
450
+ # This method is used to determine if a
451
+ # FEATURE_NOT_SUPPORTED error from the PG gem should
452
+ # be an ActiveRecord::PreparedStatementCacheExpired
453
+ # error.
454
+ #
455
+ # ActiveRecord handles this by checking that the sql state matches the
456
+ # FEATURE_NOT_SUPPORTED code and that the source function
457
+ # is "RevalidateCachedQuery" since that is the only function
458
+ # in postgres that will create this error.
459
+ #
460
+ # That method will not work for CockroachDB because the error
461
+ # originates from the "runExecBuilder" function, so we need
462
+ # to modify the original to match the CockroachDB behavior.
463
+ def is_cached_plan_failure?(e)
464
+ pgerror = e.cause
465
+
466
+ pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE) == FEATURE_NOT_SUPPORTED &&
467
+ pgerror.result.result_error_field(PG::PG_DIAG_SOURCE_FUNCTION) == "runExecBuilder"
468
+ rescue
469
+ false
470
+ end
471
+
472
+ # override
473
+ # This method loads info about data types from the database to
474
+ # populate the TypeMap.
475
+ #
476
+ # Currently, querying from the pg_type catalog can be slow due to geo-partitioning
477
+ # so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
478
+ def load_additional_types(oids = nil)
479
+ if @config[:use_follower_reads_for_type_introspection]
480
+ initializer = OID::TypeMapInitializer.new(type_map)
481
+
482
+ query = <<~SQL
483
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
484
+ FROM pg_type as t
485
+ LEFT JOIN pg_range as r ON oid = rngtypid AS OF SYSTEM TIME '-10s'
486
+ SQL
487
+
488
+ if oids
489
+ query += "WHERE t.oid IN (%s)" % oids.join(", ")
490
+ else
491
+ query += initializer.query_conditions_for_initial_load
492
+ end
493
+
494
+ execute_and_clear(query, "SCHEMA", []) do |records|
495
+ initializer.run(records)
496
+ end
497
+ else
498
+ super
499
+ end
500
+ rescue ActiveRecord::StatementInvalid => e
501
+ raise e unless e.cause.is_a? PG::InvalidCatalogName
502
+ # use original if database is younger than 10s
503
+ super
504
+ end
505
+
506
+ # override
507
+ # This method maps data types to their proper decoder.
508
+ #
509
+ # Currently, querying from the pg_type catalog can be slow due to geo-partitioning
510
+ # so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
511
+ def add_pg_decoders
512
+ if @config[:use_follower_reads_for_type_introspection]
513
+ @default_timezone = nil
514
+ @timestamp_decoder = nil
515
+
516
+ coders_by_name = {
517
+ "int2" => PG::TextDecoder::Integer,
518
+ "int4" => PG::TextDecoder::Integer,
519
+ "int8" => PG::TextDecoder::Integer,
520
+ "oid" => PG::TextDecoder::Integer,
521
+ "float4" => PG::TextDecoder::Float,
522
+ "float8" => PG::TextDecoder::Float,
523
+ "numeric" => PG::TextDecoder::Numeric,
524
+ "bool" => PG::TextDecoder::Boolean,
525
+ "timestamp" => PG::TextDecoder::TimestampUtc,
526
+ "timestamptz" => PG::TextDecoder::TimestampWithTimeZone,
527
+ }
528
+
529
+ known_coder_types = coders_by_name.keys.map { |n| quote(n) }
530
+ query = <<~SQL % known_coder_types.join(", ")
531
+ SELECT t.oid, t.typname
532
+ FROM pg_type as t AS OF SYSTEM TIME '-10s'
533
+ WHERE t.typname IN (%s)
534
+ SQL
535
+
536
+ coders = execute_and_clear(query, "SCHEMA", []) do |result|
537
+ result
538
+ .map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
539
+ .compact
540
+ end
541
+
542
+ map = PG::TypeMapByOid.new
543
+ coders.each { |coder| map.add_coder(coder) }
544
+ @connection.type_map_for_results = map
545
+
546
+ @type_map_for_results = PG::TypeMapByOid.new
547
+ @type_map_for_results.default_type_map = map
548
+ @type_map_for_results.add_coder(PG::TextDecoder::Bytea.new(oid: 17, name: "bytea"))
549
+ @type_map_for_results.add_coder(MoneyDecoder.new(oid: 790, name: "money"))
550
+
551
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
552
+ @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
553
+ update_typemap_for_default_timezone
554
+ else
555
+ super
556
+ end
557
+ rescue ActiveRecord::StatementInvalid => e
558
+ raise e unless e.cause.is_a? PG::InvalidCatalogName
559
+ # use original if database is younger than 10s
560
+ super
561
+ end
562
+
563
+ def arel_visitor
564
+ Arel::Visitors::CockroachDB.new(self)
565
+ end
566
+
402
567
  # end private
403
568
  end
404
569
  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.0.pre.beta.4
4
+ version: 6.1.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: 2021-03-06 00:00:00.000000000 Z
11
+ date: 2021-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,28 +16,28 @@ 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
39
  - !ruby/object:Gem::Version
40
- version: '0.20'
40
+ version: '1.2'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rgeo-activerecord
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -82,6 +82,7 @@ files:
82
82
  - lib/active_record/connection_adapters/cockroachdb/column_methods.rb
83
83
  - lib/active_record/connection_adapters/cockroachdb/database_statements.rb
84
84
  - lib/active_record/connection_adapters/cockroachdb/database_tasks.rb
85
+ - lib/active_record/connection_adapters/cockroachdb/oid/interval.rb
85
86
  - lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb
86
87
  - lib/active_record/connection_adapters/cockroachdb/oid/type_map_initializer.rb
87
88
  - lib/active_record/connection_adapters/cockroachdb/quoting.rb
@@ -110,9 +111,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
110
111
  version: '0'
111
112
  required_rubygems_version: !ruby/object:Gem::Requirement
112
113
  requirements:
113
- - - ">"
114
+ - - ">="
114
115
  - !ruby/object:Gem::Version
115
- version: 1.3.1
116
+ version: '0'
116
117
  requirements: []
117
118
  rubygems_version: 3.1.4
118
119
  signing_key: