activerecord-cockroachdb-adapter 7.0.2 → 7.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.
@@ -1,5 +1,6 @@
1
1
  require "rgeo/active_record"
2
2
 
3
+ require_relative "../../arel/nodes/join_source_ext"
3
4
  require "active_record/connection_adapters/postgresql_adapter"
4
5
  require "active_record/connection_adapters/cockroachdb/attribute_methods"
5
6
  require "active_record/connection_adapters/cockroachdb/column_methods"
@@ -23,6 +24,8 @@ require "active_record/connection_adapters/cockroachdb/arel_tosql"
23
24
  require_relative "../migration/cockroachdb/compatibility"
24
25
  require_relative "../../version"
25
26
 
27
+ require_relative "../relation/query_methods_ext"
28
+
26
29
  # Run to ignore spatial tables that will break schemna dumper.
27
30
  # Defined in ./setup.rb
28
31
  ActiveRecord::ConnectionAdapters::CockroachDB.initial_setup
@@ -158,7 +161,7 @@ module ActiveRecord
158
161
  end
159
162
 
160
163
  def supports_bulk_alter?
161
- false
164
+ true
162
165
  end
163
166
 
164
167
  def supports_json?
@@ -179,7 +182,15 @@ module ActiveRecord
179
182
  end
180
183
 
181
184
  def supports_partial_index?
182
- @crdb_version >= 2020
185
+ true
186
+ end
187
+
188
+ def supports_index_include?
189
+ false
190
+ end
191
+
192
+ def supports_exclusion_constraints?
193
+ false
183
194
  end
184
195
 
185
196
  def supports_expression_index?
@@ -194,7 +205,7 @@ module ActiveRecord
194
205
  end
195
206
 
196
207
  def supports_comments?
197
- @crdb_version >= 2010
208
+ true
198
209
  end
199
210
 
200
211
  def supports_comments_in_create?
@@ -206,11 +217,11 @@ module ActiveRecord
206
217
  end
207
218
 
208
219
  def supports_virtual_columns?
209
- @crdb_version >= 2110
220
+ true
210
221
  end
211
222
 
212
223
  def supports_string_to_array_coercion?
213
- @crdb_version >= 2020
224
+ true
214
225
  end
215
226
 
216
227
  def supports_partitioned_indexes?
@@ -236,62 +247,30 @@ module ActiveRecord
236
247
  alias index_name_length max_identifier_length
237
248
  alias table_alias_length max_identifier_length
238
249
 
