activerecord-cockroachdb-adapter 6.0.0beta3 → 6.1.0beta1

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: a2fb5df1ff24503630d2700365d1c6e9b8b8f84306ce5d2b331712f86adea1f5
4
- data.tar.gz: ffa086c2093b8e80424e5b85847d6f0a7a3503437df8168e72c1abab0a042528
3
+ metadata.gz: 5d0954fd425d90342f04c1b6559b78e398be9e523e67779476d81f925a1aa611
4
+ data.tar.gz: 1f990c34784394bafa77933eb0d49965a0eac81151f2468c92a66c1bc9beb78a
5
5
  SHA512:
6
- metadata.gz: b8e3520c981683e242469bcef709c715aa5fe9240b1b6a49fb930dfb8d9f9f395ee4acfae615d06b477e70fc10f8ba1c98bf4bb3489484ea4a2a7f4ec4f5fd54
7
- data.tar.gz: 6368fb8ed0c73348538dc978585beb495838002cea456071ddd4026c2435d3ff146626c19f3101070c1d2fba84efc222aab17a57bba3410130e213eb6379b2ea
6
+ metadata.gz: 3074426e81a83bb4ae2e50047c6db97dc75d9b8a822c3fd1153fb3403457bb56e7f36acb42802e5dd777c056dd0864db7dc26e875e7a644cb158f0a1c8001f3a
7
+ data.tar.gz: 8ff7c637812c1b262452ca1ef6ad8308a3f620a47e3798c6ef7b93096fedb362ce52372f6c0b870af1b475a31cf83f670f4e6d65498c231f5759a475820568af
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/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.0beta3"
7
+ spec.version = "6.1.0beta1"
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'
@@ -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"
@@ -14,6 +14,7 @@ require "active_record/connection_adapters/cockroachdb/column"
14
14
  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/spatial"
17
+ require "active_record/connection_adapters/cockroachdb/oid/interval"
17
18
  require "active_record/connection_adapters/cockroachdb/arel_tosql"
18
19
 
19
20
  # Run to ignore spatial tables that will break schemna dumper.
@@ -24,25 +25,29 @@ module ActiveRecord
24
25
  module ConnectionHandling
25
26
  def cockroachdb_connection(config)
26
27
  # This is copied from the PostgreSQL adapter.
27
- conn_params = config.symbolize_keys
28
-
29
- conn_params.delete_if { |_, v| v.nil? }
28
+ conn_params = config.symbolize_keys.compact
30
29
 
31
30
  # Map ActiveRecords param names to PGs.
32
31
  conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
33
32
  conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
34
33
 
35
34
  # Forward only valid config params to PG::Connection.connect.
36
- valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:sslmode, :application_name]
35
+ valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
37
36
  conn_params.slice!(*valid_conn_param_keys)
38
37
 
39
- conn = PG.connect(conn_params)
40
- ConnectionAdapters::CockroachDBAdapter.new(conn, logger, conn_params, config)
41
- rescue ::PG::Error, ActiveRecord::ActiveRecordError => error
42
- if error.message.include?("does not exist")
38
+ ConnectionAdapters::CockroachDBAdapter.new(
39
+ ConnectionAdapters::CockroachDBAdapter.new_client(conn_params),
40
+ logger,
41
+ conn_params,
42
+ config
43
+ )
44
+ # This rescue flow appears in new_client, but it is needed here as well
45
+ # since Cockroach will sometimes not raise until a query is made.
46
+ rescue ActiveRecord::StatementInvalid => error
47
+ if conn_params && conn_params[:dbname] && error.cause.message.include?(conn_params[:dbname])
43
48
  raise ActiveRecord::NoDatabaseError
44
49
  else
45
- raise
50
+ raise ActiveRecord::ConnectionNotEstablished, error.message
46
51
  end
47
52
  end
48
53
  end
@@ -76,56 +81,6 @@ module ActiveRecord
76
81
  include CockroachDB::DatabaseStatements
