activerecord-rescue_from_duplicate 0.1.6 → 0.2.7

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: 998e5fbd4ae44c14022a24a43b6501e37ebeaa4fae22d5ae98b150bb286c8013
4
- data.tar.gz: a9aff249013923879e7e2e9a746a16d32851d15c5b9eb327113b3f1926905e1f
3
+ metadata.gz: 00a9576198c85da3503fc44c7420dd287082a3a8219fe716cd88340ab8da432d
4
+ data.tar.gz: 196bb9b82c0d333e4fdbaf254992f9e2cf997d62d171420bff1859bdf4d1aca8
5
5
  SHA512:
6
- metadata.gz: a61214eaacecb66b6aa8088a590ea9f944207ad2578868df5b29d517bc8abf61992b630ef8a28854d591a30409e99bccad6ecc3672936601a829b9fde1cb78ff
7
- data.tar.gz: c0fbac267cb75a708a3dcd5a798e14861b6ff332826ef6d51bca73a2979bd5d1331679def180e8cde1c3be4839584ec7a454adbcb7f399345a32d5ba81a33393
6
+ metadata.gz: df70343a3219b0c4c2ce0497329d3f59ac3c1e60b6888fabb8b1b5a0562981d1a1b0a27ad1eb0f52e7b190dbfb7584cd6dbbcf72df9f4672b8d4de34f04807cd
7
+ data.tar.gz: fb404a3201ab1bc5739a4ac71686d7bb5002ce849e439c87063aa9f530f9390165e9044a549bcc733fcc15d7d004c4dd5b1386b1b8a203be4762c5f94bb03c9d
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: "/"
5
+ schedule:
6
+ interval: weekly
@@ -0,0 +1,42 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ name: Ruby ${{ matrix.ruby }}
9
+ services:
10
+ postgres:
11
+ image: postgres
12
+ env:
13
+ POSTGRES_USER: "test"
14
+ POSTGRES_PASSWORD: "test"
15
+ POSTGRES_DB: "rescue_from_duplicate"
16
+ ports:
17
+ - "5432:5432"
18
+ mysql:
19
+ image: mysql:8.0
20
+ env:
21
+ MYSQL_USER: "test"
22
+ MYSQL_PASSWORD: "test"
23
+ MYSQL_ALLOW_EMPTY_PASSWORD: "true"
24
+ MYSQL_DATABASE: "rescue_from_duplicate"
25
+ ports:
26
+ - "3306:3306"
27
+ options: --health-cmd "mysqladmin ping" --health-interval 10s --health-timeout 5s --health-retries 10
28
+ strategy:
29
+ fail-fast: false
30
+ matrix:
31
+ ruby: ["3.0", "3.1", "3.2", "3.3"]
32
+ steps:
33
+ - name: Check out code
34
+ uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
35
+ - name: Set up Ruby ${{ matrix.ruby }}
36
+ uses: ruby/setup-ruby@e34163cd15f4bb403dcd72d98e295997e6a55798 # v1.238.0
37
+ with:
38
+ ruby-version: ${{ matrix.ruby }}
39
+ bundler-cache: true
40
+ cache-version: 1
41
+ - name: Tests
42
+ run: bundle exec rake
@@ -0,0 +1,24 @@
1
+ # .github/workflows/cla.yml
2
+ name: Contributor License Agreement (CLA)
3
+
4
+ on:
5
+ pull_request_target:
6
+ types: [opened, synchronize]
7
+ issue_comment:
8
+ types: [created]
9
+
10
+ jobs:
11
+ cla:
12
+ runs-on: ubuntu-latest
13
+ if: |
14
+ (github.event.issue.pull_request
15
+ && !github.event.issue.pull_request.merged_at
16
+ && contains(github.event.comment.body, 'signed')
17
+ )
18
+ || (github.event.pull_request && !github.event.pull_request.merged)
19
+ steps:
20
+ - uses: Shopify/shopify-cla-action@v1
21
+ with:
22
+ github-token: ${{ secrets.GITHUB_TOKEN }}
23
+ cla-token: ${{ secrets.CLA_TOKEN }}
24
+
@@ -0,0 +1,26 @@
1
+ name: Release
2
+
3
+ on: workflow_dispatch
4
+
5
+ jobs:
6
+ release:
7
+ permissions:
8
+ contents: write
9
+ id-token: write
10
+
11
+ environment: release
12
+
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
17
+ - name: Install required packages
18
+ run: |
19
+ sudo apt-get update
20
+ sudo apt-get -y install libmemcached-dev libmysqlclient-dev libpq-dev libsasl2-dev
21
+ - name: Set up Ruby
22
+ uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
23
+ with:
24
+ bundler-cache: true
25
+ ruby-version: 3.3
26
+ - uses: rubygems/release-gem@052cc82692552de3ef2b81fd670e41d13cba8092 # v1.4.0
data/.gitignore CHANGED
@@ -3,7 +3,6 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
- Gemfile.lock
7
6
  InstalledFiles
