activerecord-cockroachdb-adapter 7.1.1 → 7.2.1

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +6 -3
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +7 -1
  5. data/CONTRIBUTING.md +39 -90
  6. data/Gemfile +4 -4
  7. data/LICENSE +1 -2
  8. data/README.md +7 -5
  9. data/activerecord-cockroachdb-adapter.gemspec +2 -2
  10. data/bin/console +7 -5
  11. data/bin/console_schemas/default.rb +2 -0
  12. data/lib/active_record/connection_adapters/cockroachdb/arel_tosql.rb +14 -0
  13. data/lib/active_record/connection_adapters/cockroachdb/attribute_methods.rb +16 -0
  14. data/lib/active_record/connection_adapters/cockroachdb/column.rb +16 -0
  15. data/lib/active_record/connection_adapters/cockroachdb/column_methods.rb +14 -0
  16. data/lib/active_record/connection_adapters/cockroachdb/database_statements.rb +27 -3
  17. data/lib/active_record/connection_adapters/cockroachdb/database_tasks.rb +18 -2
  18. data/lib/active_record/connection_adapters/cockroachdb/oid/date_time.rb +14 -0
  19. data/lib/active_record/connection_adapters/cockroachdb/oid/interval.rb +16 -0
  20. data/lib/active_record/connection_adapters/cockroachdb/oid/spatial.rb +14 -0
  21. data/lib/active_record/connection_adapters/cockroachdb/quoting.rb +16 -0
  22. data/lib/active_record/connection_adapters/cockroachdb/referential_integrity.rb +99 -9
  23. data/lib/active_record/connection_adapters/cockroachdb/schema_creation.rb +14 -1
  24. data/lib/active_record/connection_adapters/cockroachdb/schema_dumper.rb +14 -1
  25. data/lib/active_record/connection_adapters/cockroachdb/schema_statements.rb +125 -25
  26. data/lib/active_record/connection_adapters/cockroachdb/setup.rb +14 -0
  27. data/lib/active_record/connection_adapters/cockroachdb/spatial_column_info.rb +16 -0
  28. data/lib/active_record/connection_adapters/cockroachdb/table_definition.rb +14 -0
  29. data/lib/active_record/connection_adapters/cockroachdb/transaction_manager.rb +35 -0
  30. data/lib/active_record/connection_adapters/cockroachdb/type.rb +16 -0
  31. data/lib/active_record/connection_adapters/cockroachdb_adapter.rb +56 -55
  32. data/lib/active_record/migration/cockroachdb/compatibility.rb +16 -0
  33. data/lib/active_record/relation/query_methods_ext.rb +14 -0
  34. data/lib/activerecord-cockroachdb-adapter.rb +21 -0
  35. data/lib/arel/nodes/join_source_ext.rb +16 -0
  36. data/lib/version.rb +15 -1
  37. metadata +7 -8
  38. data/.ruby-version +0 -1
@@ -1,5 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Copyright 2024 The Cockroach Authors.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
3
17
  # The PostgresSQL Adapter's ReferentialIntegrity module can disable and
4
18
  # re-enable foreign key constraints by disabling all table triggers. Since
5
19
  # triggers are not available in CockroachDB, we have to remove foreign keys and
@@ -20,11 +34,18 @@ module ActiveRecord
20
34
  end
21
35
 
22
36
  def disable_referential_integrity
23
- foreign_keys = tables.map { |table| foreign_keys(table) }.flatten
37
+ foreign_keys = all_foreign_keys
24
38
 
25
- foreign_keys.each do |foreign_key|
26
- remove_foreign_key(foreign_key.from_table, name: foreign_key.options[:name])
39
+ statements = foreign_keys.map do |foreign_key|
40
+ # We do not use the `#remove_foreign_key` method here because it
41
+ # checks for foreign keys existance in the schema cache. This method
42
+ # is performance critical and we know the foreign key exist.
43
+ at = create_alter_table foreign_key.from_table
44
+ at.drop_foreign_key foreign_key.name
45
+
46
+ schema_creation.accept(at)
27
47
  end
48
+ execute_batch(statements, "Disable referential integrity -> remove foreign keys")
28
49
 
29
50
  yield
30
51
 
@@ -38,19 +59,88 @@ module ActiveRecord
38
59
  ActiveRecord::Base.table_name_suffix = ""
39
60
 
40
61
  begin
41
- foreign_keys.each do |foreign_key|
42
- # Avoid having PG:DuplicateObject error if a test is ran in transaction.
43
- # TODO: verify that there is no cache issue related to running this (e.g: fk
44
- # still in cache but not in db)
45
- next if foreign_key_exists?(foreign_key.from_table, name: foreign_key.options[:name])
62
+ # Avoid having PG:DuplicateObject error if a test is ran in transaction.
63
+ # TODO: verify that there is no cache issue related to running this (e.g: fk
64
+ # still in cache but not in db)
65
+ #
66
+ # We avoid using `foreign_key_exists?` here because it checks the schema cache
67
+ # for every key. This method is performance critical for the test suite, hence
68
+ # we use the `#all_foreign_keys` method that only make one query to the database.
69
+ already_inserted_foreign_keys = all_foreign_keys
70
+ statements = foreign_keys.map do |foreign_key|
71
+ next if already_inserted_foreign_keys.any? { |fk| fk.from_table == foreign_key.from_table && fk.options[:name] == foreign_key.options[:name] }
72
+
73
+ options = foreign_key_options(foreign_key.from_table, foreign_key.to_table, foreign_key.options)
74
+ at = create_alter_table foreign_key.from_table
75
+ at.add_foreign_key foreign_key.to_table, options
46
76
 
47
- add_foreign_key(foreign_key.from_table, foreign_key.to_table, **foreign_key.options)
77
+ schema_creation.accept(at)
48
78
  end
79
+ execute_batch(statements.compact, "Disable referential integrity -> add foreign keys")
49
80
  ensure
50
81
  ActiveRecord::Base.table_name_prefix = old_prefix
51
82
  ActiveRecord::Base.table_name_suffix = old_suffix
52
83
  end
53
84
  end
85
+
86
+ private
87
+
88
+ # Copy/paste of the `#foreign_keys(table)` method adapted to return every single
89
+ # foreign key in the database.
90
+ def all_foreign_keys
91
+ fk_info = exec_query(<<~SQL, "SCHEMA")
92
+ SELECT CASE
93
+ WHEN n1.nspname = current_schema()
94
+ THEN ''
95
+ ELSE n1.nspname || '.'
96
+ END || t1.relname AS from_table,
97
+ CASE
98
+ WHEN n2.nspname = current_schema()
99
+ THEN ''
100
+ ELSE n2.nspname || '.'
101
+ END || t2.relname AS to_table,
102
+ a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid, c.condeferrable AS deferrable, c.condeferred AS deferred,
103
+ c.conkey, c.confkey, c.conrelid, c.confrelid
104
+ FROM pg_constraint c
105
+ JOIN pg_class t1 ON c.conrelid = t1.oid
106
+ JOIN pg_class t2 ON c.confrelid = t2.oid
107
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
108
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
109
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
110
+ JOIN pg_namespace n1 ON t1.relnamespace = n1.oid
111
+ JOIN pg_namespace n2 ON t2.relnamespace = n2.oid
112
+ WHERE c.contype = 'f'
113
+ ORDER BY c.conname
114
+ SQL
115
+
116
+ fk_info.map do |row|
117
+ from_table = PostgreSQL::Utils.unquote_identifier(row["from_table"])
118
+ to_table = PostgreSQL::Utils.unquote_identifier(row["to_table"])
119
+ conkey = row["conkey"].scan(/\d+/).map(&:to_i)
120
+ confkey = row["confkey"].scan(/\d+/).map(&:to_i)
121
+
122
+ if conkey.size > 1
123
+ column = column_names_from_column_numbers(row["conrelid"], conkey)
124
+ primary_key = column_names_from_column_numbers(row["confrelid"], confkey)
125
+ else
126
+ column = PostgreSQL::Utils.unquote_identifier(row["column"])
127
+ primary_key = row["primary_key"]
128
+ end
129
+
130
+ options = {
131
+ column: column,
132
+ name: row["name"],
133
+ primary_key: primary_key
134
+ }
135
+ options[:on_delete] = extract_foreign_key_action(row["on_delete"])
136
+ options[:on_update] = extract_foreign_key_action(row["on_update"])
137
+ options[:deferrable] = extract_constraint_deferrable(row["deferrable"], row["deferred"])
138
+
139
+ options[:validate] = row["valid"]
140
+
141
+ ForeignKeyDefinition.new(from_table, to_table, options)
142
+ end
143
+ end
54
144
  end
