activerecord-cockroachdb-adapter 8.0.0 → 8.0.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: 297d02b873231237d61965f02c1a058a02f81927866af87a62aba2dd8da7d19a
4
- data.tar.gz: b1d74cd086386ea3ae8d6ef9af2e3c8ba054e55136accadd667b3348f7a63a91
3
+ metadata.gz: 9721532ff04318993f0835604dff8899289ee81e84e0f6daff67ad8a0024b608
4
+ data.tar.gz: 82db81fab9b8358ea7953bfd7a9a3863794ea0946b56dfd5b57aefe7f2951eba
5
5
  SHA512:
6
- metadata.gz: e35e21188ac363b1e7f676588fbc478e0792af4669cdeab74c9eda58d6e1eae55dfc474de48cd7dcf9a9c95a516d3fbd49c56a42d265931aa738c8e9800046f7
7
- data.tar.gz: 4c45ff7d9ffc569efa0b9838e67fb2a1d76fda059ddbeb0ed073fe1486286092477beaa83990863d08122028adb25b5f25f92beb9bce6b8118f6aed63502351b
6
+ metadata.gz: 6089da8ff8fcbb51435f6c408c663fe31d76c2652e7053b1f074ca36769ef88e99356a25b3530eb36dc2dbbc93e3a1db14d52ef76323abda301a093ed0217200
7
+ data.tar.gz: c236373fdc39156b05aa546b596eda2aba501de27f9a486947fc4ec25a8f871210e436675e191d0b369ad1d159dd851544d0df538725f1bef18b3fc2efb1354f
@@ -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
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Ongoing
4
4
 
5
+ ## 8.0.1 - 2025-03-04
6
+
7
+ - Fixed retry logic after transaction rollback ([#364](https://github.com/cockroachdb/activerecord-cockroachdb-adapter/pull/364))
8
+
5
9
  ## 8.0.0 - 2024-12-19
6
10
 
7
11
  - Add support for Rails 8.0 ([#356](https://github.com/cockroachdb/activerecord-cockroachdb-adapter/pull/356))
data/CONTRIBUTING.md CHANGED
@@ -115,9 +115,10 @@ This section intent to help you with a checklist.
115
115
  - Verify the written text at the beginning of the test suite, there are likely
116
116
  some changes in excluded tests.
117
117
  - Check for some important methods, some will change for sure:
118
- - [x] `def new_column_from_field(`
119
- - [x] `def column_definitions(`
120
- - [x] `def pk_and_sequence_for(`
118
+ - [ ] `def new_column_from_field(`
119
+ - [ ] `def column_definitions(`
120
+ - [ ] `def pk_and_sequence_for(`
121
+ - [ ] `def foreign_keys(` and `def all_foreign_keys(`
121
122
  - [ ] ...
122
123
  - Check for setups containing `drop_table` in the test suite.
123
124
  Especially if you have tons of failure, this is likely the cause.
@@ -24,8 +24,14 @@ 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
- disable_referential_integrity do
28
- execute_batch(statements, "Fixtures Load")
27
+ begin # much faster without disabling referential integrity, worth trying.
28
+ transaction(requires_new: true) do
29
+ execute_batch(statements, "Fixtures Load")
30
+ end
31
+ rescue
32
+ disable_referential_integrity do
33
+ execute_batch(statements, "Fixtures Load")
34
+ end
29
35
  end
30
36
  end
31
37
  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 = internal_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
@@ -216,7 +216,6 @@ module ActiveRecord
216
216
  options[:deferrable] = extract_constraint_deferrable(row["deferrable"], row["deferred"])
217
217
 
218
218
  options[:validate] = row["valid"]
219
- to_table = PostgreSQL::Utils.unquote_identifier(row["to_table"])
220
219
 
221
220
  ForeignKeyDefinition.new(table_name, to_table, options)
222
221
  end
@@ -296,31 +295,6 @@ module ActiveRecord
296
295
  sql
297
296
  end
298
297
 
299
- # This overrides the method from PostegreSQL adapter
300
- # Resets the sequence of a table's primary key to the maximum value.
301
- def reset_pk_sequence!(table, pk = nil, sequence = nil)
302
- unless pk && sequence
303
- default_pk, default_sequence = pk_and_sequence_for(table)
304
-
305
- pk ||= default_pk
306
- sequence ||= default_sequence
307
- end
308
-
309
- if @logger && pk && !sequence
310
- @logger.warn "#{table} has primary key #{pk} with no default sequence."
311
- end
312
-
313
- if pk && sequence
314
- quoted_sequence = quote_table_name(sequence)
315
- max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA")
316
- if max_pk.nil?
317
- minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA")
318
- end
319
-
320
- query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false})", "SCHEMA")
321
- end
322
- end
323
-
324
298
  # override
325
299
  def native_database_types
326
300
  # 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 = "8.0.0"
18
+ COCKROACH_DB_ADAPTER_VERSION = "8.0.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: 8.0.0
4
+ version: 8.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cockroach Labs
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-12-20 00:00:00.000000000 Z
11
+ date: 2025-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord