atomically 1.0.6 → 1.1.0
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 +5 -5
- data/.travis.yml +13 -2
- data/CHANGELOG.md +3 -0
- data/README.md +87 -25
- data/atomically.gemspec +9 -0
- data/gemfiles/3.2.gemfile +2 -1
- data/gemfiles/4.2.gemfile +2 -1
- data/gemfiles/5.0.gemfile +2 -1
- data/gemfiles/5.1.gemfile +2 -1
- data/gemfiles/5.2.gemfile +2 -1
- data/gemfiles/6.0.gemfile +16 -0
- data/lib/atomically/adapter_check_service.rb +16 -0
- data/lib/atomically/on_duplicate_sql_service.rb +28 -0
- data/lib/atomically/query_service.rb +41 -9
- data/lib/atomically/version.rb +1 -1
- metadata +40 -6
- data/Gemfile.gemfile +0 -12
- data/lib/atomically/update_all_scope.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f11789d3b23462ec0a597434cbd71ad693684fb2
|
4
|
+
data.tar.gz: '0365791ed9e8a5f1b0d035a0db850cc8a5619983'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
- 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.
|
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
|
-
|
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
|
-
|
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
|
-

|
78
100
|
|
79
101
|
after
|
80
102
|
|
81
|
-

|
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
|
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
|
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)
|
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: [
|
145
|
-
# => 2
|
178
|
+
User.where(id: [5, 6]).atomically.update_all(2, name: '')
|
179
|
+
# => 2 (success)
|
146
180
|
|
147
|
-
User.where(id: [
|
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: [
|
155
|
-
UPDATE `users` SET `users`.`name` = '' WHERE `users`.`id` IN (
|
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 (
|
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
|
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.
|
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
|
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 '
|
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 '
|
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 '
|
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 '
|
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 '
|
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 '
|
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
|
-
|
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 =
|
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
|
78
|
-
|
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
|
data/lib/atomically/version.rb
CHANGED
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
|
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-
|
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.
|
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,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
|