8
7
  _yardoc
9
8
  coverage
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.7] - 2026-08-26
11
+
12
+ ### Fixed
13
+
14
+ - It was possible that the gem wouldn't rescue a unique constraint violation error in
15
+ the even that a non unique index name would match a subset of the unique index
16
+ to rescue.
data/Gemfile.lock ADDED
@@ -0,0 +1,100 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ activerecord-rescue_from_duplicate (0.2.7)
5
+ activerecord (>= 7)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (7.1.5.2)
11
+ activesupport (= 7.1.5.2)
12
+ activerecord (7.1.5.2)
13
+ activemodel (= 7.1.5.2)
14
+ activesupport (= 7.1.5.2)
15
+ timeout (>= 0.4.0)
16
+ activesupport (7.1.5.2)
17
+ base64
18
+ benchmark (>= 0.3)
19
+ bigdecimal
20
+ concurrent-ruby (~> 1.0, >= 1.0.2)
21
+ connection_pool (>= 2.2.5)
22
+ drb
23
+ i18n (>= 1.6, < 2)
24
+ logger (>= 1.4.2)
25
+ minitest (>= 5.1)
26
+ mutex_m
27
+ securerandom (>= 0.3)
28
+ tzinfo (~> 2.0)
29
+ base64 (0.3.0)
30
+ benchmark (0.4.1)
31
+ bigdecimal (3.2.2)
32
+ coderay (1.1.3)
33
+ concurrent-ruby (1.3.5)
34
+ connection_pool (2.5.3)
35
+ diff-lcs (1.5.1)
36
+ docile (1.4.0)
37
+ drb (2.2.3)
38
+ i18n (1.14.7)
39
+ concurrent-ruby (~> 1.0)
40
+ logger (1.7.0)
41
+ method_source (1.0.0)
42
+ minitest (5.25.5)
43
+ mutex_m (0.3.0)
44
+ mysql2 (0.5.6)
45
+ pg (1.5.6)
46
+ pry (0.14.2)
47
+ coderay (~> 1.1)
48
+ method_source (~> 1.0)
49
+ rake (13.2.0)
50
+ rspec (3.13.0)
51
+ rspec-core (~> 3.13.0)
52
+ rspec-expectations (~> 3.13.0)
53
+ rspec-mocks (~> 3.13.0)
54
+ rspec-core (3.13.0)
55
+ rspec-support (~> 3.13.0)
56
+ rspec-expectations (3.13.0)
57
+ diff-lcs (>= 1.2.0, < 2.0)
58
+ rspec-support (~> 3.13.0)
59
+ rspec-mocks (3.13.0)
60
+ diff-lcs (>= 1.2.0, < 2.0)
61
+ rspec-support (~> 3.13.0)
62
+ rspec-support (3.13.1)
63
+ securerandom (0.3.2)
64
+ simplecov (0.22.0)
65
+ docile (~> 1.1)
66
+ simplecov-html (~> 0.11)
67
+ simplecov_json_formatter (~> 0.1)
68
+ simplecov-html (0.12.3)
69
+ simplecov_json_formatter (0.1.4)
70
+ sqlite3 (1.7.3-aarch64-linux)
71
+ sqlite3 (1.7.3-arm-linux)
72
+ sqlite3 (1.7.3-arm64-darwin)
73
+ sqlite3 (1.7.3-x86-linux)
74
+ sqlite3 (1.7.3-x86_64-darwin)
75
+ sqlite3 (1.7.3-x86_64-linux)
76
+ timeout (0.4.3)
77
+ tzinfo (2.0.6)
78
+ concurrent-ruby (~> 1.0)
79
+
80
+ PLATFORMS
81
+ aarch64-linux
82
+ arm-linux
83
+ arm64-darwin
84
+ x86-linux
85
+ x86_64-darwin
86
+ x86_64-linux
87
+
88
+ DEPENDENCIES
89
+ activerecord-rescue_from_duplicate!
90
+ bundler
91
+ mysql2
92
+ pg
93
+ pry
94
+ rake
95
+ rspec
96
+ simplecov
97
+ sqlite3
98
+
99
+ BUNDLED WITH
100
+ 2.5.3
data/README.md CHANGED
@@ -10,8 +10,6 @@ Additionally, a macro allows you to assume that the record will be unique and re
10
10
 