239
- def initialize(connection, logger, conn_params, config)
240
- super(connection, logger, conn_params, config)
241
-
242
- # crdb_version is the version of the binary running on the node. We
243
- # really want to use `SHOW CLUSTER SETTING version` to get the cluster
244
- # version, but that is only available to admins. Instead, we can use
245
- # crdb_internal.is_at_least_version, but that's only available in 22.1.
246
- crdb_version_string = query_value("SHOW crdb_version")
247
- if crdb_version_string.include? "v22.1"
248
- version_num = query_value(<<~SQL, "VERSION")
249
- SELECT
250
- CASE
251
- WHEN crdb_internal.is_at_least_version('22.2') THEN 2220
252
- WHEN crdb_internal.is_at_least_version('22.1') THEN 2210
253
- ELSE 2120
254
- END;
255
- SQL
256
- else
257
- # This branch can be removed once the dialect stops supporting v21.2
258
- # and earlier.
259
- if crdb_version_string.include? "v1."
260
- version_num = 1
261
- elsif crdb_version_string.include? "v2."
262
- version_num 2
263
- elsif crdb_version_string.include? "v19.1."
264
- version_num = 1910
265
- elsif crdb_version_string.include? "v19.2."
266
- version_num = 1920
267
- elsif crdb_version_string.include? "v20.1."
268
- version_num = 2010
269
- elsif crdb_version_string.include? "v20.2."
270
- version_num = 2020
271
- elsif crdb_version_string.include? "v21.1."
272
- version_num = 2110
273
- else
274
- version_num = 2120
275
- end
276
- end
277
- @crdb_version = version_num.to_i
278
-
279
- # NOTE: this is normally in configure_connection, but that is run
280
- # before crdb_version is determined. Once all supported versions
281
- # of CockroachDB support SET intervalstyle it can safely be moved
282
- # back.
283
- # Set interval output format to ISO 8601 for ease of parsing by ActiveSupport::Duration.parse
284
- if @crdb_version >= 2120
285
- begin
286
- execute("SET intervalstyle_enabled = true", "SCHEMA")
287
- execute("SET intervalstyle = iso_8601", "SCHEMA")
288
- rescue
289
- # Ignore any error. This can happen with a cluster that has
290
- # not yet finalized the v21.2 upgrade. v21.2 does not have
291
- # a way to tell if the upgrade was finalized (see comment above).
292
- end
293
- end
294
- end
250
+ # NOTE: This commented bit of code allows to have access to crdb version,
251
+ # which can be useful for feature detection. However, we currently don't
252
+ # need, hence we avoid the extra queries.
253
+ #
254
+ # def initialize(connection, logger, conn_params, config)
255
+ # super(connection, logger, conn_params, config)
256
+
257
+ # # crdb_version is the version of the binary running on the node. We
258
+ # # really want to use `SHOW CLUSTER SETTING version` to get the cluster
259
+ # # version, but that is only available to admins. Instead, we can use
260
+ # # crdb_internal.is_at_least_version, but that's only available in 22.1.
261
+ # crdb_version_string = query_value("SHOW crdb_version")
262
+ # if crdb_version_string.include? "v22.1"
263
+ # version_num = query_value(<<~SQL, "VERSION")
264
+ # SELECT
265
+ # CASE
266
+ # WHEN crdb_internal.is_at_least_version('22.2') THEN 2220
267
+ # WHEN crdb_internal.is_at_least_version('22.1') THEN 2210
268
+ # ELSE 2120
269
+ # END;
270
+ # SQL
271
+ # end
272
+ # @crdb_version = version_num.to_i
273
+ # end
295
274
 
296
275
  def self.database_exists?(config)
297
276
  !!ActiveRecord::Base.cockroachdb_connection(config)
@@ -304,12 +283,12 @@ module ActiveRecord
304
283
  # (DO $$) that CockroachDB does not support.
305
284
  #
306
285
  # Given a name and an array of values, creates an enum type.
307
- def create_enum(name, values)
308
- sql_values = values.map { |s| "'#{s}'" }.join(", ")
286
+ def create_enum(name, values, **options)
287
+ sql_values = values.map { |s| quote(s) }.join(", ")
309
288
  query = <<~SQL
310
- CREATE TYPE IF NOT EXISTS \"#{name}\" AS ENUM (#{sql_values});
289
+ CREATE TYPE IF NOT EXISTS #{quote_table_name(name)} AS ENUM (#{sql_values});
311
290
  SQL
312
- exec_query(query)
291
+ internal_exec_query(query).tap { reload_type_map }
313
292
  end
314
293
 
315
294
  class << self
@@ -367,56 +346,6 @@ module ActiveRecord
367
346
 
368
347
  private
369
348
 
