activerecord-cockroachdb-adapter 7.2.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 496e736f6d7baf4f0acb18461a70d7686765262e3fd09f4a65ac49534582d871
4
- data.tar.gz: 9bd072d1ac20b7f1099076fb5019c9cfbee305da4463de7f91ec493c8f6eebe3
3
+ metadata.gz: c28f723090995949b4ca82645717432e09a8d71d9c5e714bea958432af87b0cc
4
+ data.tar.gz: 4855cb4a35acad52324f86833777b9b9f0f74d5a0fbade33fad8f0f5c5fb67fa
5
5
  SHA512:
6
- metadata.gz: '044899369534c133039634cc835ce3efd71500a527d9249b10ca34fb8cb343f4754e0c72c8bc0f560267348881fe5d8e7701921b270431dfde1a810f1c48efad'
7
- data.tar.gz: 5c67f56709062e0982998ff59f49f9caa1b14bb630e81339d6183821add59bfaf731f2af5e416b724fc00d3f57e17d63138eabec63166288185d69284bd53891
6
+ metadata.gz: 9a75bb056071fc5ced4b3df285a74e42d2c4bbfd4ae34a21aa860702e88e5dae51ede402be1ff24664d1cee95f5d9c182a26106473a486f6b65591212a82a067
7
+ data.tar.gz: cb96f815a2d9970ca62006bded80d5d57b5ba6b8ce43be5dae0bfe0d9d610cbe5394d7f321c5605df9bc806fd87fb26105a97c448ba58dd1486528bc3409282f
@@ -41,7 +41,7 @@ jobs:
41
41
  matrix:
42
42
  # https://www.cockroachlabs.com/docs/releases/release-support-policy
43
43
  crdb: [v23.2, v24.1, v24.2]
44
- ruby: ["3.3"]
44
+ ruby: [3.4]
45
45
  name: Test (crdb=${{ matrix.crdb }} ruby=${{ matrix.ruby }})
46
46
  steps:
47
47
  - name: Set Up Actions
@@ -88,4 +88,7 @@ jobs:
88
88
  done
89
89
  cat ${{ github.workspace }}/setup.sql | cockroach sql --insecure
90
90
  - name: Test
91
- run: bundle exec rake test TESTOPTS='--profile=5'
91
+ run: bundle exec rake test
92
+ env:
93
+ TESTOPTS: "--profile=5"
94
+ RAILS_MINITEST_PLUGIN: "1" # Make sure that we use the minitest plugin for profiling.
data/CHANGELOG.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Changelog
2
2
 