11
11
  Tested with:
12
12
 
13
- - ActiveRecord: 3.2, 4.0, 4.1, 4.2, edge
14
- - Ruby 1.9.3, 2.0.0, 2.1.2
15
13
  - MySQL, PostgreSQL, Sqlite3
16
14
 
17
15
  **Note:**
@@ -69,7 +67,7 @@ And then execute:
69
67
  Or install it yourself as:
70
68
 
71
69
  $ gem install activerecord-rescue_from_duplicate
72
-
70
+
73
71
  ## Development Setup
74
72
 
75
73
  Install:
@@ -13,12 +13,14 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = "https://github.com/Shopify/activerecord-rescue_from_duplicate"
14
14
  spec.license = "MIT"
15
15
 
16
+ spec.required_ruby_version = ">= 3.0"
17
+
16
18
  spec.files = `git ls-files`.split($/)
17
19
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
20
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
21
  spec.require_paths = ["lib"]
20
22
 
21
- spec.add_dependency "activerecord", ">= 5.2"
23
+ spec.add_dependency "activerecord", ">= 7"
22
24
 
23
25
  spec.add_development_dependency "bundler"
24
26
  spec.add_development_dependency "rake"
@@ -2,6 +2,8 @@ require 'active_support/core_ext/class'
2
2
 
3
3
  module RescueFromDuplicate::ActiveRecord
4
4
  module Extension
5
+ DUPLICATE_KEY_REGEX = /for key ['"](?<key>[^'"]+)['"]/
6
+
5
7
  extend ActiveSupport::Concern
6
8
 
7
9
  module ClassMethods
@@ -43,11 +45,14 @@ module RescueFromDuplicate::ActiveRecord
43
45
  end
44
46
 
45
47
  def exception_handler(exception)
48
+ handlers = self.class._rescue_from_duplicate_handlers
49
+ return if handlers.empty?
50
+
46
51
  columns = exception_columns(exception)
47
52
  return unless columns
48
53
  columns = columns.sort
49
54
 
50
- self.class._rescue_from_duplicate_handlers.detect do |handler|
55
+ handlers.detect do |handler|
51
56
  handler.rescue? && columns == handler.columns
52
57
  end
53
58
  end
@@ -67,7 +72,7 @@ module RescueFromDuplicate::ActiveRecord
67
72
  end
68
73
 
69
74
  def sqlite3_exception_columns(exception)
70
- extract_columns(exception.message[/columns? (.*) (?:is|are) not unique/, 1]) ||
75
+ extract_columns(exception.message[/columns? (.*) (?:is|are) not unique/, 1]) ||
71
76
  extract_columns(exception.message[/UNIQUE constraint failed: ([^:]*)\:?/, 1])
72
77
  end
73
78
 
@@ -77,8 +82,15 @@ module RescueFromDuplicate::ActiveRecord
77
82
  end
78
83
 
79
84
  def other_exception_columns(exception)
80
- indexes = self.class.connection.indexes(self.class.table_name)
81
- indexes.detect{ |i| exception.message.include?(i.name) }.try(:columns) || []
85
+ key = exception.message[DUPLICATE_KEY_REGEX, :key]
86
+ return [] unless key
87
+
88
+ # Mysql > 8.0 prefixes indexes with the table name.
89
+ index_name = key.delete_prefix("#{self.class.table_name}.")
90
+
91
+ indexes = self.class.connection.schema_cache.indexes(self.class.table_name)
92
+ index = indexes.find { |i| i.name == index_name }
93
+ index ? index.columns : []
82
94
  end