370
- # Configures the encoding, verbosity, schema search path, and time zone of the connection.
371
- # This is called by #connect and should not be called manually.
372
- #
373
- # NOTE(joey): This was cradled from postgresql_adapter.rb. This
374
- # was due to needing to override configuration statements.
375
- def configure_connection
376
- if @config[:encoding]
377
- @connection.set_client_encoding(@config[:encoding])
378
- end
379
- self.client_min_messages = @config[:min_messages] || "warning"
380
- self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
381
-
382
- # Use standard-conforming strings so we don't have to do the E'...' dance.
383
- set_standard_conforming_strings
384
-
385
- variables = @config.fetch(:variables, {}).stringify_keys
386
-
387
- # If using Active Record's time zone support configure the connection to return
388
- # TIMESTAMP WITH ZONE types in UTC.
389
- unless variables["timezone"]
390
- if ActiveRecord.default_timezone == :utc
391
- variables["timezone"] = "UTC"
392
- elsif @local_tz
393
- variables["timezone"] = @local_tz
394
- end
395
- end
396
-
397
- # NOTE(joey): This is a workaround as CockroachDB 1.1.x
398
- # supports SET TIME ZONE <...> and SET "time zone" = <...> but
399
- # not SET timezone = <...>.
400
- if variables.key?("timezone")
401
- tz = variables.delete("timezone")
402
- execute("SET TIME ZONE #{quote(tz)}", "SCHEMA")
403
- end
404
-
405
- # SET statements from :variables config hash
406
- # https://www.postgresql.org/docs/current/static/sql-set.html
407
- variables.map do |k, v|
408
- if v == ":default" || v == :default
409
- # Sets the value to the global or compile default
410
-
411
- # NOTE(joey): I am not sure if simply commenting this out
412
- # is technically correct.
413
- # execute("SET #{k} = DEFAULT", "SCHEMA")
414
- elsif !v.nil?
415
- execute("SET SESSION #{k} = #{quote(v)}", "SCHEMA")
416
- end
417
- end
418
- end
419
-
420
349
  # Override extract_value_from_default because the upstream definition
421
350
  # doesn't handle the variations in CockroachDB's behavior.
422
351
  def extract_value_from_default(default)
@@ -500,7 +429,8 @@ module ActiveRecord
500
429
  SELECT a.attname, format_type(a.atttypid, a.atttypmod),
501
430
  pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
502
431
  c.collname, NULL AS comment,
503
- #{supports_virtual_columns? ? 'attgenerated' : quote('')} as attgenerated,
432
+ attidentity,
433
+ attgenerated,
504
434
  NULL as is_hidden
505
435
  FROM pg_attribute a
506
436
  LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
@@ -515,26 +445,31 @@ module ActiveRecord
515
445
 
516
446
  # Use regex comparison because if a type is an array it will
517
447
  # have [] appended to the end of it.
518
- target_types = [
519
- /geometry/,
520
- /geography/,
521
- /interval/,
522
- /numeric/
523
- ]
524
-
525
- re = Regexp.union(target_types)
448
+ re = /\A(?:geometry|geography|interval|numeric)/
449
+
450
+ # 0: attname
451
+ # 1: type
452
+ # 2: default
453
+ # 3: attnotnull
454
+ # 4: atttypid
455
+ # 5: atttypmod
456
+ # 6: collname
457
+ # 7: comment
458
+ # 8: attidentity
459
+ # 9: attgenerated
460
+ # 10: is_hidden
526
461
  fields.map do |field|
527
462
  dtype = field[1]
528
463
  field[1] = crdb_fields[field[0]][2].downcase if re.match(dtype)