3
- ## Ongoing
3
+ ## 7.2.1 - 2025-03-26
4
+
5
+ - Fix transaction state on rollback ([#364](https://github.com/cockroachdb/activerecord-cockroachdb-adapter/pull/364))
4
6
 
5
7
  ## 7.2.0 - 2024-09-24
6
8
 
data/CONTRIBUTING.md CHANGED
@@ -117,7 +117,11 @@ This section intent to help you with a checklist.
117
117
  - Check for some important methods, some will change for sure:
118
118
  - [x] `def new_column_from_field(`
119
119
  - [x] `def column_definitions(`
120
- - [x] `def pk_and_sequence_for(`
120
+ - [x] # `def pk_and_sequence_for(`
121
+ - [ ] `def new_column_from_field(`
122
+ - [ ] `def column_definitions(`
123
+ - [ ] `def pk_and_sequence_for(`
124
+ - [ ] `def foreign_keys(` and `def all_foreign_keys(`
121
125
  - [ ] ...
122
126
  - Check for setups containing `drop_table` in the test suite.
123
127
  Especially if you have tons of failure, this is likely the cause.
data/bin/console CHANGED
@@ -12,20 +12,22 @@ require "activerecord-cockroachdb-adapter"
12
12
  # structure_load(Post.connection_db_config, "awesome-file.sql")
13
13
  require "active_record/connection_adapters/cockroachdb/database_tasks"
14
14
 
15
+ DB_NAME = "ar_crdb_console"
16
+
15
17
  schema_kind = ENV.fetch("SCHEMA_KIND", ENV.fetch("SCHEMA", "default"))
16
18
 
17
- system("cockroach sql --insecure --host=localhost:26257 --execute='drop database if exists ar_crdb_console'",
19
+ system("cockroach sql --insecure --host=localhost:26257 --execute='drop database if exists #{DB_NAME}'",
18
20
  exception: true)
19
- system("cockroach sql --insecure --host=localhost:26257 --execute='create database ar_crdb_console'",
21
+ system("cockroach sql --insecure --host=localhost:26257 --execute='create database #{DB_NAME}'",
20
22
  exception: true)
21
23
 
22
24
  ActiveRecord::Base.establish_connection(
23
- #Alternative version: "cockroachdb://root@localhost:26257/ar_crdb_console"
25
+ #Alternative version: "cockroachdb://root@localhost:26257/#{DB_NAME}"
24
26
  adapter: "cockroachdb",
25
27
  host: "localhost",
26
28
  port: 26257,
27
29
  user: "root",
28
- database: "ar_crdb_console"
30
+ database: DB_NAME
29
31
  )
30
32
 
31
33
  load "#{__dir__}/console_schemas/#{schema_kind}.rb"
@@ -6,4 +6,6 @@ ActiveRecord::Schema.define do
6
6
  t.string :title
7
7
  t.text :body
8
8
  end
9
+
10
+ add_index("posts", ["title"], name: "index_posts_on_title", unique: true)
9
11
  end
@@ -24,9 +24,17 @@ module ActiveRecord
24
24
  table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name(table)}" }
25
25
  statements = table_deletes + fixture_inserts
26
26
 
27
- with_multi_statements do
28
- disable_referential_integrity do
29
- execute_batch(statements, "Fixtures Load")
27
+ begin # much faster without disabling referential integrity, worth trying.
28
+ with_multi_statements do
29
+ transaction(requires_new: true) do
30
+ execute_batch(statements, "Fixtures Load")
31
+ end
32
+ end
33
+ rescue
34
+ with_multi_statements do
35
+ disable_referential_integrity do
36
+ execute_batch(statements, "Fixtures Load")
37
+ end
30
38
  end
31
39
  end
32
40
  end
@@ -34,11 +34,18 @@ module ActiveRecord
34
34
  end
35
35
 
36
36
  def disable_referential_integrity
37
- foreign_keys = tables.map { |table| foreign_keys(table) }.flatten
37
+ foreign_keys = all_foreign_keys
38
38
 
39
- foreign_keys.each do |foreign_key|
40
- 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)
41
47
  end
48
+ execute_batch(statements, "Disable referential integrity -> remove foreign keys")
42
49
 
43
50
  yield
44
51
 
@@ -52,19 +59,88 @@ module ActiveRecord
52
59
  ActiveRecord::Base.table_name_suffix = ""
53
60
 
54
61
  begin
55
- foreign_keys.each do |foreign_key|
56
- # Avoid having PG:DuplicateObject error if a test is ran in transaction.
57
- # TODO: verify that there is no cache issue related to running this (e.g: fk
58
- # still in cache but not in db)
59
- 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
60
76
 
61
- add_foreign_key(foreign_key.from_table, foreign_key.to_table, **foreign_key.options)
77
+ schema_creation.accept(at)
62
78
  end
79
+ execute_batch(statements.compact, "Disable referential integrity -> add foreign keys")
63
80
  ensure
64
81
  ActiveRecord::Base.table_name_prefix = old_prefix
65
82
  ActiveRecord::Base.table_name_suffix = old_suffix
66
83
  end
67
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
68
144
  end
69
145
  end
70
146
  end
@@ -55,6 +55,37 @@ module ActiveRecord
55
55
  end
56
56
  end
57
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
+
58
89
  # OVERRIDE: CockroachDB does not support deferrable constraints.
59
90
  # See: https://go.crdb.dev/issue-v/31632/v23.1
60
91
  def foreign_key_options(from_table, to_table, options)
@@ -265,31 +296,6 @@ module ActiveRecord
265
296
  sql
266
297
  end
267
298
 
268
- # This overrides the method from PostegreSQL adapter
269
- # Resets the sequence of a table's primary key to the maximum value.
270
- def reset_pk_sequence!(table, pk = nil, sequence = nil)
271
- unless pk && sequence
272
- default_pk, default_sequence = pk_and_sequence_for(table)
273
-
274
- pk ||= default_pk
275
- sequence ||= default_sequence
276
- end
277
-
278
- if @logger && pk && !sequence
279
- @logger.warn "#{table} has primary key #{pk} with no default sequence."
280
- end
281
-
282
- if pk && sequence
283
- quoted_sequence = quote_table_name(sequence)
284
- max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
285
- if max_pk.nil?
286
- minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
287
- end
288
-
289
- query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false})", "SCHEMA")
290
- end
291
- end
292
-
293
299
  # override
294
300
  def native_database_types
295
301
  # Add spatial types
@@ -45,6 +45,27 @@ module ActiveRecord
45
45
  within_new_transaction(isolation: isolation, joinable: joinable, attempts: attempts + 1) { yield }
46
46
  end
47
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
+
48
69
  def retryable?(error)
49
70
  return true if serialization_error?(error)
50
71
  return true if error.is_a? ActiveRecord::SerializationFailure
data/lib/version.rb CHANGED
@@ -15,5 +15,5 @@
15
15
  # limitations under the License.
16
16
 
17
17
  module ActiveRecord
18
- COCKROACH_DB_ADAPTER_VERSION = "7.2.0"
18
+ COCKROACH_DB_ADAPTER_VERSION = "7.2.1"
19
19
  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.2.0
4
+ version: 7.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cockroach Labs
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-30 00:00:00.000000000 Z
11
+ date: 2025-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -127,7 +127,7 @@ licenses:
127
127
  - Apache-2.0
128
128
  metadata:
129
129
  allowed_push_host: https://rubygems.org
130
- post_install_message:
130
+ post_install_message:
131
131
  rdoc_options: []
132
132
  require_paths:
133
133
  - lib
@@ -142,8 +142,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
142
  - !ruby/object:Gem::Version
143
143
  version: '0'
144
144
  requirements: []
145
- rubygems_version: 3.0.3.1
146
- signing_key:
145
+ rubygems_version: 3.5.23
146
+ signing_key:
147
147
  specification_version: 4
148
148
  summary: CockroachDB adapter for ActiveRecord.
149
149
  test_files: []