83
95
  end
84
96
  end
@@ -1,5 +1,5 @@
1
1
  module Activerecord
2
2
  module RescueFromDuplicate
3
- VERSION = '0.1.6'
3
+ VERSION = '0.2.7'
4
4
  end
5
5
  end
@@ -16,7 +16,7 @@ module RescueFromDuplicate
16
16
  klasses.each do |klass|
17
17
  klass._rescue_from_duplicate_handlers.each do |handler|
18
18
  next unless klass.connection.table_exists?(klass.table_name)
19
- unique_indexes = klass.connection.indexes(klass.table_name).select(&:unique)
19
+ unique_indexes = klass.connection.schema_cache.indexes(klass.table_name).select(&:unique)
20
20
 
21
21
  unless unique_indexes.any? { |index| index.columns.map(&:to_s).sort == handler.columns }
22
22
  missing_unique_indexes << MissingUniqueIndex.new(klass, handler.attributes, handler.columns)
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ module RescueFromDuplicate
4
+ describe "#other_exception_columns (MySQL/MariaDB duplicate-key parsing)" do
5
+ let(:prefix_index) do
6
+ ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(
7
+ "order_transactions", "index_order_transactions_on_shop_id", false, ["shop_id"]
8
+ )
9
+ end
10
+ let(:unique_index) do
11
+ ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(
12
+ "order_transactions", "index_order_transactions_on_shop_id_and_unique_token", true, ["shop_id", "unique_token"]
13
+ )
14
+ end
15
+
16
+ let(:model) do
17
+ indexes = [prefix_index, unique_index]
18
+ connection = double(schema_cache: double(indexes: indexes))
19
+ Class.new do
20
+ include RescueFromDuplicate::ActiveRecord::Extension
21
+ define_singleton_method(:table_name) { "order_transactions" }
22
+ define_singleton_method(:connection) { connection }
23
+ end
24
+ end
25
+
26
+ subject(:columns) { model.allocate.send(:other_exception_columns, exception) }
27
+
28
+ context "with a MySQL 8.0+ table-qualified key name" do
29
+ let(:exception) do
30
+ ::ActiveRecord::RecordNotUnique.new(
31
+ "Duplicate entry '1-abc' for key 'order_transactions.index_order_transactions_on_shop_id_and_unique_token'"
32
+ )
33
+ end
34
+
35
+ it "selects the violated index regardless of ordering" do
36
+ expect(columns).to eq ["shop_id", "unique_token"]
37
+ end
38
+ end
39
+
40
+ context "with a MySQL 5.7 / MariaDB bare key name" do
41
+ let(:exception) do
42
+ ::ActiveRecord::RecordNotUnique.new(
43
+ "Duplicate entry '1-abc' for key 'index_order_transactions_on_shop_id_and_unique_token'"
44
+ )
45
+ end
46
+
47
+ it "selects the violated index by exact name" do
48
+ expect(columns).to eq ["shop_id", "unique_token"]
49
+ end
50
+ end
51
+
52
+ context "when the message carries no key name" do
53
+ let(:exception) { ::ActiveRecord::RecordNotUnique.new("some unrelated error") }
54
+
55
+ it "returns no columns" do
56
+ expect(columns).to eq []
57
+ end
58
+ end
59
+ end
60
+ end
@@ -7,7 +7,8 @@ shared_examples 'database error rescuing' do
7
7
  subject { Rescuable.new }
8
8
 
9
9
  before do
10
- allow(Rescuable).to(receive(:connection).and_return(double(indexes: [Rescuable.index])))
10
+ connection = double(schema_cache: double(indexes: [Rescuable.index]))
11
+ allow(Rescuable).to(receive(:connection).and_return(connection))
11
12
  end
12
13
 
13
14
  describe "#create_or_update when the validation fails" do
@@ -26,6 +27,13 @@ shared_examples 'database error rescuing' do
26
27
  it "raises an exception" do
27
28
  expect { subject.create_or_update }.to raise_error(ActiveRecord::RecordNotUnique)
28
29
  end