55
145
  end
56
146
  end
@@ -1,5 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Copyright 2024 The Cockroach Authors.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
3
17
  module ActiveRecord
4
18
  module ConnectionAdapters
5
19
  module CockroachDB
@@ -15,4 +29,3 @@ module ActiveRecord
15
29
  end
16
30
  end
17
31
  end
18
-
@@ -1,5 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Copyright 2024 The Cockroach Authors.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
3
17
  module ActiveRecord
4
18
  module ConnectionAdapters
5
19
  module CockroachDB
@@ -16,4 +30,3 @@ module ActiveRecord
16
30
  end
17
31
  end
18
32
  end
19
-
@@ -1,3 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 The Cockroach Authors.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
1
17
  module ActiveRecord
2
18
  module ConnectionAdapters
3
19
  module CockroachDB
@@ -39,6 +55,115 @@ module ActiveRecord
39
55
  end
40
56
  end
41
57
 
58
+ def primary_keys(table_name)
59
+ return super unless database_version >= 24_02_02
60
+
61
+ query_values(<<~SQL, "SCHEMA")
62
+ SELECT a.attname
63
+ FROM (
64
+ SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx
65
+ FROM pg_index
66
+ WHERE indrelid = #{quote(quote_table_name(table_name))}::regclass
67
+ AND indisprimary
68
+ ) i
69
+ JOIN pg_attribute a
70
+ ON a.attrelid = i.indrelid
71
+ AND a.attnum = i.indkey[i.idx]
72
+ AND NOT a.attishidden
73
+ ORDER BY i.idx
74
+ SQL
75
+ end
76
+
77
+ def column_names_from_column_numbers(table_oid, column_numbers)
78
+ return super unless database_version >= 24_02_02
79
+
80
+ Hash[query(<<~SQL, "SCHEMA")].values_at(*column_numbers).compact
81
+ SELECT a.attnum, a.attname
82
+ FROM pg_attribute a
83
+ WHERE a.attrelid = #{table_oid}
84
+ AND a.attnum IN (#{column_numbers.join(", ")})
85
+ AND NOT a.attishidden
86
+ SQL
87
+ end
88
+
89
+ # OVERRIDE: CockroachDB does not support deferrable constraints.
90
+ # See: https://go.crdb.dev/issue-v/31632/v23.1
91
+ def foreign_key_options(from_table, to_table, options)
92
+ options = super
93
+ options.delete(:deferrable) unless supports_deferrable_constraints?
94
+ options
95
+ end
96
+
97
+ # OVERRIDE: Added `unique_rowid` to the last line of the second query.
98
+ # This is a CockroachDB-specific function used for primary keys.
99
+ # Also make sure we don't consider `NOT VISIBLE` columns.
100
+ #
101
+ # Returns a table's primary key and belonging sequence.
102
+ def pk_and_sequence_for(table) # :nodoc:
103
+ # First try looking for a sequence with a dependency on the
104
+ # given table's primary key.
105
+ result = query(<<~SQL, "SCHEMA")[0]
106
+ SELECT attr.attname, nsp.nspname, seq.relname
107
+ FROM pg_class seq,
108
+ pg_attribute attr,
109
+ pg_depend dep,
110
+ pg_constraint cons,
111
+ pg_namespace nsp,
112
+ -- TODO: use the pg_catalog.pg_attribute(attishidden) column when
113
+ -- it is added instead of joining on crdb_internal.
114
+ -- See https://github.com/cockroachdb/cockroach/pull/126397
115
+ crdb_internal.table_columns tc
116
+ WHERE seq.oid = dep.objid
117
+ AND seq.relkind = 'S'
118
+ AND attr.attrelid = dep.refobjid
119
+ AND attr.attnum = dep.refobjsubid
120
+ AND attr.attrelid = cons.conrelid
121
+ AND attr.attnum = cons.conkey[1]
122
+ AND seq.relnamespace = nsp.oid
123
+ AND attr.attrelid = tc.descriptor_id
124
+ AND attr.attname = tc.column_name
125
+ AND tc.hidden = false
126
+ AND cons.contype = 'p'
127
+ AND dep.classid = 'pg_class'::regclass
128
+ AND dep.refobjid = #{quote(quote_table_name(table))}::regclass
129
+ SQL
130
+
131
+ if result.nil? || result.empty?
132
+ result = query(<<~SQL, "SCHEMA")[0]
133
+ SELECT attr.attname, nsp.nspname,
134
+ CASE
135
+ WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
136
+ WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
137
+ substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
138
+ strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
139
+ ELSE split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2)
140
+ END
141
+ FROM pg_class t
142
+ JOIN pg_attribute attr ON (t.oid = attrelid)
143
+ JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
144
+ JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
145
+ JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid)
146
+ -- TODO: use the pg_catalog.pg_attribute(attishidden) column when
147
+ -- it is added instead of joining on crdb_internal.
148
+ -- See https://github.com/cockroachdb/cockroach/pull/126397
149
+ JOIN crdb_internal.table_columns tc ON (attr.attrelid = tc.descriptor_id AND attr.attname = tc.column_name)
150
+ WHERE t.oid = #{quote(quote_table_name(table))}::regclass
151
+ AND tc.hidden = false
152
+ AND cons.contype = 'p'
153
+ AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate|gen_random_uuid|unique_rowid'
154
+ SQL
155
+ end
156
+
157
+ pk = result.shift
158
+ if result.last
159
+ [pk, PostgreSQL::Name.new(*result)]
160
+ else
161
+ [pk, nil]
162
+ end
163
+ rescue
164
+ nil
165
+ end
166
+
42
167
  # override