77
82
  include CockroachDB::Quoting
78
83
 
79
- # override
80
- # This method makes a sql query to gather information about columns
81
- # in a table. It returns an array of arrays (one for each col) and
82
- # passes each to the SchemaStatements#new_column_from_field method
83
- # as the field parameter. This data is then used to format the column
84
- # objects for the model and sent to the OID for data casting.
85
- #
86
- # The issue with the default method is that the sql_type field is
87
- # retrieved with the `format_type` function, but this is implemented
88
- # differently in CockroachDB than PostGIS, so geometry/geography
89
- # types are missing information which makes parsing them impossible.
90
- # Below is an example of what `format_type` returns for a geometry
91
- # column.
92
- #
93
- # column_type: geometry(POINT, 4326)
94
- # Expected: geometry(POINT, 4326)
95
- # Actual: geometry
96
- #
97
- # The solution is to make the default query with super, then
98
- # iterate through the columns and if it is a spatial type,
99
- # access the proper column_type with the information_schema.columns
100
- # table.
101
- #
102
- # @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
103
- def column_definitions(table_name)
104
- fields = super
105
- # iterate through and identify all spatial fields based on format_type
106
- # being geometry or geography, then query for the information_schema.column
107
- # column_type because that contains the necessary information.
108
- fields.map do |field|
109
- dtype = field[1]
110
- if dtype == 'geometry' || dtype == 'geography'
111
- col_name = field[0]
112
- data_type = \
113
- query(<<~SQL, "SCHEMA")
114
- SELECT c.data_type
115
- FROM information_schema.columns c
116
- WHERE c.table_name = #{quote(table_name)}
117
- AND c.column_name = #{quote(col_name)}
118
- SQL
119
- field[1] = data_type[0][0]
120
- end
121
- field
122
- end
123
- end
124
-
125
- def arel_visitor
126
- Arel::Visitors::CockroachDB.new(self)
127
- end
128
-
129
84
  def self.spatial_column_options(key)
130
85
  SPATIAL_COLUMN_OPTIONS[key]
131
86
  end
@@ -276,19 +231,31 @@ module ActiveRecord
276
231
  end
277
232
  end
278
233
 
279
- # NOTE(joey): PostgreSQL intervals have a precision.
280
- # CockroachDB intervals do not, so overide the type
281
- # definition. Returning a ArgumentError may not be correct.
282
- # This needs to be tested.
283
- m.register_type "interval" do |_, _, sql_type|
234
+ # Belongs after other types are defined because of issues described
235
+ # in this https://github.com/rails/rails/pull/38571
236
+ # Once that PR is merged, we can call super at the top.
237
+ super(m)
238
+
239
+ # Override numeric type. This is almost identical to the default,
240
+ # except that the conditional based on the fmod is changed.
241
+ m.register_type "numeric" do |_, fmod, sql_type|
284
242
  precision = extract_precision(sql_type)
285
- if precision
286
- raise(ArgumentError, "CockroachDB does not support precision on intervals, but got precision: #{precision}")
243
+ scale = extract_scale(sql_type)
244
+
245
+ # TODO(#178) this should never use DecimalWithoutScale since scale
246
+ # is assumed to be 0 if it is not explicitly defined.
247
+ #
248
+ # If fmod is -1, that means that precision is defined but not
249
+ # scale, or neither is defined.
250
+ if fmod && fmod == -1
251
+ # Below comment is from ActiveRecord
252
+ # FIXME: Remove this class, and the second argument to
253
+ # lookups on PG
254
+ Type::DecimalWithoutScale.new(precision: precision)
255
+ else
256
+ OID::Decimal.new(precision: precision, scale: scale)
287
257
  end
288
- OID::SpecializedString.new(:interval, precision: precision)
289
258
  end
290
-
291
- super(m)
292
259
  end
293
260
 
294
261
  # Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -398,6 +365,84 @@ module ActiveRecord
398
365
  return "{}"
399
366
  end