30
+
31
+ it "doesn't parse the message" do
32
+ allow(uniqueness_exception).to(
33
+ receive(:message).and_raise(StandardError, "Message should not have been accessed")
34
+ )
35
+ expect { subject.create_or_update }.to raise_error(ActiveRecord::RecordNotUnique)
36
+ end
29
37
  end
30
38
  end
31
39
 
@@ -13,7 +13,7 @@ CONNECTIONS = {
13
13
  username: "test",
14
14
  password: "test",
15
15
  host: "127.0.0.1",
16
- port: "29292",
16
+ port: "5432",
17
17
  },
18
18
  test_mysql: {
19
19
  adapter: "mysql2",
@@ -21,7 +21,7 @@ CONNECTIONS = {
21
21
  username: "test",
22
22
  password: "test",
23
23
  host: "127.0.0.1",
24
- port: "29291",
24
+ port: "3306",
25
25
  },
26
26
  }
27
27
  class CreateAllTables < ActiveRecord::Migration[5.2]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-rescue_from_duplicate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guillaume Malette
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-26 00:00:00.000000000 Z
11
+ date: 2026-08-26 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: '5.2'
19
+ version: '7'
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: '5.2'
26
+ version: '7'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -143,10 +143,16 @@ executables: []
143
143
  extensions: []
144
144
  extra_rdoc_files: []
145
145
  files:
146
+ - ".github/dependabot.yml"
147
+ - ".github/workflows/ci.yml"
148
+ - ".github/workflows/cla.yml"
149
+ - ".github/workflows/release.yml"
146
150
  - ".gitignore"
147
151
  - ".rspec"
148
- - ".travis.yml"
152
+ - ".ruby-version"
153
+ - CHANGELOG.md
149
154
  - Gemfile
155
+ - Gemfile.lock
150
156
  - LICENSE.txt
151
157
  - README.md
152
158
  - Rakefile
@@ -160,6 +166,7 @@ files:
160
166
  - lib/rescue_from_duplicate/rescuer.rb
161
167
  - lib/rescue_from_duplicate/uniqueness_rescuer.rb
162
168
  - shipit.rubygems.yml
169
+ - spec/other_exception_columns_spec.rb
163
170
  - spec/rescue_from_duplicate_spec.rb
164
171
  - spec/rescuer_spec.rb
165
172
  - spec/spec_helper.rb
@@ -171,7 +178,7 @@ licenses:
171
178
  - MIT
172
179
  metadata:
173
180
  allowed_push_host: https://rubygems.org
174
- post_install_message:
181
+ post_install_message:
175
182
  rdoc_options: []
176
183
  require_paths:
177
184
  - lib
@@ -179,19 +186,20 @@ required_ruby_version: !ruby/object:Gem::Requirement
179
186
  requirements:
180
187
  - - ">="
181
188
  - !ruby/object:Gem::Version
182
- version: '0'
189
+ version: '3.0'
183
190
  required_rubygems_version: !ruby/object:Gem::Requirement
184
191
  requirements:
185
192
  - - ">="
186
193
  - !ruby/object:Gem::Version
187
194
  version: '0'
188
195
  requirements: []
189
- rubygems_version: 3.0.3
190
- signing_key:
196
+ rubygems_version: 3.5.22
197
+ signing_key:
191
198
  specification_version: 4
192
199
  summary: Rescue from MySQL and Sqlite duplicate errors when trying to insert records
193
200
  that fail uniqueness validation
194
201
  test_files:
202
+ - spec/other_exception_columns_spec.rb
195
203
  - spec/rescue_from_duplicate_spec.rb
196
204
  - spec/rescuer_spec.rb
197
205
  - spec/spec_helper.rb
data/.travis.yml DELETED
@@ -1,18 +0,0 @@
1
- sudo: false
2
- rvm:
3
- - '2.5'
4
- - '2.6'
5
- - '2.7'
6
-
7
- env:
8
- - DOCKER_COMPOSE_VERSION=1.20.1
9
-
10
- before_script:
11
- - docker-compose --version
12
- - docker-compose pull
13
- - docker-compose build
14
- - docker-compose up --no-start
15
- - docker-compose start
16
- - sleep 30 # Wait for databases to come online
17
- - docker ps
18
-