43
168
  # Modified version of the postgresql foreign_keys method.
44
169
  # Replaces t2.oid::regclass::text with t2.relname since this is
@@ -171,31 +296,6 @@ module ActiveRecord
171
296
  sql
172
297
  end
173
298
 
174
- # This overrides the method from PostegreSQL adapter
175
- # Resets the sequence of a table's primary key to the maximum value.
176
- def reset_pk_sequence!(table, pk = nil, sequence = nil)
177
- unless pk && sequence
178
- default_pk, default_sequence = pk_and_sequence_for(table)
179
-
180
- pk ||= default_pk
181
- sequence ||= default_sequence
182
- end
183
-
184
- if @logger && pk && !sequence
185
- @logger.warn "#{table} has primary key #{pk} with no default sequence."
186
- end
187
-
188
- if pk && sequence
189
- quoted_sequence = quote_table_name(sequence)
190
- max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
191
- if max_pk.nil?
192
- minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
193
- end
194
-
195
- query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false})", "SCHEMA")
196
- end
197
- end
198
-
199
299
  # override
200
300
  def native_database_types
201
301
  # Add spatial types
@@ -1,5 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Copyright 2024 The Cockroach Authors.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
3
17
  module ActiveRecord # :nodoc:
4
18
  module ConnectionAdapters # :nodoc:
5
19
  module CockroachDB # :nodoc:
@@ -1,3 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 The Cockroach Authors.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
1
17
  module ActiveRecord
2
18
  module ConnectionAdapters
3
19
  module CockroachDB
@@ -1,5 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Copyright 2024 The Cockroach Authors.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
3
17
  module ActiveRecord # :nodoc:
4
18
  module ConnectionAdapters # :nodoc:
5
19
  module CockroachDB # :nodoc:
@@ -1,5 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Copyright 2024 The Cockroach Authors.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
3
17
  module ActiveRecord
4
18
  module ConnectionAdapters
5
19
  module CockroachDB
@@ -31,6 +45,27 @@ module ActiveRecord
31
45
  within_new_transaction(isolation: isolation, joinable: joinable, attempts: attempts + 1) { yield }
32
46
  end
33
47
 
48
+ # OVERRIDE: the `rescue ActiveRecord::StatementInvalid` block is new, see comment.
49
+ def rollback_transaction(transaction = nil)
50
+ @connection.lock.synchronize do
51
+ transaction ||= @stack.last
52
+ begin
53
+ transaction.rollback
54
+ rescue ActiveRecord::StatementInvalid => err
55
+ # This is important to make Active Record aware the record was not inserted/saved
56
+ # Otherwise Active Record will assume save was successful and it doesn't retry the transaction
57
+ # See this thread for more details:
58
+ # https://github.com/cockroachdb/activerecord-cockroachdb-adapter/issues/258#issuecomment-2256633329
59
+ transaction.rollback_records if err.cause.is_a?(PG::NoActiveSqlTransaction)
60
+
61
+ raise
62
+ ensure
63
+ @stack.pop if @stack.last == transaction
64
+ end
65
+ transaction.rollback_records
66
+ end
67
+ end
68
+
34
69
  def retryable?(error)
