atomically 1.0.6 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 920b276434066c1fd3d0172fe14ef28d818d17aaf8f7c4c5c12bff0554631daf
4
- data.tar.gz: b89ddf56fbe003a5d54a085b66c46e854462eae6f40c28843b5c19f835bb7127
2
+ SHA1:
3
+ metadata.gz: f11789d3b23462ec0a597434cbd71ad693684fb2
4
+ data.tar.gz: '0365791ed9e8a5f1b0d035a0db850cc8a5619983'
5
5
  SHA512:
6
- metadata.gz: 15edd248d731a80e0e2797cc32fd046f224d0e094691ee3e3740923fb4372877b3041d7436c8685e658476959a5da7022519d2a83eb39c56345e491f14395221
7
- data.tar.gz: aee6b53ff12ccf5424073746812b84f269f94e08bda83998f7b2ca60d7672b5a6c54e5053388b1ab14eca36b3041e7d3c2b90282c411caaa664e4b736810d75c
6
+ metadata.gz: 1bd013da03d5b05cb5dd98545f94cca224efa8815822b0ef1eed537bb1f2f5f5fe05fb82d9aa9fdfefb6c6f676e194a344c6a84254a999d7b3e6e69bc80badbc
7
+ data.tar.gz: 16335847dbb0540de93a43595a151324109539d9b6d6b728b885e7e83499c7293f6e4ecd5e81d34e41ce2279ab40f67cb10a19eba7529425a602ce4e5c39155f
data/.travis.yml CHANGED
@@ -2,22 +2,30 @@ sudo: false
2
2
  language: ruby
3
3
  rvm:
4
4
  - 2.2
5
- - 2.5
5
+ - 2.6
6
+ services:
7
+ - mysql
8
+ addons:
9
+ postgresql: "9.6"
6
10
  env:
7
11
  global:
8
12
  - CC_TEST_REPORTER_ID=12e1facab2e8910c9b9d6b9e6870c5544a5c44a1bef25cc6638fd132aa4af6b4
9
13
  matrix:
10
14
  - DB=mysql
15
+ - DB=pg
11
16
  gemfile:
12
17
  - gemfiles/3.2.gemfile
13
18
  - gemfiles/4.2.gemfile
14
19
  - gemfiles/5.0.gemfile
15
20
  - gemfiles/5.1.gemfile
16
21
  - gemfiles/5.2.gemfile
22
+ - gemfiles/6.0.gemfile
17
23
  matrix:
18
24
  exclude:
19
25
  - gemfile: gemfiles/3.2.gemfile
20
- rvm: 2.5
26
+ rvm: 2.6
27
+ - gemfile: gemfiles/6.0.gemfile
28
+ rvm: 2.2
21
29
  before_install:
22
30
  - gem i rubygems-update -v '<3' && update_rubygems
23
31
  - gem install bundler -v 1.17.3
@@ -26,7 +34,10 @@ before_install:
26
34
  - chmod +x ./cc-test-reporter
27
35
  - ./cc-test-reporter before-build
28
36
  before_script:
37
+ - mysql -V
29
38
  - mysql -u root -e 'CREATE DATABASE travis_ci_test;'
39
+ - psql -c "SELECT version();"
40
+ - psql -c 'create database travis_ci_test;' -U postgres
30
41
  script:
31
42
  - bundle exec rake test
32
43
  after_script:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
1
  ## Change Log
2
2
 