529
464
  field[7] = crdb_fields[field[0]][1]&.gsub!(/^\'|\'?$/, '')
530
- field[9] = true if crdb_fields[field[0]][3]
465
+ field[10] = true if crdb_fields[field[0]][3]
531
466
  field
532
467
  end
533
468
  fields.delete_if do |field|
534
469
  # Don't include rowid column if it is hidden and the primary key
535
470
  # is not defined (meaning CRDB implicitly created it).
536
471
  if field[0] == CockroachDBAdapter::DEFAULT_PRIMARY_KEY
537
- field[9] && !primary_key(table_name)
472
+ field[10] && !primary_key(table_name)
538
473
  else
539
474
  false # Keep this entry.
540
475
  end
@@ -547,11 +482,14 @@ module ActiveRecord
547
482
  # precision, and scale information in the type.
548
483
  # Ex. geometry -> geometry(point, 4326)
549
484
  def crdb_column_definitions(table_name)
485
+ table_name = PostgreSQL::Utils.extract_schema_qualified_name(table_name)
486
+ table = table_name.identifier
487
+ with_schema = " AND c.table_schema = #{quote(table_name.schema)}" if table_name.schema
550
488
  fields = \
551
489
  query(<<~SQL, "SCHEMA")
552
490
  SELECT c.column_name, c.column_comment, c.crdb_sql_type, c.is_hidden::BOOLEAN
553
- FROM information_schema.columns c
554
- WHERE c.table_name = #{quote(table_name)}
491
+ FROM information_schema.columns c
492
+ WHERE c.table_name = #{quote(table)}#{with_schema}
555
493
  SQL
556
494
 
557
495
  fields.reduce({}) do |a, e|
@@ -592,21 +530,10 @@ module ActiveRecord
592
530
  def load_additional_types(oids = nil)
593
531
  if @config[:use_follower_reads_for_type_introspection]
594
532
  initializer = OID::TypeMapInitializer.new(type_map)
595
-
596
- query = <<~SQL
597
- SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
598
- FROM pg_type as t
599
- LEFT JOIN pg_range as r ON oid = rngtypid AS OF SYSTEM TIME '-10s'
600
- SQL
601
-
602
- if oids
603
- query += "WHERE t.oid IN (%s)" % oids.join(", ")
604
- else
605
- query += initializer.query_conditions_for_initial_load
606
- end
607
-
608
- execute_and_clear(query, "SCHEMA", []) do |records|
609
- initializer.run(records)
533
+ load_types_queries_with_aost(initializer, oids) do |query|
534
+ execute_and_clear(query, "SCHEMA", [], allow_retry: true, materialize_transactions: false) do |records|
535
+ initializer.run(records)
536
+ end
610
537
  end
611
538
  else
612
539
  super
@@ -617,6 +544,21 @@ module ActiveRecord
617
544
  super
618
545
  end
619
546
 
547
+ def load_types_queries_with_aost(initializer, oids)
548
+ query = <<~SQL
549
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
550
+ FROM pg_type as t
551
+ LEFT JOIN pg_range as r ON oid = rngtypid AS OF SYSTEM TIME '-10s'
552
+ SQL
553
+ if oids
554
+ yield query + "WHERE t.oid IN (%s)" % oids.join(", ")
555
+ else
556
+ yield query + initializer.query_conditions_for_known_type_names
557
+ yield query + initializer.query_conditions_for_known_type_types
558
+ yield query + initializer.query_conditions_for_array_types
559
+ end
560
+ end
561
+
620
562
  # override
621
563
  # This method maps data types to their proper decoder.
622
564
  #
@@ -647,15 +589,13 @@ module ActiveRecord
647
589
  WHERE t.typname IN (%s)
648
590
  SQL
649
591
 
650
- coders = execute_and_clear(query, "SCHEMA", []) do |result|
651
- result
652
- .map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
653
- .compact
592
+ coders = execute_and_clear(query, "SCHEMA", [], allow_retry: true, materialize_transactions: false) do |result|
593
+ result.filter_map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
654
594
  end
655
595
 
656
596
  map = PG::TypeMapByOid.new
657
597
  coders.each { |coder| map.add_coder(coder) }
658
- @connection.type_map_for_results = map
598
+ @raw_connection.type_map_for_results = map
659
599
 
660
600
  @type_map_for_results = PG::TypeMapByOid.new
661
601
  @type_map_for_results.default_type_map = map
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ class Relation
5
+ module QueryMethodsExt
6
+ def aost!(time) # :nodoc:
7
+ unless time.nil? || time.is_a?(Time)
8
+ raise ArgumentError, "Unsupported argument type: #{time} (#{time.class})"
9
+ end
10
+
11
+ @aost = time
12
+ self
13
+ end
14
+
15
+ # Set system time for the current query. Using
16
+ # `.aost(nil)` resets.
17
+ #
18
+ # See cockroachlabs.com/docs/stable/as-of-system-time
19
+ def aost(time)
20
+ spawn.aost!(time)
21
+ end
22
+
23
+ def from!(...) # :nodoc:
24
+ @force_index = nil
25
+ @index_hint = nil
26
+ super
27
+ end
28
+
29
+ # Set table index hint for the query to the
30
+ # given `index_name`, and `direction` (either
31
+ # `ASC` or `DESC`).
32
+ #
33
+ # Any call to `ActiveRecord::QueryMethods#from`
34
+ # will reset the index hint. Index hints are
35
+ # not set if the `from` clause is not a table
36
+ # name.
37
+ #
38
+ # @see https://www.cockroachlabs.com/docs/v22.2/table-expressions#force-index-selection
39
+ def force_index(index_name, direction: nil)
40
+ spawn.force_index!(index_name, direction: direction)
41
+ end
42
+
43
+ def force_index!(index_name, direction: nil)
44
+ return self unless from_clause_is_a_table_name?
45
+
46
+ index_name = sanitize_sql(index_name.to_s)
47
+ direction = direction.to_s.upcase
48
+ direction = %w[ASC DESC].include?(direction) ? ",#{direction}" : ""
49
+
50
+ @force_index = "FORCE_INDEX=#{index_name}#{direction}"
51
+ self.from_clause = build_from_clause_with_hints
52
+ self
53
+ end
54
+
55
+ # Set table index hint for the query with the
56
+ # given `hint`. This allows more control over
57
+ # the hint than `ActiveRecord::Relation#force_index`.
58
+ # For instance, you could set it to `NO_FULL_SCAN`.
59
+ #
60
+ # Any call to `ActiveRecord::QueryMethods#from`
61
+ # will reset the index hint. Index hints are
62
+ # not set if the `from` clause is not a table
63
+ # name.
64
+ #
65
+ # @see https://www.cockroachlabs.com/docs/v22.2/table-expressions#force-index-selection
66
+ def index_hint(hint)
67
+ spawn.index_hint!(hint)
68
+ end
69
+
70
+ def index_hint!(hint)
71
+ return self unless from_clause_is_a_table_name?
72
+
73
+ hint = sanitize_sql(hint.to_s)
74
+ @index_hint = hint.to_s
75
+ self.from_clause = build_from_clause_with_hints
76
+ self
77
+ end
78
+
79
+ # TODO: reset or no reset?
80
+
81
+ def show_create
82
+ connection.execute("show create table #{connection.quote_table_name self.table_name}").first["create_statement"]
83
+ end
84
+
85
+ private
86
+
87
+ def build_arel(...)
88
+ arel = super
89
+ arel.aost(@aost) if @aost.present?
90
+ arel
91
+ end
92
+
93
+ def from_clause_is_a_table_name?
94
+ # if empty, we are just dealing with the current table.
95
+ return true if from_clause.empty?
96
+ # `from_clause` can be a subquery.
97
+ return false unless from_clause.value.is_a?(String)
98
+ # `from_clause` can be a list of tables or a function.
99
+ # A simple way to check is to see if the string
100
+ # contains special characters. But we have to
101
+ # not check against an existing table hint.
102
+ return !from_clause.value.gsub(/\@{.*?\}/, "").match?(/[,\(]/)
103
+ end
104
+
105
+ def build_from_clause_with_hints
106
+ table_hints = [@index_hint, @force_index].compact.join(",")
107
+
108
+ table_name =
109
+ if from_clause.empty?
110
+ quoted_table_name
111
+ else
112
+ # Remove previous table hints if any. And spaces.
113
+ from_clause.value.partition("@").first.strip
114
+ end
115
+ Relation::FromClause.new("#{table_name}@{#{table_hints}}", nil)
116
+ end
117
+ end
118
+
119
+ QueryMethods.prepend(QueryMethodsExt)
120
+ end
121
+ # `ActiveRecord::Base` ancestors do not include `QueryMethods`.
122
+ # But the `#all` method returns a relation, which has `QueryMethods`
123
+ # as ancestor. That is how active_record is doing is as well.
124
+ #
125
+ # @see https://github.com/rails/rails/blob/914130a9f/activerecord/lib/active_record/querying.rb#L23
126
+ Querying.delegate(:force_index, :index_hint, :aost, :show_create, to: :all)
127
+ end
@@ -1,4 +1,4 @@
1
- if defined?(Rails)
1
+ if defined?(Rails::Railtie)
2
2
  module ActiveRecord
3
3
  module ConnectionAdapters
4
4
  class CockroachDBRailtie < ::Rails::Railtie
@@ -0,0 +1,28 @@
1
+ module Arel
2
+ module Nodes
3
+ module JoinSourceExt
4
+ def initialize(...)
5
+ super
6
+ @aost = nil
7
+ end
8
+
9
+ def hash
10
+ [*super, aost].hash
11
+ end
12
+
13
+ def eql?(other)
14
+ super && aost == other.aost
15
+ end
16
+ alias_method :==, :eql?
17
+ end
18
+ JoinSource.attr_accessor :aost
19
+ JoinSource.prepend JoinSourceExt
20
+ end
21
+ module SelectManagerExt
22
+ def aost(time)
23
+ @ctx.source.aost = time
24
+ nil
25
+ end
26
+ end
27
+ SelectManager.prepend SelectManagerExt
28
+ end
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecord
4
- COCKROACH_DB_ADAPTER_VERSION = "7.0.2"
4
+ COCKROACH_DB_ADAPTER_VERSION = "7.1.0"
5
5
  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: 7.0.2
4
+ version: 7.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: 2023-05-30 00:00:00.000000000 Z
11
+ date: 2024-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 7.0.3
19
+ version: 7.1.0
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: 7.0.3
26
+ version: 7.1.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: pg
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -74,6 +74,8 @@ executables: []
74
74
  extensions: []
75
75
  extra_rdoc_files: []
76
76
  files:
77
+ - ".editorconfig"
78
+ - ".github/workflows/ci.yml"
77
79
  - ".github/workflows/docker.yml"
78
80
  - ".gitignore"
79
81
  - ".gitmodules"
@@ -86,7 +88,10 @@ files:
86
88
  - Rakefile
87
89
  - activerecord-cockroachdb-adapter.gemspec
88
90
  - bin/console
91
+ - bin/console_schemas/default.rb
92
+ - bin/console_schemas/schemas.rb
89
93
  - bin/setup
94
+ - bin/start-cockroachdb
90
95
  - build/Dockerfile
91
96
  - build/config.teamcity.yml
92
97
  - build/local-test.sh
@@ -114,7 +119,9 @@ files:
114
119
  - lib/active_record/connection_adapters/cockroachdb/type.rb
115
120
  - lib/active_record/connection_adapters/cockroachdb_adapter.rb
116
121
  - lib/active_record/migration/cockroachdb/compatibility.rb
122
+ - lib/active_record/relation/query_methods_ext.rb
117
123
  - lib/activerecord-cockroachdb-adapter.rb
124
+ - lib/arel/nodes/join_source_ext.rb
118
125
  - lib/version.rb
119
126
  homepage: https://github.com/cockroachdb/activerecord-cockroachdb-adapter
120
127
  licenses:
@@ -136,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
143
  - !ruby/object:Gem::Version
137
144
  version: '0'
138
145
  requirements: []
139
- rubygems_version: 3.4.9
146
+ rubygems_version: 3.4.10
140
147
  signing_key:
141
148
  specification_version: 4
142
149
  summary: CockroachDB adapter for ActiveRecord.