35
70
  return true if serialization_error?(error)
36
71
  return true if error.is_a? ActiveRecord::SerializationFailure
@@ -1,3 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2024 The Cockroach Authors.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
1
17
  module ActiveRecord
2
18
  module Type
3
19
  module CRDBExt
@@ -1,5 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Copyright 2024 The Cockroach Authors.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
3
17
  require "rgeo/active_record"
4
18
 
5
19
  require_relative "../../arel/nodes/join_source_ext"
@@ -31,20 +45,6 @@ require_relative "../relation/query_methods_ext"
31
45
  # Defined in ./setup.rb
32
46
  ActiveRecord::ConnectionAdapters::CockroachDB.initial_setup
33
47
 
34
- module ActiveRecord
35
- # TODO: once in rails 7.2, remove this and replace with a `#register` call.
36
- # See: https://github.com/rails/rails/commit/22a26d7f74ea8f0d5f7c4169531ae38441cfd5e5#diff-2468c670eb10c24bd2823e42708489a336d6f21c6efc7e3c4a574166fa77bb22
37
- module ConnectionHandling
38
- def cockroachdb_adapter_class
39
- ConnectionAdapters::CockroachDBAdapter
40
- end
41
-
42
- def cockroachdb_connection(config)
43
- cockroachdb_adapter_class.new(config)
44
- end
45
- end
46
- end
47
-
48
48
  module ActiveRecord
49
49
  module ConnectionAdapters
50
50
  module CockroachDBConnectionPool
@@ -137,15 +137,23 @@ module ActiveRecord
137
137
  end
138
138
 
139
139
  def get_database_version
140
- major, minor, patch = query_value("SHOW crdb_version").match(/v(\d+).(\d+).(\d+)/)[1..].map(&:to_i)
141
- major * 100 * 100 + minor * 100 + patch
140
+ with_raw_connection do |conn|
141
+ conn.async_exec("SHOW crdb_version") do |result|
142
+ major, minor, patch = result
143
+ .getvalue(0, 0)
144
+ .match(/v(\d+).(\d+).(\d+)/)
145
+ .captures
146
+ .map(&:to_i)
147
+ major * 100 * 100 + minor * 100 + patch
148
+ end
149
+ end
142
150
  end
143
151
  undef :postgresql_version
144
152
  alias :cockroachdb_version :database_version
145
153
 
146
154
  def supports_datetime_with_precision?
147
155
  # https://github.com/cockroachdb/cockroach/pull/111400
148
- database_version >= 23_01_13
156
+ true
149
157
  end
150
158
 
151
159
  def supports_nulls_not_distinct?
@@ -162,7 +170,7 @@ module ActiveRecord
162
170
  end
163
171
 
164
172
  def supports_materialized_views?
165
- false
173
+ true
166
174
  end
167
175
 
168
176
  def supports_index_include?
@@ -201,50 +209,44 @@ module ActiveRecord
201
209
  end
202
210
 
203
211
  def supports_deferrable_constraints?
212
+ # https://go.crdb.dev/issue-v/31632/v23.1
204
213
  false
205
214
  end
206
215
 
207
- # NOTE: This commented bit of code allows to have access to crdb version,
208
- # which can be useful for feature detection. However, we currently don't
209
- # need, hence we avoid the extra queries.
210
- #
211
- # def initialize(connection, logger, conn_params, config)
212
- # super(connection, logger, conn_params, config)
213
-
214
- # # crdb_version is the version of the binary running on the node. We
215
- # # really want to use `SHOW CLUSTER SETTING version` to get the cluster
216
- # # version, but that is only available to admins. Instead, we can use
217
- # # crdb_internal.is_at_least_version, but that's only available in 22.1.
218
- # crdb_version_string = query_value("SHOW crdb_version")
219
- # if crdb_version_string.include? "v22.1"
220
- # version_num = query_value(<<~SQL, "VERSION")
221
- # SELECT
222
- # CASE
223
- # WHEN crdb_internal.is_at_least_version('22.2') THEN 2220
224
- # WHEN crdb_internal.is_at_least_version('22.1') THEN 2210
225
- # ELSE 2120
226
- # END;
227
- # SQL
228
- # end
229
- # @crdb_version = version_num.to_i
230
- # end
231
-
232
- def self.database_exists?(config)
233
- !!ActiveRecord::Base.cockroachdb_connection(config)
234
- rescue ActiveRecord::NoDatabaseError
235
- false
216
+ def check_version # :nodoc:
217
+ # https://www.cockroachlabs.com/docs/releases/release-support-policy
218
+ if database_version < 23_01_12 # < 23.1.12
219
+ raise "Your version of CockroachDB (#{database_version}) is too old. Active Record supports CockroachDB >= 23.1.12."
220
+ end
236
221
  end