400
367
 
368
+ # override
369
+ # This method makes a query to gather information about columns
370
+ # in a table. It returns an array of arrays (one for each col) and
371
+ # passes each to the SchemaStatements#new_column_from_field method
372
+ # as the field parameter. This data is then used to format the column
373
+ # objects for the model and sent to the OID for data casting.
374
+ #
375
+ # Sometimes there are differences between how data is formatted
376
+ # in Postgres and CockroachDB, so additional queries for certain types
377
+ # may be necessary to properly form the column definition.
378
+ #
379
+ # @see: https://github.com/rails/rails/blob/8695b028261bdd244e254993255c6641bdbc17a5/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L829
380
+ def column_definitions(table_name)
381
+ fields = super
382
+
383
+ # Use regex comparison because if a type is an array it will
384
+ # have [] appended to the end of it.
385
+ target_types = [
386
+ /geometry/,
387
+ /geography/,
388
+ /interval/,
389
+ /numeric/
390
+ ]
391
+ re = Regexp.union(target_types)
392
+ fields.map do |field|
393
+ dtype = field[1]
394
+ if re.match(dtype)
395
+ crdb_column_definition(field, table_name)
396
+ else
397
+ field
398
+ end
399
+ end
400
+ end
401
+
402
+ # Use the crdb_sql_type instead of the sql_type returned by
403
+ # column_definitions. This will include limit,
404
+ # precision, and scale information in the type.
405
+ # Ex. geometry -> geometry(point, 4326)
406
+ def crdb_column_definition(field, table_name)
407
+ col_name = field[0]
408
+ data_type = \
409
+ query(<<~SQL, "SCHEMA")
410
+ SELECT c.crdb_sql_type
411
+ FROM information_schema.columns c
412
+ WHERE c.table_name = #{quote(table_name)}
413
+ AND c.column_name = #{quote(col_name)}
414
+ SQL
415
+ field[1] = data_type[0][0].downcase
416
+ field
417
+ end
418
+
419
+ # override
420
+ # This method is used to determine if a
421
+ # FEATURE_NOT_SUPPORTED error from the PG gem should
422
+ # be an ActiveRecord::PreparedStatementCacheExpired
423
+ # error.
424
+ #
425
+ # ActiveRecord handles this by checking that the sql state matches the
426
+ # FEATURE_NOT_SUPPORTED code and that the source function
427
+ # is "RevalidateCachedQuery" since that is the only function
428
+ # in postgres that will create this error.
429
+ #
430
+ # That method will not work for CockroachDB because the error
431
+ # originates from the "runExecBuilder" function, so we need
432
+ # to modify the original to match the CockroachDB behavior.
433
+ def is_cached_plan_failure?(e)
434
+ pgerror = e.cause
435
+
436
+ pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE) == FEATURE_NOT_SUPPORTED &&
437
+ pgerror.result.result_error_field(PG::PG_DIAG_SOURCE_FUNCTION) == "runExecBuilder"
438
+ rescue
439
+ false
440
+ end
441
+
442
+ def arel_visitor
443
+ Arel::Visitors::CockroachDB.new(self)
444
+ end
445
+
401
446
  # end private
402
447
  end
403
448
  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.0beta3
4
+ version: 6.1.0beta1
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-01-20 00:00:00.000000000 Z
11
+ date: 2021-02-10 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
@@ -81,6 +81,7 @@ files:
81
81
  - lib/active_record/connection_adapters/cockroachdb/column_methods.rb
82
82
  - lib/active_record/connection_adapters/cockroachdb/database_statements.rb
83
83
  - lib/active_record/connection_adapters/cockroachdb/database_tasks.rb
84
+ - lib/active_record/connection_adapters/cockroachdb/oid/interval.rb
84
85
  - lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb
85
86
  - lib/active_record/connection_adapters/cockroachdb/quoting.rb
86
87
  - lib/active_record/connection_adapters/cockroachdb/referential_integrity.rb