3
+ ### [v1.0.6](https://github.com/khiav223577/atomically/compare/v1.0.5...v1.0.6) 2019/01/28
4
+ - [#10](https://github.com/khiav223577/atomically/pull/10) `decrement_unsigned_counters` should be able to decrement the field to zero (@khiav223577)
5
+
3
6
  ### [v1.0.5](https://github.com/khiav223577/atomically/compare/v1.0.4...v1.0.5) 2019/01/28
4
7
  - [#9](https://github.com/khiav223577/atomically/pull/9) Implement `decrement_unsigned_counters` (@khiav223577)
5
8
  - [#8](https://github.com/khiav223577/atomically/pull/8) Fix: broken test cases after bundler 2.0 was released (@khiav223577)
data/README.md CHANGED
@@ -8,14 +8,17 @@
8
8
 
9
9
  `atomically` is a Ruby Gem for you to write atomic query with ease.
10
10
 
11
- Supports Rails 3.2, 4.2, 5.0, 5.1, 5.2.
11
+ All methods are defined in `Atomically::QueryService` instead of defining in `ActiveRecord` directly, in order not to pollute the model instance.
12
+
13
+ - Supports Rails 3.2, 4.2, 5.0, 5.1, 5.2, 6.0.
14
+ - Supports PostgreSQL, MySQL.
12
15
 
13
16
  ## Table of contents
14
17
 
15
18
  1. [Installation](#installation)
16
19
  2. [Methods](#methods)
17
20
  - Relation Methods
18
- - [create_or_plus](#create_or_plus-columns-values-on_duplicate_update_columns)
21
+ - [create_or_plus](#create_or_plus-columns-values-on_duplicate_update_columns-conflict_target)
19
22
  - [pay_all](#pay_all-hash-update_columns-primary_key-id)
20
23
  - [update_all](#update_all-expected_number-updates)
21
24
  - [update_all_and_get_ids](#update_all_and_get_ids-updates)
@@ -46,19 +49,35 @@ Or install it yourself as:
46
49
 
47
50
  Note: ActiveRecord validations and callbacks will **NOT** be triggered when calling below methods.
48
51
 
49
- ### create_or_plus _(columns, values, on_duplicate_update_columns)_
52
+ ### create_or_plus _(columns, values, on_duplicate_update_columns, conflict_target:)_
50
53
 
51
54
  Import an array of records. When key is duplicate, plus the old value with new value.
52
- It is useful to add `items` to `user` when `user_items` may not exist.
55
+ It is useful to add `items` to `user` when `user_items` may not exist. (Let `User` and `Item` are many-to-many relationship.)
53
56
 
54
57
  #### Parameters
55
58
 
56
59
  - First two args (`columns`, `values`) are the same with the [import](https://github.com/zdennis/activerecord-import#columns-and-arrays) method.
57
60
  - `on_duplicate_update_columns` - The column that will be updated on duplicate.
61
+ - `conflict_target` - Needed only in pg. Specifies which columns have unique index.
58
62
 
59
63
  #### Example
60
64
 
61
65
  ```rb
66
+ class User < ApplicationRecord
67
+ has_many :user_items
68
+ has_many :items, through: :user_items
69
+ end
70
+
71
+ class UserItem < ApplicationRecord
72
+ belongs_to :user
73
+ belongs_to :item
74
+ end
75
+
76
+ class Item < ApplicationRecord
77
+ has_many :user_items
78
+ has_many :users, through: :user_items
79
+ end
80
+
62
81
  user = User.find(2)
63
82
  item1 = Item.find(1)
64
83
  item2 = Item.find(2)
@@ -67,44 +86,59 @@ item2 = Item.find(2)
67
86
  ```rb
68
87
  columns = [:user_id, :item_id, :quantity]
69
88
  values = [[user.id, item1.id, 3], [user.id, item2.id, 2]]
70
- on_duplicate_update_columns = [:quantity]
71
89
 
72
- UserItem.atomically.create_or_plus(columns, values, on_duplicate_update_columns)
90
+ # mysql
91
+ UserItem.atomically.create_or_plus(columns, values, [:quantity])
92
+
93
+ # pg
94
+ UserItem.atomically.create_or_plus(columns, values, [:quantity], conflict_target: [:user_id, :item_id])
73
95
  ```
74
96
 
75
97
  before
76
98
 
77
- ![before](https://user-images.githubusercontent.com/4011729/48998921-ff430600-f18f-11e8-8eeb-e8a71bbf5802.png)
99
+ ![before](https://user-images.githubusercontent.com/4011729/67365648-95e89480-f5a4-11e9-8147-279385c6f442.png)
78
100
 
79
101
  after
80
102
 
81
- ![image](https://user-images.githubusercontent.com/4011729/48999092-8d1ef100-f190-11e8-8372-86e2e99cbe08.png)
103
+ ![after](https://user-images.githubusercontent.com/4011729/67365653-97b25800-f5a4-11e9-8314-8e6ff8d2cd61.png)
104
+
82
105
 
83
106
  #### SQL queries
84
107
 
85
108
  ```sql
109
+ # mysql
86
110
  INSERT INTO `user_items` (`user_id`,`item_id`,`quantity`,`created_at`,`updated_at`) VALUES
87
111
  (2,1,3,'2018-11-27 03:44:25','2018-11-27 03:44:25'),
88
112
  (2,2,2,'2018-11-27 03:44:25','2018-11-27 03:44:25')
89
- ON DUPLICATE KEY UPDATE `quantity` = `quantity` + VALUES(`quantity`)
113
+ ON DUPLICATE KEY UPDATE
114
+ `quantity` = `quantity` + VALUES(`quantity`)
115
+
116
+ # pg
117
+ INSERT INTO "user_items" ("user_id","item_id","quantity","created_at","updated_at") VALUES
118
+ (2,1,3,'2018-11-27 03:44:25.847909','2018-11-27 03:44:25.847909'),
119
+ (2,2,2,'2018-11-27 03:44:25.847909','2018-11-27 03:44:25.847909')
120
+ ON CONFLICT (user_id, item_id) DO UPDATE SET
121
+ "quantity" = "user_items"."quantity" + excluded."quantity" RETURNING "id"
90
122
  ```
91
123
 
92
124
  ---
93
125
  ### pay_all _(hash, update_columns, primary_key: :id)_
94
126
 
95
- Reduce the quantity of items and return how many rows and updated if all of them is enough.
127
+ Reduce the quantity of items and return how many rows and updated if all of them are enough.
96
128
  Do nothing and return zero if any of them is not enough.
97
129
 
98
130
  #### Parameters
99
131
 
100
132
  - `hash` - A hash contains the id of the models as keys and the amount to update the field by as values.
101
133
  - `update_columns` - The column that will be updated.
102
- - `primary_key` - Specify the column that `id`(the key of hash) refer to.
134
+ - `primary_key` - Specify the column that `id`(the key of hash) refers to.
103
135
 
104
136
  #### Example
105
137
 
106
138
  ```rb
107
139
  user.user_items.atomically.pay_all({ item1.id => 4, item2.id => 3 }, [:quantity], primary_key: :item_id)
140
+ # => 2 (if success)
141
+ # => 0 (if some aren't enough)
108
142
  ```
109
143
 
110
144
  #### SQL queries
@@ -141,21 +175,21 @@ Behaves like [ActiveRecord::Relation#update_all](https://apidock.com/rails/Activ
141
175
 
142
176
  #### Examples
143
177
  ```rb
144
- User.where(id: [1, 2]).atomically.update_all(2, name: '')
145
- # => 2
178
+ User.where(id: [5, 6]).atomically.update_all(2, name: '')
179
+ # => 2 (success)
146
180
 
147
- User.where(id: [1, 2, 3]).atomically.update_all(2, name: '')
148
- # => 0
181
+ User.where(id: [7, 8, 9]).atomically.update_all(2, name: '')
182
+ # => 0 (fail)
149
183
  ```
150
184
 
151
185
  #### SQL queries
152
186
 
153
187
  ```sql
154
- # User.where(id: [1, 2, 3]).atomically.update_all(2, name: '')
155
- UPDATE `users` SET `users`.`name` = '' WHERE `users`.`id` IN (1, 2, 3) AND (
188
+ # User.where(id: [7, 8, 9]).atomically.update_all(2, name: '')
189
+ UPDATE `users` SET `users`.`name` = '' WHERE `users`.`id` IN (7, 8, 9) AND (
156
190
  (
157
191
  SELECT COUNT(*) FROM (
158
- SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 2, 3)
192
+ SELECT `users`.* FROM `users` WHERE `users`.`id` IN (7, 8, 9)
159
193
  ) subquery
160
194
  ) = 2
161
195
  )
@@ -164,7 +198,7 @@ UPDATE `users` SET `users`.`name` = '' WHERE `users`.`id` IN (1, 2, 3) AND (
164
198
  ---
165
199
  ### update_all_and_get_ids _(updates)_
166
200
 
167
- Behaves like [ActiveRecord::Relation#update_all](https://apidock.com/rails/ActiveRecord/Relation/update_all), but return the ids array of updated records instead of the number of updated records.
201
+ Behaves like [ActiveRecord::Relation#update_all](https://apidock.com/rails/ActiveRecord/Relation/update_all), but return an array of updated records' id instead of the number of updated records.
168
202
 
169
203
 
170
204
  #### Parameters
@@ -175,23 +209,32 @@ Behaves like [ActiveRecord::Relation#update_all](https://apidock.com/rails/Activ
175
209
 
176
210
  ```rb
177
211
  User.where(account: ['moon', 'wolf']).atomically.update_all_and_get_ids('money = money + 1')
178
- # => [254, 371]
212
+ # => [254, 371] (array of updated user ids)
213
+
214
+ User.where(account: ['moon', 'wolf']).update_all('money = money + 1')
215
+ # => 2 (the number of updated records)
179
216
  ```
180
217
 
181
218
  #### SQL queries
182
219
 
183
220
  ```sql
221
+ # mysql
184
222
  BEGIN
185
223
  SET @ids := NULL
186
224
  UPDATE `users` SET money = money + 1 WHERE `users`.`account` IN ('moon', 'wolf') AND ((SELECT @ids := CONCAT_WS(',', `users`.`id`, @ids)))
187
225
  SELECT @ids FROM DUAL
188
226
  COMMIT
227
+
228
+ # pg
229
+ UPDATE 'users' SET money = money + 1 RETURNING id
189
230
  ```
190
231
 
191
232
  ---
192
233
  ### update _(attrs, from: :not_set)_
193
234
 
194
- Updates the attributes of the model from the passed-in hash and saves the record. The difference between this method and [ActiveRecord#update](https://apidock.com/rails/ActiveRecord/Persistence/update) is that it will add extra WHERE conditions to prevent race condition.
235
+ Updates the attributes of the model from the passed-in hash and saves the record. Return true if update successfully, false otherwise. This method can detect race condition and make sure the model is updated only once.
236
+
237
+ The difference between this method and [ActiveRecord#update](https://apidock.com/rails/ActiveRecord/Persistence/update) is that it will add extra WHERE conditions to prevent race condition.
195
238
 
196
239
  #### Parameters
197
240
 
@@ -212,11 +255,30 @@ class Arena < ApplicationRecord
212
255
  end
213
256
  ```
214
257
 
258
+ Let `arena.closed_at` be nil.
259
+
260
+ ```rb
261
+ arena.atomically_close!
262
+ # => true (if success)
263
+ # => false (if race condition occurs)
264
+ ```
265
+
266
+ The return value can be used to prevent race condition and make sure some piece of code is executed once.
267
+
268
+ ```rb
269
+ if arena.atomically_close!
270
+ # Only one request can pass this check and excete the code here.
271
+ # You can send rewards, calculate ranking, or fire background job here.
272
+ # No need to worry about being invoked multiple times.
273
+ do_something
274
+ end
275
+ ```
276
+
215
277
  #### SQL queries
216
278
 
279
+
217
280
  ```sql
218
281
  # arena.atomically_close!
219
- # (let arena.closed_at to be nil)
220
282
  UPDATE `arenas` SET `arenas`.`closed_at` = '2018-11-27 03:44:25', `updated_at` = '2018-11-27 03:44:25'
221
283
  WHERE `arenas`.`id` = 1752 AND `arenas`.`closed_at` IS NULL
222
284
 
@@ -244,12 +306,12 @@ user.money
244
306
  # => 100
245
307
 
246
308
  user.atomically.decrement_unsigned_counters(money: 10)
247
- # => true
309
+ # => true (success)
248
310
  user.reload.money
249
311
  # => 90
250
312
 
251
313
  user.atomically.decrement_unsigned_counters(money: 999)
252
- # => false
314
+ # => false (fail)
253
315
  user.reload.money
254
316
  # => 90
255
317
  ```
@@ -258,7 +320,7 @@ user.reload.money
258
320
 
259
321
  ```sql
260
322
  # user.atomically.decrement_unsigned_counters(money: 140)
261
- UPDATE `users` SET money = money - 140 WHERE `users`.`id` = 1 AND (money > 140)
323
+ UPDATE `users` SET money = money - 140 WHERE `users`.`id` = 1 AND (money >= 140)
262
324
  ```
263
325
 
264
326
  ## Development
data/atomically.gemspec CHANGED
@@ -26,16 +26,25 @@ Gem::Specification.new do |spec|
26
26
  spec.bindir = 'exe'
27
27
  spec.executables = spec.files.grep(%r{^exe/}){|f| File.basename(f) }
28
28
  spec.require_paths = ['lib']
29
+ spec.metadata = {
30
+ 'homepage_uri' => 'https://github.com/khiav223577/atomically',
31
+ 'changelog_uri' => 'https://github.com/khiav223577/atomically/blob/master/CHANGELOG.md',
32
+ 'source_code_uri' => 'https://github.com/khiav223577/atomically',
33
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/atomically',
34
+ 'bug_tracker_uri' => 'https://github.com/khiav223577/atomically/issues',
35
+ }
29
36
 
30
37
  spec.add_development_dependency 'bundler', '>= 1.17', '< 3.x'
31
38
  spec.add_development_dependency 'rake', '~> 12.0'
32
39
  spec.add_development_dependency 'sqlite3', '~> 1.3'
33
40
  spec.add_development_dependency 'minitest', '~> 5.0'
34
41
  spec.add_development_dependency 'mysql2', '>= 0.3'
42
+ spec.add_development_dependency "pg", "~> 0.18"
35
43
  spec.add_development_dependency 'pluck_all', '>= 2.0.3'
36
44
  spec.add_development_dependency 'timecop', '~> 0.9.1'
37
45
 
38
46
  spec.add_dependency 'activerecord', '>= 3'
39
47
  spec.add_dependency 'activerecord-import', '>= 0.27.0'
40
48
  spec.add_dependency 'rails_or', '>= 1.1.8'
49
+ spec.add_dependency 'update_all_scope', '~> 0.1.0'
41
50
  end
data/gemfiles/3.2.gemfile CHANGED
@@ -8,9 +8,10 @@ group :test do
8
8
  when 'postgres' ; gem 'pg', '~> 0.18'
9
9
  end
10
10
  gem 'simplecov'
11
- gem 'codeclimate-test-reporter', '~> 1.0.0'
11
+ gem 'i18n', '< 1.6'
12
12
  gem 'pluck_all', '>= 2.0.3'
13
13
  gem 'timecop', '~> 0.9.1'
14
+ gem 'update_all_scope', '~> 0.1.0'
14
15
  end
15
16
 
16
17
  gemspec path: '../'
data/gemfiles/4.2.gemfile CHANGED
@@ -8,9 +8,10 @@ group :test do
8
8
  when 'postgres' ; gem 'pg', '~> 0.18'
9
9
  end
10
10
  gem 'simplecov'
11
- gem 'codeclimate-test-reporter', '~> 1.0.0'
11
+ gem 'i18n', '< 1.6'
12
12
  gem 'pluck_all', '>= 2.0.3'
13
13
  gem 'timecop', '~> 0.9.1'
14
+ gem 'update_all_scope', '~> 0.1.0'
14
15
  end
15
16
 
16
17
  gemspec path: '../'
data/gemfiles/5.0.gemfile CHANGED
@@ -8,9 +8,10 @@ group :test do
8
8
  when 'postgres' ; gem 'pg', '~> 0.18'
9
9
  end
10
10
  gem 'simplecov'
11
- gem 'codeclimate-test-reporter', '~> 1.0.0'
11
+ gem 'i18n', '< 1.6'
12
12
  gem 'pluck_all', '>= 2.0.3'
13
13
  gem 'timecop', '~> 0.9.1'
14
+ gem 'update_all_scope', '~> 0.1.0'
14
15
  end
15
16
 
16
17
  gemspec path: '../'
data/gemfiles/5.1.gemfile CHANGED
@@ -8,9 +8,10 @@ group :test do
8
8
  when 'postgres' ; gem 'pg', '~> 0.18'
9
9
  end
10
10
  gem 'simplecov'
11
- gem 'codeclimate-test-reporter', '~> 1.0.0'
11
+ gem 'i18n', '< 1.6'
12
12
  gem 'pluck_all', '>= 2.0.3'
13
13
  gem 'timecop', '~> 0.9.1'
14
+ gem 'update_all_scope', '~> 0.1.0'
14
15
  end
15
16
 
16
17
  gemspec path: '../'
data/gemfiles/5.2.gemfile CHANGED
@@ -8,9 +8,10 @@ group :test do
8
8
  when 'postgres' ; gem 'pg', '~> 0.18'
9
9
  end
10
10
  gem 'simplecov'
11
- gem 'codeclimate-test-reporter', '~> 1.0.0'
11
+ gem 'i18n', '< 1.6'
12
12
  gem 'pluck_all', '>= 2.0.3'
13
13
  gem 'timecop', '~> 0.9.1'
14
+ gem 'update_all_scope', '~> 0.1.0'
14
15
  end
15
16
 
16
17
  gemspec path: '../'
@@ -0,0 +1,16 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 6.0.0'
4
+
5
+ group :test do
6
+ case ENV['DB']
7
+ when 'mysql' ; gem 'mysql2', '0.5.1'
8
+ when 'postgres' ; gem 'pg', '~> 0.18'
9
+ end
10
+ gem 'simplecov'
11
+ gem 'pluck_all', '>= 2.0.4'
12
+ gem 'timecop', '~> 0.9.1'
13
+ gem 'update_all_scope', '~> 0.1.0'
14
+ end
15
+
16
+ gemspec path: '../'
@@ -0,0 +1,16 @@
1
+
2
+ class Atomically::AdapterCheckService
3
+ def initialize(klass)
4
+ @klass = klass
5
+ end
6
+
7
+ def pg?
8
+ return false if not defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
9
+ return @klass.connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
10
+ end
11
+
12
+ def mysql?
13
+ return false if not defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
14
+ return @klass.connection.is_a?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Atomically::OnDuplicateSqlService
4
+ def initialize(klass, columns)
5
+ @klass = klass
6
+ @columns = columns
7
+ end
8
+
9
+ def mysql_quote_columns_for_plus
10
+ return @columns.map do |column|
11
+ quoted_column = quote_column(column)
12
+ next "#{quoted_column} = #{quoted_column} + VALUES(#{quoted_column})"
13
+ end
14
+ end
15
+
16
+ def pg_quote_columns_for_plus
17
+ return @columns.map do |column|
18
+ quoted_column = quote_column(column)
19
+ next "#{quoted_column} = #{@klass.quoted_table_name}.#{quoted_column} + excluded.#{quoted_column}"
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def quote_column(column)
26
+ @klass.connection.quote_column_name(column)
27
+ end
28
+ end
@@ -2,20 +2,24 @@
2
2
 
3
3
  require 'activerecord-import'
4
4
  require 'rails_or'
5
- require 'atomically/update_all_scope'
5
+ require 'update_all_scope'
6
+ require 'atomically/on_duplicate_sql_service'
7
+ require 'atomically/adapter_check_service'
6
8
  require 'atomically/patches/clear_attribute_changes' if not ActiveModel::Dirty.method_defined?(:clear_attribute_changes) and not ActiveModel::Dirty.private_method_defined?(:clear_attribute_changes)
7
9
  require 'atomically/patches/none' if not ActiveRecord::Base.respond_to?(:none)
8
10
  require 'atomically/patches/from' if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new('4.0.0')
9
11
 
10
12
  class Atomically::QueryService
13
+ DEFAULT_CONFLICT_TARGETS = [:id].freeze
14
+
11
15
  def initialize(klass, relation: nil, model: nil)
12
16
  @klass = klass
13
17
  @relation = relation || @klass
14
18
  @model = model
15
19
  end
16
20
 
17
- def create_or_plus(columns, data, update_columns)
18
- @klass.import(columns, data, on_duplicate_key_update: on_duplicate_key_plus_sql(update_columns))
21
+ def create_or_plus(columns, data, update_columns, conflict_target: DEFAULT_CONFLICT_TARGETS)
22
+ @klass.import(columns, data, on_duplicate_key_update: on_duplicate_key_plus_sql(update_columns, conflict_target))
19
23
  end
20
24
 
21
25
  def pay_all(hash, update_columns, primary_key: :id) # { id => pay_count }
@@ -30,8 +34,13 @@ class Atomically::QueryService
30
34
  end
31
35
 
32
36
  raw_when_sql = hash.map{|id, pay_count| "WHEN #{sanitize(id)} THEN #{sanitize(-pay_count)}" }.join("\n")
37
+ no_var_in_sql = true if update_columns.size == 1 or db_is_pg?
33
38
  update_sqls = update_columns.map.with_index do |column, idx|
34
- value = idx == 0 ? "(@change := \nCASE #{quote_column(primary_key)}\n#{raw_when_sql}\nEND)" : '@change'
39
+ if no_var_in_sql
40
+ value = "(\nCASE #{quote_column(primary_key)}\n#{raw_when_sql}\nEND)"
41
+ else
42
+ value = idx == 0 ? "(@change := \nCASE #{quote_column(primary_key)}\n#{raw_when_sql}\nEND)" : '@change'
43
+ end
35
44
  next "#{column} = #{column} + #{value}"
36
45
  end
37
46
 
@@ -62,20 +71,43 @@ class Atomically::QueryService
62
71
  end
63
72
 
64
73
  def update_all_and_get_ids(*args)
74
+ if db_is_pg?
75
+ scope = UpdateAllScope::UpdateAllScope.new(model: @model, relation: @relation.where(''))
76
+ scope.update(*args)
77
+ return @klass.connection.execute("#{scope.to_sql} RETURNING id", "#{@klass} Update All").map{|s| s['id'].to_i }
78
+ end
79
+
65
80
  ids = nil
66
- id_column = "#{@klass.quoted_table_name}.#{quote_column(:id)}"
81
+ id_column = quote_column_with_table(:id)
67
82
  @klass.transaction do
68
83
  @relation.connection.execute('SET @ids := NULL')
69
84
  @relation.where("(SELECT @ids := CONCAT_WS(',', #{id_column}, @ids))").update_all(*args) # 撈出有真的被更新的 id,用逗號串在一起
70
- ids = @klass.from(nil).pluck('@ids').first
85
+ ids = @klass.from(nil).pluck(Arel.sql('@ids')).first
71
86
  end
72
87
  return ids.try{|s| s.split(',').map(&:to_i).uniq.sort } || [] # 將 id 從字串取出來 @id 的格式範例: '1,4,12'
73
88
  end
74
89
 
75
90
  private
76
91
 
77
- def on_duplicate_key_plus_sql(columns)
78
- columns.lazy.map(&method(:quote_column)).map{|s| "#{s} = #{s} + VALUES(#{s})" }.force.join(', ')
92
+ def db_is_pg?
93
+ Atomically::AdapterCheckService.new(@klass).pg?
94
+ end
95
+
96
+ def db_is_mysql?
97
+ Atomically::AdapterCheckService.new(@klass).mysql?
98
+ end
99
+
100
+ def on_duplicate_key_plus_sql(columns, conflict_target)
101
+ service = Atomically::OnDuplicateSqlService.new(@klass, columns)
102
+ return service.mysql_quote_columns_for_plus.join(', ') if db_is_mysql?
103
+ return {
104
+ conflict_target: conflict_target,
105
+ columns: service.pg_quote_columns_for_plus.join(', ')
106
+ }
107
+ end
108
+
109
+ def quote_column_with_table(column)
110
+ "#{@klass.quoted_table_name}.#{quote_column(column)}"
79
111
  end
80
112
 
81
113
  def quote_column(column)
@@ -103,7 +135,7 @@ class Atomically::QueryService
103
135
 
104
136
  def open_update_all_scope(&block)
105
137
  return 0 if @model == nil
106
- scope = UpdateAllScope.new(model: @model)
138
+ scope = UpdateAllScope::UpdateAllScope.new(model: @model)
107
139
  scope.instance_exec(&block)
108
140
  return scope.do_query!
109
141
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Atomically
4
- VERSION = '1.0.6'
4
+ VERSION = '1.1.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomically
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - khiav reoy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-01-29 00:00:00.000000000 Z
11
+ date: 2019-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -86,6 +86,20 @@ dependencies:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
88
  version: '0.3'
89
+ - !ruby/object:Gem::Dependency
90
+ name: pg
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.18'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.18'
89
103
  - !ruby/object:Gem::Dependency
90
104
  name: pluck_all
91
105
  requirement: !ruby/object:Gem::Requirement
@@ -156,6 +170,20 @@ dependencies:
156
170
  - - ">="
157
171
  - !ruby/object:Gem::Version
158
172
  version: 1.1.8
173
+ - !ruby/object:Gem::Dependency
174
+ name: update_all_scope
175
+ requirement: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: 0.1.0
180
+ type: :runtime
181
+ prerelease: false
182
+ version_requirements: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: 0.1.0
159
187
  description: Adds commonly useful atomic SQL statements to ActiveRecord to avoid race
160
188
  condition.
161
189
  email:
@@ -169,7 +197,6 @@ files:
169
197
  - ".travis.yml"
170
198
  - CHANGELOG.md
171
199
  - CODE_OF_CONDUCT.md
172
- - Gemfile.gemfile
173
200
  - LICENSE
174
201
  - LICENSE.txt
175
202
  - README.md
@@ -182,18 +209,25 @@ files:
182
209
  - gemfiles/5.0.gemfile
183
210
  - gemfiles/5.1.gemfile
184
211
  - gemfiles/5.2.gemfile
212
+ - gemfiles/6.0.gemfile
185
213
  - lib/atomically.rb
186
214
  - lib/atomically/active_record/extension.rb
215
+ - lib/atomically/adapter_check_service.rb
216
+ - lib/atomically/on_duplicate_sql_service.rb
187
217
  - lib/atomically/patches/clear_attribute_changes.rb
188
218
  - lib/atomically/patches/from.rb
189
219
  - lib/atomically/patches/none.rb
190
220
  - lib/atomically/query_service.rb
191
- - lib/atomically/update_all_scope.rb
192
221
  - lib/atomically/version.rb
193
222
  homepage: https://github.com/khiav223577/atomically
194
223
  licenses:
195
224
  - MIT
196
- metadata: {}
225
+ metadata:
226
+ homepage_uri: https://github.com/khiav223577/atomically
227
+ changelog_uri: https://github.com/khiav223577/atomically/blob/master/CHANGELOG.md
228
+ source_code_uri: https://github.com/khiav223577/atomically
229
+ documentation_uri: https://www.rubydoc.info/gems/atomically
230
+ bug_tracker_uri: https://github.com/khiav223577/atomically/issues
197
231
  post_install_message:
198
232
  rdoc_options: []
199
233
  require_paths:
@@ -210,7 +244,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
210
244
  version: '0'
211
245
  requirements: []
212
246
  rubyforge_project:
213
- rubygems_version: 2.7.6
247
+ rubygems_version: 2.6.14
214
248
  signing_key:
215
249
  specification_version: 4
216
250
  summary: Adds commonly useful atomic SQL statements to ActiveRecord to avoid race
data/Gemfile.gemfile DELETED
@@ -1,12 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in rails_or.gemspec
4
-
5
- gem 'activerecord', '~> 5.0'
6
-
7
- group :test do
8
- gem 'simplecov'
9
- gem 'codeclimate-test-reporter', '~> 1.0.0'
10
- end
11
-
12
- gemspec path: '../'
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class UpdateAllScope
4
- def initialize(model: nil, relation: nil)
5
- @queries = []
6
- @relation = relation || model.class.where(id: model.id)
7
- end
8
-
9
- def where(*args)
10
- @relation = @relation.where(*args)
11
- return self
12
- end
13
-
14
- def update(query, *binding_values)
15
- args = binding_values.size > 0 ? [[query, *binding_values]] : [query]
16
- @queries << klass.send(:sanitize_sql_for_assignment, *args)
17
- return self
18
- end
19
-
20
- def do_query!
21
- return 0 if @queries.empty?
22
- return @relation.update_all(@queries.join(','))
23
- end
24
-
25
- def klass
26
- @relation.klass
27
- end
28
- end