237
222
 
238
- def initialize(...)
223
+ def configure_connection(...)
239
224
  super
240
225
 
241
226
  # This rescue flow appears in new_client, but it is needed here as well
242
227
  # since Cockroach will sometimes not raise until a query is made.
228
+ #
229
+ # See https://github.com/cockroachdb/activerecord-cockroachdb-adapter/pull/337#issuecomment-2328419453
230
+ #
231
+ # The error conditions used to differ from the ones in new_client, but
232
+ # the reasons why are no longer relevant. We keep this in sync with new_client
233
+ # even though some conditions might never be checked.
234
+ #
235
+ # See https://github.com/cockroachdb/activerecord-cockroachdb-adapter/pull/229
236
+ #
237
+ # We have to rescue `ActiveRecord::StatementInvalid` instead of `::PG::Error`
238
+ # here as the error has already been casted (in `#with_raw_connection` as
239
+ # of Rails 7.2.1).
243
240
  rescue ActiveRecord::StatementInvalid => error
244
- no_db_err_check1 = @connection_parameters && @connection_parameters[:dbname] && error.cause.message.include?(@connection_parameters[:dbname])
245
- no_db_err_check2 = @connection_parameters && @connection_parameters[:dbname] && error.cause.message.include?("pg_type")
246
- if no_db_err_check1 || no_db_err_check2
247
- raise ActiveRecord::NoDatabaseError
241
+ conn_params = @connection_parameters
242
+ if conn_params && conn_params[:dbname] == "postgres"
243
+ raise ActiveRecord::ConnectionNotEstablished, error.message
244
+ elsif conn_params && conn_params[:dbname] && error.cause.message.include?(conn_params[:dbname])
245
+ raise ActiveRecord::NoDatabaseError.db_error(conn_params[:dbname])
246
+ elsif conn_params && conn_params[:user] && error.cause.message.include?(conn_params[:user])
247
+ raise ActiveRecord::DatabaseConnectionError.username_error(conn_params[:user])
248
+ elsif conn_params && conn_params[:host] && error.cause.message.include?(conn_params[:host])
249
+ raise ActiveRecord::DatabaseConnectionError.hostname_error(conn_params[:host])
248
250
  else
249
251
  raise ActiveRecord::ConnectionNotEstablished, error.message
250
252
  end
@@ -292,7 +294,6 @@ module ActiveRecord
292
294
  precision = extract_precision(sql_type)
293
295
  scale = extract_scale(sql_type)
294
296
 
295
-
296
297
  # The type for the numeric depends on the width of the field,
297
298
  # so we'll do something special here.
298
299
  #
@@ -511,7 +512,7 @@ module ActiveRecord
511
512
  # so this modified query uses AS OF SYSTEM TIME '-10s' to read historical data.
512
513
  def add_pg_decoders
513
514
  if @config[:use_follower_reads_for_type_introspection]
514
- @default_timezone = nil
515
+ @mapped_default_timezone = nil
515
516
  @timestamp_decoder = nil
516
517
 
517
518
  coders_by_name = {
@@ -526,6 +527,7 @@ module ActiveRecord
526
527
  "timestamp" => PG::TextDecoder::TimestampUtc,
527
528
  "timestamptz" => PG::TextDecoder::TimestampWithTimeZone,
528
529
  }
530
+ coders_by_name["date"] = PG::TextDecoder::Date if decode_dates
529
531
 
530
532
  known_coder_types = coders_by_name.keys.map { |n| quote(n) }
531
533
  query = <<~SQL % known_coder_types.join(", ")
@@ -533,7 +535,6 @@ module ActiveRecord
533
535
  FROM pg_type as t AS OF SYSTEM TIME '-10s'
534
536
  WHERE t.typname IN (%s)
535
537
  SQL
536
-
537
538
  coders = execute_and_clear(query, "SCHEMA", [], allow_retry: true, materialize_transactions: false) do |result|
538
539
  result.filter_map { |row| construct_coder(row, coders_by_name[row["typname"]]) }
539
540
  end