atomically 1.0.4 → 1.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +36 -35
- data/.travis.yml +39 -8
- data/CHANGELOG.md +30 -1
- data/README.md +138 -34
- data/atomically.gemspec +12 -3
- data/gemfiles/3.2.gemfile +5 -5
- data/gemfiles/4.2.gemfile +5 -5
- data/gemfiles/5.0.gemfile +5 -5
- data/gemfiles/5.1.gemfile +5 -5
- data/gemfiles/5.2.gemfile +5 -5
- data/gemfiles/6.0.gemfile +15 -0
- data/lib/atomically/adapter_check_service.rb +30 -0
- data/lib/atomically/on_duplicate_sql_service.rb +28 -0
- data/lib/atomically/query_service.rb +63 -12
- data/lib/atomically/version.rb +1 -1
- metadata +54 -13
- data/Gemfile.gemfile +0 -12
- data/lib/atomically/update_all_scope.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c08c4d945a90102861c424a610fab072472cf17e31497936987ec842db6be426
|
4
|
+
data.tar.gz: c8c2b834fb5a652bdf3f31eb16fd0996ff2b3762613a31be57b038a7afcf9bd0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d4ab1744413db5f7c14d3982b94289c836c7d007206fdc4f7cc40c2398ce165b97b65d95d12a095a915724e9fff3a59f2f35957f8fb6f72ccd7557ef12255fb
|
7
|
+
data.tar.gz: 8016f81e284201892faa8f8cad82302c8b674d50fc1181906b970d7e73533a886de33be9b22a61df8d180cf667292560ad28e595ece0893d45d63aeb798f55fb
|
data/.rubocop.yml
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-performance
|
3
|
+
- rubocop-rails
|
4
|
+
|
1
5
|
AllCops:
|
2
6
|
DisabledByDefault: true
|
3
7
|
Exclude: []
|
@@ -73,10 +77,6 @@ Layout/EndAlignment:
|
|
73
77
|
Description: 'Align ends correctly.'
|
74
78
|
Enabled: true
|
75
79
|
|
76
|
-
Lint/EndInMethod:
|
77
|
-
Description: 'END blocks should not be placed inside method definitions.'
|
78
|
-
Enabled: true
|
79
|
-
|
80
80
|
Lint/EnsureReturn:
|
81
81
|
Description: 'Do not use return in an ensure block.'
|
82
82
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-return-ensure'
|
@@ -90,7 +90,7 @@ Lint/FormatParameterMismatch:
|
|
90
90
|
Description: 'The number of parameters to format/sprint must match the fields.'
|
91
91
|
Enabled: true
|
92
92
|
|
93
|
-
Lint/
|
93
|
+
Lint/SuppressedException:
|
94
94
|
Description: "Don't suppress exception."
|
95
95
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions'
|
96
96
|
Enabled: true
|
@@ -139,7 +139,7 @@ Lint/ShadowingOuterLocalVariable:
|
|
139
139
|
for block arguments or block local variables.
|
140
140
|
Enabled: true
|
141
141
|
|
142
|
-
Lint/
|
142
|
+
Lint/RedundantStringCoercion:
|
143
143
|
Description: 'Checks for Object#to_s usage in string interpolation.'
|
144
144
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s'
|
145
145
|
Enabled: true
|
@@ -148,7 +148,7 @@ Lint/UnderscorePrefixedVariableName:
|
|
148
148
|
Description: 'Do not use prefix `_` for a variable that is used.'
|
149
149
|
Enabled: true
|
150
150
|
|
151
|
-
Lint/
|
151
|
+
Lint/RedundantCopDisableDirective:
|
152
152
|
Description: >-
|
153
153
|
Checks for rubocop:disable comments that can be removed.
|
154
154
|
Note: this cop is not disabled when disabling all cops.
|
@@ -221,7 +221,7 @@ Metrics/CyclomaticComplexity:
|
|
221
221
|
of test cases needed to validate a method.
|
222
222
|
Enabled: true
|
223
223
|
|
224
|
-
|
224
|
+
Layout/LineLength:
|
225
225
|
Description: 'Limit lines to 120 characters.'
|
226
226
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits'
|
227
227
|
Max: 120
|
@@ -285,7 +285,7 @@ Performance/ReverseEach:
|
|
285
285
|
Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code'
|
286
286
|
Enabled: true
|
287
287
|
|
288
|
-
|
288
|
+
Style/Sample:
|
289
289
|
Description: >-
|
290
290
|
Use `sample` instead of `shuffle.first`,
|
291
291
|
`shuffle.last`, and `shuffle[Fixnum]`.
|
@@ -353,7 +353,7 @@ Rails/TimeZone:
|
|
353
353
|
Description: 'Checks the correct usage of time zone aware methods.'
|
354
354
|
StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time'
|
355
355
|
Reference: 'http://danilenko.org/2012/7/6/rails_timezones'
|
356
|
-
Enabled:
|
356
|
+
Enabled: false
|
357
357
|
|
358
358
|
Rails/Validation:
|
359
359
|
Description: 'Use validates :attribute, hash of validations.'
|
@@ -375,20 +375,21 @@ Style/Alias:
|
|
375
375
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method'
|
376
376
|
Enabled: true
|
377
377
|
|
378
|
-
Layout/
|
378
|
+
Layout/ArrayAlignment:
|
379
379
|
Description: >-
|
380
380
|
Align the elements of an array literal if they span more than
|
381
381
|
one line.
|
382
382
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays'
|
383
383
|
Enabled: true
|
384
384
|
|
385
|
-
Layout/
|
385
|
+
Layout/HashAlignment:
|
386
386
|
Description: >-
|
387
387
|
Align the elements of a hash literal if they span more than
|
388
388
|
one line.
|
389
|
+
EnforcedHashRocketStyle: table
|
389
390
|
Enabled: true
|
390
391
|
|
391
|
-
Layout/
|
392
|
+
Layout/ParameterAlignment:
|
392
393
|
Description: >-
|
393
394
|
Align the parameters of a method call if they span more
|
394
395
|
than one line.
|
@@ -447,10 +448,6 @@ Style/BlockDelimiters:
|
|
447
448
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks'
|
448
449
|
Enabled: true
|
449
450
|
|
450
|
-
Style/BracesAroundHashParameters:
|
451
|
-
Description: 'Enforce braces style around hash parameters.'
|
452
|
-
Enabled: true
|
453
|
-
|
454
451
|
Style/CaseEquality:
|
455
452
|
Description: 'Avoid explicit use of the case equality operator(===).'
|
456
453
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality'
|
@@ -595,7 +592,7 @@ Style/EndBlock:
|
|
595
592
|
Layout/EndOfLine:
|
596
593
|
Description: 'Use Unix-style line endings.'
|
597
594
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf'
|
598
|
-
Enabled:
|
595
|
+
Enabled: false
|
599
596
|
|
600
597
|
Style/EvenOdd:
|
601
598
|
Description: 'Favor the use of Fixnum#even? && Fixnum#odd?'
|
@@ -616,14 +613,14 @@ Layout/InitialIndentation:
|
|
616
613
|
Checks the indentation of the first non-blank non-comment line in a file.
|
617
614
|
Enabled: true
|
618
615
|
|
619
|
-
Layout/
|
616
|
+
Layout/FirstArgumentIndentation:
|
620
617
|
Description: 'Checks the indentation of the first parameter in a method call.'
|
621
618
|
Enabled: true
|
622
619
|
|
623
|
-
|
620
|
+
Lint/FlipFlop:
|
624
621
|
Description: 'Checks for flip flops'
|
625
622
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops'
|
626
|
-
Enabled:
|
623
|
+
Enabled: false
|
627
624
|
|
628
625
|
Style/For:
|
629
626
|
Description: 'Checks use of for or each in multiline loops.'
|
@@ -633,9 +630,12 @@ Style/For:
|
|
633
630
|
Style/FormatString:
|
634
631
|
Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.'
|
635
632
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf'
|
636
|
-
EnforcedStyle:
|
633
|
+
EnforcedStyle: percent
|
637
634
|
Enabled: true
|
638
635
|
|
636
|
+
Style/FormatStringToken:
|
637
|
+
Enabled: false
|
638
|
+
|
639
639
|
Style/GlobalVars:
|
640
640
|
Description: 'Do not introduce global variables.'
|
641
641
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars'
|
@@ -675,14 +675,14 @@ Layout/IndentationWidth:
|
|
675
675
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
|
676
676
|
Enabled: true
|
677
677
|
|
678
|
-
Layout/
|
678
|
+
Layout/FirstArrayElementIndentation:
|
679
679
|
Description: >-
|
680
680
|
Checks the indentation of the first element in an array
|
681
681
|
literal.
|
682
682
|
EnforcedStyle: consistent
|
683
683
|
Enabled: true
|
684
684
|
|
685
|
-
Layout/
|
685
|
+
Layout/FirstHashElementIndentation:
|
686
686
|
Description: 'Checks the indentation of the first key in a hash literal.'
|
687
687
|
EnforcedStyle: consistent
|
688
688
|
Enabled: true
|
@@ -787,7 +787,8 @@ Style/Next:
|
|
787
787
|
Style/NilComparison:
|
788
788
|
Description: 'Prefer x.nil? to x == nil.'
|
789
789
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
|
790
|
-
|
790
|
+
EnforcedStyle: comparison
|
791
|
+
Enabled: true
|
791
792
|
|
792
793
|
Style/NonNilCheck:
|
793
794
|
# With `IncludeSemanticChanges` set to `true`, this cop reports offenses for
|
@@ -799,7 +800,7 @@ Style/NonNilCheck:
|
|
799
800
|
Description: 'Checks for redundant nil checks.'
|
800
801
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks'
|
801
802
|
IncludeSemanticChanges: true
|
802
|
-
Enabled:
|
803
|
+
Enabled: false
|
803
804
|
|
804
805
|
Style/Not:
|
805
806
|
Description: 'Use ! instead of not.'
|
@@ -860,7 +861,7 @@ Style/PercentQLiterals:
|
|
860
861
|
Style/PerlBackrefs:
|
861
862
|
Description: 'Avoid Perl-style regex back references.'
|
862
863
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers'
|
863
|
-
Enabled:
|
864
|
+
Enabled: false
|
864
865
|
|
865
866
|
Naming/PredicateName:
|
866
867
|
Description: 'Check the names of predicate methods.'
|
@@ -903,7 +904,7 @@ Style/RegexpLiteral:
|
|
903
904
|
Description: 'Use / or %r around regular expressions.'
|
904
905
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r'
|
905
906
|
EnforcedStyle: slashes
|
906
|
-
AllowInnerSlashes:
|
907
|
+
AllowInnerSlashes: false
|
907
908
|
Enabled: true
|
908
909
|
|
909
910
|
Layout/RescueEnsureAlignment:
|
@@ -938,11 +939,11 @@ Style/SingleLineBlockParams:
|
|
938
939
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks'
|
939
940
|
Methods:
|
940
941
|
- reduce:
|
941
|
-
-
|
942
|
-
-
|
942
|
+
- sum
|
943
|
+
- v
|
943
944
|
- inject:
|
944
|
-
-
|
945
|
-
-
|
945
|
+
- sum
|
946
|
+
- v
|
946
947
|
Enabled: true
|
947
948
|
|
948
949
|
Style/SingleLineMethods:
|
@@ -1121,7 +1122,7 @@ Layout/Tab:
|
|
1121
1122
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
|
1122
1123
|
Enabled: true
|
1123
1124
|
|
1124
|
-
Layout/
|
1125
|
+
Layout/TrailingEmptyLines:
|
1125
1126
|
Description: 'Checks trailing blank lines and final newline.'
|
1126
1127
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof'
|
1127
1128
|
Enabled: true
|
@@ -1176,11 +1177,11 @@ Style/UnlessElse:
|
|
1176
1177
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless'
|
1177
1178
|
Enabled: true
|
1178
1179
|
|
1179
|
-
Style/
|
1180
|
+
Style/RedundantCapitalW:
|
1180
1181
|
Description: 'Checks for %W when interpolation is not needed.'
|
1181
1182
|
Enabled: true
|
1182
1183
|
|
1183
|
-
Style/
|
1184
|
+
Style/RedundantPercentQ:
|
1184
1185
|
Description: 'Checks for %q/%Q when single quotes or double quotes would do.'
|
1185
1186
|
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q'
|
1186
1187
|
Enabled: true
|
data/.travis.yml
CHANGED
@@ -1,32 +1,63 @@
|
|
1
1
|
sudo: false
|
2
|
-
env:
|
3
|
-
global:
|
4
|
-
- CC_TEST_REPORTER_ID=12e1facab2e8910c9b9d6b9e6870c5544a5c44a1bef25cc6638fd132aa4af6b4
|
5
2
|
language: ruby
|
6
3
|
rvm:
|
7
4
|
- 2.2
|
8
|
-
- 2.
|
5
|
+
- 2.6
|
6
|
+
- 2.7
|
7
|
+
services:
|
8
|
+
- mysql
|
9
|
+
addons:
|
10
|
+
postgresql: "9.6"
|
9
11
|
env:
|
10
|
-
|
12
|
+
global:
|
13
|
+
- CC_TEST_REPORTER_ID=12e1facab2e8910c9b9d6b9e6870c5544a5c44a1bef25cc6638fd132aa4af6b4
|
14
|
+
matrix:
|
15
|
+
- DB=mysql
|
16
|
+
- DB=pg
|
11
17
|
gemfile:
|
12
18
|
- gemfiles/3.2.gemfile
|
13
19
|
- gemfiles/4.2.gemfile
|
14
20
|
- gemfiles/5.0.gemfile
|
15
21
|
- gemfiles/5.1.gemfile
|
16
22
|
- gemfiles/5.2.gemfile
|
23
|
+
- gemfiles/6.0.gemfile
|
17
24
|
matrix:
|
25
|
+
include:
|
26
|
+
- env: DB=makara_mysql
|
27
|
+
gemfile: gemfiles/6.0.gemfile
|
28
|
+
rvm: 2.6
|
29
|
+
- env: DB=makara_pg
|
30
|
+
gemfile: gemfiles/6.0.gemfile
|
31
|
+
rvm: 2.6
|
32
|
+
- env: DB=makara_mysql
|
33
|
+
gemfile: gemfiles/6.0.gemfile
|
34
|
+
rvm: 2.7
|
35
|
+
- env: DB=makara_pg
|
36
|
+
gemfile: gemfiles/6.0.gemfile
|
37
|
+
rvm: 2.7
|
18
38
|
exclude:
|
19
39
|
- gemfile: gemfiles/3.2.gemfile
|
20
|
-
rvm: 2.
|
40
|
+
rvm: 2.6
|
41
|
+
- gemfile: gemfiles/3.2.gemfile
|
42
|
+
rvm: 2.7
|
43
|
+
- gemfile: gemfiles/4.2.gemfile
|
44
|
+
rvm: 2.7
|
45
|
+
- gemfile: gemfiles/6.0.gemfile
|
46
|
+
rvm: 2.2
|
21
47
|
before_install:
|
22
|
-
-
|
23
|
-
|
48
|
+
- if `ruby -e 'exit(RUBY_VERSION.to_f < 2.7)'`; then
|
49
|
+
gem i rubygems-update -v '< 3' && update_rubygems;
|
50
|
+
gem install bundler -v '< 2';
|
51
|
+
fi
|
24
52
|
- gem --version
|
25
53
|
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
26
54
|
- chmod +x ./cc-test-reporter
|
27
55
|
- ./cc-test-reporter before-build
|
28
56
|
before_script:
|
57
|
+
- mysql -V
|
29
58
|
- mysql -u root -e 'CREATE DATABASE travis_ci_test;'
|
59
|
+
- psql -c "SELECT version();"
|
60
|
+
- psql -c 'create database travis_ci_test;' -U postgres
|
30
61
|
script:
|
31
62
|
- bundle exec rake test
|
32
63
|
after_script:
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,37 @@
|
|
1
1
|
## Change Log
|
2
2
|
|
3
|
+
### [v1.1.2](https://github.com/khiav223577/atomically/compare/v1.1.1...v1.1.2) 2020/09/01
|
4
|
+
- [#19](https://github.com/khiav223577/atomically/pull/19) Fix: changed attributes are not updated when calling `atomically.update` (@khiav223577)
|
5
|
+
- [#18](https://github.com/khiav223577/atomically/pull/18) Support Ruby 2.7 (@khiav223577)
|
6
|
+
- [#17](https://github.com/khiav223577/atomically/pull/17) specify gem versions by ENV['DB'] (@khiav223577)
|
7
|
+
|
8
|
+
### [v1.1.1](https://github.com/khiav223577/atomically/compare/v1.1.0...v1.1.1) 2019/11/01
|
9
|
+
- [#16](https://github.com/khiav223577/atomically/pull/16) Fix: `create_or_plus` is broken when using `makara` adapter (@khiav223577)
|
10
|
+
|
11
|
+
### [v1.1.0](https://github.com/khiav223577/atomically/compare/v1.0.6...v1.1.0) 2019/10/23
|
12
|
+
- [#12](https://github.com/khiav223577/atomically/pull/12) Support PostgreSQL (@khiav223577)
|
13
|
+
- [#14](https://github.com/khiav223577/atomically/pull/14) Support Rails 6.0 (@khiav223577)
|
14
|
+
- [#13](https://github.com/khiav223577/atomically/pull/13) Remove deprecated codeclimate-test-reporter gem (@khiav223577)
|
15
|
+
- [#11](https://github.com/khiav223577/atomically/pull/11) Fix: Non-attribute arguments will be disallowed in Rails 6.0 (@khiav223577)
|
16
|
+
|
17
|
+
### [v1.0.6](https://github.com/khiav223577/atomically/compare/v1.0.5...v1.0.6) 2019/01/28
|
18
|
+
- [#10](https://github.com/khiav223577/atomically/pull/10) `decrement_unsigned_counters` should be able to decrement the field to zero (@khiav223577)
|
19
|
+
|
20
|
+
### [v1.0.5](https://github.com/khiav223577/atomically/compare/v1.0.4...v1.0.5) 2019/01/28
|
21
|
+
- [#9](https://github.com/khiav223577/atomically/pull/9) Implement `decrement_unsigned_counters` (@khiav223577)
|
22
|
+
- [#8](https://github.com/khiav223577/atomically/pull/8) Fix: broken test cases after bundler 2.0 was released (@khiav223577)
|
23
|
+
|
24
|
+
### [v1.0.4](https://github.com/khiav223577/atomically/compare/v1.0.3...v1.0.4) 2018/12/21
|
25
|
+
- [#7](https://github.com/khiav223577/atomically/pull/7) Implement `update_all_and_get_ids` (@khiav223577)
|
26
|
+
- [#6](https://github.com/khiav223577/atomically/pull/6) Add warning (@kakas)
|
27
|
+
- [#5](https://github.com/khiav223577/atomically/pull/5) fix README `pay_all` description (@kakas)
|
28
|
+
|
29
|
+
### [v1.0.3](https://github.com/khiav223577/atomically/compare/v1.0.2...v1.0.3) 2018/11/28
|
30
|
+
- [#4](https://github.com/khiav223577/atomically/pull/4) Implement `update_all` (@khiav223577)
|
31
|
+
|
3
32
|
### [v1.0.2](https://github.com/khiav223577/atomically/compare/v1.0.1...v1.0.2) 2018/11/27
|
4
33
|
- [#3](https://github.com/khiav223577/atomically/pull/3) Implement `update` (@khiav223577)
|
5
34
|
|
6
|
-
### v1.0.1 2018/11/22
|
35
|
+
### [v1.0.1](https://github.com/khiav223577/atomically/compare/v1.0.0...v1.0.1) 2018/11/22
|
7
36
|
- [#2](https://github.com/khiav223577/atomically/pull/2) Implement `pay_all` (@khiav223577)
|
8
37
|
- [#1](https://github.com/khiav223577/atomically/pull/1) Implement `create_or_plus` (@khiav223577)
|
data/README.md
CHANGED
@@ -8,17 +8,25 @@
|
|
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
|
14
|
+
- Ruby 2.2 ~ 2.7
|
15
|
+
- Rails 3.2, 4.2, 5.0, 5.1, 5.2, 6.0
|
16
|
+
- MySQL, PostgreSQL
|
12
17
|
|
13
18
|
## Table of contents
|
14
19
|
|
15
20
|
1. [Installation](#installation)
|
16
21
|
2. [Methods](#methods)
|
17
|
-
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
+
- Relation Methods
|
23
|
+
- [create_or_plus](#create_or_plus-columns-values-on_duplicate_update_columns-conflict_target)
|
24
|
+
- [pay_all](#pay_all-hash-update_columns-primary_key-id)
|
25
|
+
- [update_all](#update_all-expected_number-updates)
|
26
|
+
- [update_all_and_get_ids](#update_all_and_get_ids-updates)
|
27
|
+
- Model Methods
|
28
|
+
- [update](#update-attrs-from-not_set)
|
29
|
+
- [decrement_unsigned_counters](#decrement_unsigned_counters-counters)
|
22
30
|
3. [Development](#development)
|
23
31
|
4. [Contributing](#contributing)
|
24
32
|
5. [License](#license)
|
@@ -43,19 +51,35 @@ Or install it yourself as:
|
|
43
51
|
|
44
52
|
Note: ActiveRecord validations and callbacks will **NOT** be triggered when calling below methods.
|
45
53
|
|
46
|
-
### create_or_plus _(columns, values, on_duplicate_update_columns)_
|
54
|
+
### create_or_plus _(columns, values, on_duplicate_update_columns, conflict_target:)_
|
47
55
|
|
48
56
|
Import an array of records. When key is duplicate, plus the old value with new value.
|
49
|
-
It is useful to add `items` to `user` when `user_items` may not exist.
|
57
|
+
It is useful to add `items` to `user` when `user_items` may not exist. (Let `User` and `Item` are many-to-many relationship.)
|
50
58
|
|
51
59
|
#### Parameters
|
52
60
|
|
53
61
|
- First two args (`columns`, `values`) are the same with the [import](https://github.com/zdennis/activerecord-import#columns-and-arrays) method.
|
54
62
|
- `on_duplicate_update_columns` - The column that will be updated on duplicate.
|
63
|
+
- `conflict_target` - Needed only in pg. Specifies which columns have unique index.
|
55
64
|
|
56
65
|
#### Example
|
57
66
|
|
58
67
|
```rb
|
68
|
+
class User < ApplicationRecord
|
69
|
+
has_many :user_items
|
70
|
+
has_many :items, through: :user_items
|
71
|
+
end
|
72
|
+
|
73
|
+
class UserItem < ApplicationRecord
|
74
|
+
belongs_to :user
|
75
|
+
belongs_to :item
|
76
|
+
end
|
77
|
+
|
78
|
+
class Item < ApplicationRecord
|
79
|
+
has_many :user_items
|
80
|
+
has_many :users, through: :user_items
|
81
|
+
end
|
82
|
+
|
59
83
|
user = User.find(2)
|
60
84
|
item1 = Item.find(1)
|
61
85
|
item2 = Item.find(2)
|
@@ -64,44 +88,59 @@ item2 = Item.find(2)
|
|
64
88
|
```rb
|
65
89
|
columns = [:user_id, :item_id, :quantity]
|
66
90
|
values = [[user.id, item1.id, 3], [user.id, item2.id, 2]]
|
67
|
-
on_duplicate_update_columns = [:quantity]
|
68
91
|
|
69
|
-
|
92
|
+
# mysql
|
93
|
+
UserItem.atomically.create_or_plus(columns, values, [:quantity])
|
94
|
+
|
95
|
+
# pg
|
96
|
+
UserItem.atomically.create_or_plus(columns, values, [:quantity], conflict_target: [:user_id, :item_id])
|
70
97
|
```
|
71
98
|
|
72
99
|
before
|
73
100
|
|
74
|
-
![before](https://user-images.githubusercontent.com/4011729/
|
101
|
+
![before](https://user-images.githubusercontent.com/4011729/67365648-95e89480-f5a4-11e9-8147-279385c6f442.png)
|
75
102
|
|
76
103
|
after
|
77
104
|
|
78
|
-
![
|
105
|
+
![after](https://user-images.githubusercontent.com/4011729/67365653-97b25800-f5a4-11e9-8314-8e6ff8d2cd61.png)
|
106
|
+
|
79
107
|
|
80
108
|
#### SQL queries
|
81
109
|
|
82
110
|
```sql
|
111
|
+
# mysql
|
83
112
|
INSERT INTO `user_items` (`user_id`,`item_id`,`quantity`,`created_at`,`updated_at`) VALUES
|
84
113
|
(2,1,3,'2018-11-27 03:44:25','2018-11-27 03:44:25'),
|
85
114
|
(2,2,2,'2018-11-27 03:44:25','2018-11-27 03:44:25')
|
86
|
-
ON DUPLICATE KEY UPDATE
|
115
|
+
ON DUPLICATE KEY UPDATE
|
116
|
+
`quantity` = `quantity` + VALUES(`quantity`)
|
117
|
+
|
118
|
+
# pg
|
119
|
+
INSERT INTO "user_items" ("user_id","item_id","quantity","created_at","updated_at") VALUES
|
120
|
+
(2,1,3,'2018-11-27 03:44:25.847909','2018-11-27 03:44:25.847909'),
|
121
|
+
(2,2,2,'2018-11-27 03:44:25.847909','2018-11-27 03:44:25.847909')
|
122
|
+
ON CONFLICT (user_id, item_id) DO UPDATE SET
|
123
|
+
"quantity" = "user_items"."quantity" + excluded."quantity" RETURNING "id"
|
87
124
|
```
|
88
125
|
|
89
126
|
---
|
90
127
|
### pay_all _(hash, update_columns, primary_key: :id)_
|
91
128
|
|
92
|
-
Reduce the quantity of items and return how many rows and updated if all of them
|
129
|
+
Reduce the quantity of items and return how many rows and updated if all of them are enough.
|
93
130
|
Do nothing and return zero if any of them is not enough.
|
94
131
|
|
95
132
|
#### Parameters
|
96
133
|
|
97
134
|
- `hash` - A hash contains the id of the models as keys and the amount to update the field by as values.
|
98
135
|
- `update_columns` - The column that will be updated.
|
99
|
-
- `primary_key` - Specify the column that `id`(the key of hash)
|
136
|
+
- `primary_key` - Specify the column that `id`(the key of hash) refers to.
|
100
137
|
|
101
138
|
#### Example
|
102
139
|
|
103
140
|
```rb
|
104
141
|
user.user_items.atomically.pay_all({ item1.id => 4, item2.id => 3 }, [:quantity], primary_key: :item_id)
|
142
|
+
# => 2 (if success)
|
143
|
+
# => 0 (if some aren't enough)
|
105
144
|
```
|
106
145
|
|
107
146
|
#### SQL queries
|
@@ -138,35 +177,71 @@ Behaves like [ActiveRecord::Relation#update_all](https://apidock.com/rails/Activ
|
|
138
177
|
|
139
178
|
#### Examples
|
140
179
|
```rb
|
141
|
-
User.where(id: [
|
142
|
-
# => 2
|
180
|
+
User.where(id: [5, 6]).atomically.update_all(2, name: '')
|
181
|
+
# => 2 (success)
|
143
182
|
|
144
|
-
User.where(id: [
|
145
|
-
# => 0
|
183
|
+
User.where(id: [7, 8, 9]).atomically.update_all(2, name: '')
|
184
|
+
# => 0 (fail)
|
146
185
|
```
|
147
186
|
|
148
187
|
#### SQL queries
|
149
188
|
|
150
189
|
```sql
|
151
|
-
# User.where(id: [
|
152
|
-
UPDATE `users` SET `users`.`name` = '' WHERE `users`.`id` IN (
|
190
|
+
# User.where(id: [7, 8, 9]).atomically.update_all(2, name: '')
|
191
|
+
UPDATE `users` SET `users`.`name` = '' WHERE `users`.`id` IN (7, 8, 9) AND (
|
153
192
|
(
|
154
193
|
SELECT COUNT(*) FROM (
|
155
|
-
SELECT `users`.* FROM `users` WHERE `users`.`id` IN (
|
194
|
+
SELECT `users`.* FROM `users` WHERE `users`.`id` IN (7, 8, 9)
|
156
195
|
) subquery
|
157
196
|
) = 2
|
158
197
|
)
|
159
198
|
```
|
160
199
|
|
200
|
+
---
|
201
|
+
### update_all_and_get_ids _(updates)_
|
202
|
+
|
203
|
+
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.
|
204
|
+
|
205
|
+
|
206
|
+
#### Parameters
|
207
|
+
|
208
|
+
- `updates` - A string, array, or hash representing the SET part of an SQL statement.
|
209
|
+
|
210
|
+
#### Example
|
211
|
+
|
212
|
+
```rb
|
213
|
+
User.where(account: ['moon', 'wolf']).atomically.update_all_and_get_ids('money = money + 1')
|
214
|
+
# => [254, 371] (array of updated user ids)
|
215
|
+
|
216
|
+
User.where(account: ['moon', 'wolf']).update_all('money = money + 1')
|
217
|
+
# => 2 (the number of updated records)
|
218
|
+
```
|
219
|
+
|
220
|
+
#### SQL queries
|
221
|
+
|
222
|
+
```sql
|
223
|
+
# mysql
|
224
|
+
BEGIN
|
225
|
+
SET @ids := NULL
|
226
|
+
UPDATE `users` SET money = money + 1 WHERE `users`.`account` IN ('moon', 'wolf') AND ((SELECT @ids := CONCAT_WS(',', `users`.`id`, @ids)))
|
227
|
+
SELECT @ids FROM DUAL
|
228
|
+
COMMIT
|
229
|
+
|
230
|
+
# pg
|
231
|
+
UPDATE 'users' SET money = money + 1 RETURNING id
|
232
|
+
```
|
233
|
+
|
161
234
|
---
|
162
235
|
### update _(attrs, from: :not_set)_
|
163
236
|
|
164
|
-
Updates the attributes of the model from the passed-in hash and saves the record.
|
237
|
+
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.
|
238
|
+
|
239
|
+
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.
|
165
240
|
|
166
241
|
#### Parameters
|
167
242
|
|
168
243
|
- `attrs` - Same with the first parameter of [ActiveRecord#update](https://apidock.com/rails/ActiveRecord/Persistence/update)
|
169
|
-
- `from` - The value before update. If not set, use the attriutes of the model.
|
244
|
+
- `from` - The value before update. If not set, use the current attriutes of the model.
|
170
245
|
|
171
246
|
#### Example
|
172
247
|
|
@@ -182,8 +257,28 @@ class Arena < ApplicationRecord
|
|
182
257
|
end
|
183
258
|
```
|
184
259
|
|
260
|
+
Let `arena.closed_at` be nil.
|
261
|
+
|
262
|
+
```rb
|
263
|
+
arena.atomically_close!
|
264
|
+
# => true (if success)
|
265
|
+
# => false (if race condition occurs)
|
266
|
+
```
|
267
|
+
|
268
|
+
The return value can be used to prevent race condition and make sure some piece of code is executed only once.
|
269
|
+
|
270
|
+
```rb
|
271
|
+
if arena.atomically_close!
|
272
|
+
# Only one request can pass this check and execute the code here.
|
273
|
+
# You can send rewards, calculate ranking, or fire background job here.
|
274
|
+
# No need to worry about being invoked multiple times.
|
275
|
+
do_something
|
276
|
+
end
|
277
|
+
```
|
278
|
+
|
185
279
|
#### SQL queries
|
186
280
|
|
281
|
+
|
187
282
|
```sql
|
188
283
|
# arena.atomically_close!
|
189
284
|
UPDATE `arenas` SET `arenas`.`closed_at` = '2018-11-27 03:44:25', `updated_at` = '2018-11-27 03:44:25'
|
@@ -195,30 +290,39 @@ WHERE `arenas`.`id` = 1752
|
|
195
290
|
```
|
196
291
|
|
197
292
|
---
|
198
|
-
###
|
293
|
+
### decrement_unsigned_counters _(counters)_
|
294
|
+
|
295
|
+
Decrement numeric fields via a direct SQL update, and make sure that it will not become negative.
|
199
296
|
|
200
|
-
|
297
|
+
Return true if update successfully, false otherwise.
|
201
298
|
|
202
299
|
|
203
300
|
#### Parameters
|
204
301
|
|
205
|
-
- `
|
302
|
+
- `counters` - A Hash containing the names of the fields to update as keys and the amount to update the field by as values.
|
206
303
|
|
207
304
|
#### Example
|
208
305
|
|
209
306
|
```rb
|
210
|
-
|
211
|
-
# =>
|
307
|
+
user.money
|
308
|
+
# => 100
|
309
|
+
|
310
|
+
user.atomically.decrement_unsigned_counters(money: 10)
|
311
|
+
# => true (success)
|
312
|
+
user.reload.money
|
313
|
+
# => 90
|
314
|
+
|
315
|
+
user.atomically.decrement_unsigned_counters(money: 999)
|
316
|
+
# => false (fail)
|
317
|
+
user.reload.money
|
318
|
+
# => 90
|
212
319
|
```
|
213
320
|
|
214
321
|
#### SQL queries
|
215
322
|
|
216
323
|
```sql
|
217
|
-
|
218
|
-
|
219
|
-
UPDATE `users` SET money = money + 1 WHERE `users`.`account` IN ('moon', 'wolf') AND ((SELECT @ids := CONCAT_WS(',', `users`.`id`, @ids)))
|
220
|
-
SELECT @ids FROM DUAL
|
221
|
-
COMMIT
|
324
|
+
# user.atomically.decrement_unsigned_counters(money: 140)
|
325
|
+
UPDATE `users` SET money = money - 140 WHERE `users`.`id` = 1 AND (money >= 140)
|
222
326
|
```
|
223
327
|
|
224
328
|
## Development
|
data/atomically.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ['khiav reoy']
|
10
10
|
spec.email = ['mrtmrt15xn@yahoo.com.tw']
|
11
11
|
|
12
|
-
spec.summary = ''
|
13
|
-
spec.description = ''
|
12
|
+
spec.summary = 'An ActiveRecord extension for writing commonly useful atomic SQL statements to avoid race condition.'
|
13
|
+
spec.description = 'An ActiveRecord extension for writing commonly useful atomic SQL statements to avoid race condition.'
|
14
14
|
spec.homepage = 'https://github.com/khiav223577/atomically'
|
15
15
|
spec.license = 'MIT'
|
16
16
|
|
@@ -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
|
-
spec.add_development_dependency 'bundler', '
|
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
@@ -3,14 +3,14 @@ source 'https://rubygems.org'
|
|
3
3
|
gem 'activerecord', '~> 3.2.0'
|
4
4
|
|
5
5
|
group :test do
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
6
|
+
gem 'mysql2', '0.3.21' if %w[mysql makara_mysql].include?(ENV['DB'])
|
7
|
+
gem 'pg', '~> 0.18' if %w[pg makara_pg].include?(ENV['DB'])
|
8
|
+
gem 'makara', '~> 0.4.1' if %w[makara_mysql makara_pg].include?(ENV['DB'])
|
10
9
|
gem 'simplecov'
|
11
|
-
gem '
|
10
|
+
gem 'i18n', '< 1.6'
|
12
11
|
gem 'pluck_all', '>= 2.0.3'
|
13
12
|
gem 'timecop', '~> 0.9.1'
|
13
|
+
gem 'update_all_scope', '~> 0.1.0'
|
14
14
|
end
|
15
15
|
|
16
16
|
gemspec path: '../'
|
data/gemfiles/4.2.gemfile
CHANGED
@@ -3,14 +3,14 @@ source 'https://rubygems.org'
|
|
3
3
|
gem 'activerecord', '~> 4.2.0'
|
4
4
|
|
5
5
|
group :test do
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
6
|
+
gem 'mysql2', '0.3.21' if %w[mysql makara_mysql].include?(ENV['DB'])
|
7
|
+
gem 'pg', '~> 0.18' if %w[pg makara_pg].include?(ENV['DB'])
|
8
|
+
gem 'makara', '~> 0.4.1' if %w[makara_mysql makara_pg].include?(ENV['DB'])
|
10
9
|
gem 'simplecov'
|
11
|
-
gem '
|
10
|
+
gem 'i18n', '< 1.6'
|
12
11
|
gem 'pluck_all', '>= 2.0.3'
|
13
12
|
gem 'timecop', '~> 0.9.1'
|
13
|
+
gem 'update_all_scope', '~> 0.1.0'
|
14
14
|
end
|
15
15
|
|
16
16
|
gemspec path: '../'
|
data/gemfiles/5.0.gemfile
CHANGED
@@ -3,14 +3,14 @@ source 'https://rubygems.org'
|
|
3
3
|
gem 'activerecord', '~> 5.0.0'
|
4
4
|
|
5
5
|
group :test do
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
6
|
+
gem 'mysql2', '0.3.21' if %w[mysql makara_mysql].include?(ENV['DB'])
|
7
|
+
gem 'pg', '~> 0.18' if %w[pg makara_pg].include?(ENV['DB'])
|
8
|
+
gem 'makara', '~> 0.4.1' if %w[makara_mysql makara_pg].include?(ENV['DB'])
|
10
9
|
gem 'simplecov'
|
11
|
-
gem '
|
10
|
+
gem 'i18n', '< 1.6'
|
12
11
|
gem 'pluck_all', '>= 2.0.3'
|
13
12
|
gem 'timecop', '~> 0.9.1'
|
13
|
+
gem 'update_all_scope', '~> 0.1.0'
|
14
14
|
end
|
15
15
|
|
16
16
|
gemspec path: '../'
|
data/gemfiles/5.1.gemfile
CHANGED
@@ -3,14 +3,14 @@ source 'https://rubygems.org'
|
|
3
3
|
gem 'activerecord', '~> 5.1.0'
|
4
4
|
|
5
5
|
group :test do
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
6
|
+
gem 'mysql2', '0.3.21' if %w[mysql makara_mysql].include?(ENV['DB'])
|
7
|
+
gem 'pg', '~> 0.18' if %w[pg makara_pg].include?(ENV['DB'])
|
8
|
+
gem 'makara', '~> 0.4.1' if %w[makara_mysql makara_pg].include?(ENV['DB'])
|
10
9
|
gem 'simplecov'
|
11
|
-
gem '
|
10
|
+
gem 'i18n', '< 1.6'
|
12
11
|
gem 'pluck_all', '>= 2.0.3'
|
13
12
|
gem 'timecop', '~> 0.9.1'
|
13
|
+
gem 'update_all_scope', '~> 0.1.0'
|
14
14
|
end
|
15
15
|
|
16
16
|
gemspec path: '../'
|
data/gemfiles/5.2.gemfile
CHANGED
@@ -3,14 +3,14 @@ source 'https://rubygems.org'
|
|
3
3
|
gem 'activerecord', '~> 5.2.0'
|
4
4
|
|
5
5
|
group :test do
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
6
|
+
gem 'mysql2', '0.5.1' if %w[mysql makara_mysql].include?(ENV['DB'])
|
7
|
+
gem 'pg', '~> 0.18' if %w[pg makara_pg].include?(ENV['DB'])
|
8
|
+
gem 'makara', '~> 0.4.1' if %w[makara_mysql makara_pg].include?(ENV['DB'])
|
10
9
|
gem 'simplecov'
|
11
|
-
gem '
|
10
|
+
gem 'i18n', '< 1.6'
|
12
11
|
gem 'pluck_all', '>= 2.0.3'
|
13
12
|
gem 'timecop', '~> 0.9.1'
|
13
|
+
gem 'update_all_scope', '~> 0.1.0'
|
14
14
|
end
|
15
15
|
|
16
16
|
gemspec path: '../'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'activerecord', '~> 6.0.0'
|
4
|
+
|
5
|
+
group :test do
|
6
|
+
gem 'mysql2', '0.5.1' if %w[mysql makara_mysql].include?(ENV['DB'])
|
7
|
+
gem 'pg', '~> 0.18' if %w[pg makara_pg].include?(ENV['DB'])
|
8
|
+
gem 'makara', '~> 0.4.1' if %w[makara_mysql makara_pg].include?(ENV['DB'])
|
9
|
+
gem 'simplecov'
|
10
|
+
gem 'pluck_all', '>= 2.0.4'
|
11
|
+
gem 'timecop', '~> 0.9.1'
|
12
|
+
gem 'update_all_scope', '~> 0.1.0'
|
13
|
+
end
|
14
|
+
|
15
|
+
gemspec path: '../'
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
class Atomically::AdapterCheckService
|
3
|
+
def initialize(klass)
|
4
|
+
@klass = klass
|
5
|
+
end
|
6
|
+
|
7
|
+
def pg?
|
8
|
+
possible_pg_klasses.any?{|s| @klass.connection.is_a?(s) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def mysql?
|
12
|
+
possible_mysql_klasses.any?{|s| @klass.connection.is_a?(s) }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def possible_pg_klasses
|
18
|
+
@possible_pg_klasses ||= [].tap do |result|
|
19
|
+
result << ActiveRecord::ConnectionAdapters::PostgreSQLAdapter if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
20
|
+
result << ActiveRecord::ConnectionAdapters::MakaraPostgreSQLAdapter if defined?(ActiveRecord::ConnectionAdapters::MakaraPostgreSQLAdapter)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def possible_mysql_klasses
|
25
|
+
@possible_mysql_klasses ||= [].tap do |result|
|
26
|
+
result << ActiveRecord::ConnectionAdapters::Mysql2Adapter if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
|
27
|
+
result << ActiveRecord::ConnectionAdapters::MakaraMysql2Adapter if defined?(ActiveRecord::ConnectionAdapters::MakaraMysql2Adapter)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
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 adapter_check_service.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
|
|
@@ -44,25 +53,62 @@ class Atomically::QueryService
|
|
44
53
|
|
45
54
|
def update(attrs, from: :not_set)
|
46
55
|
success = update_and_return_number_of_updated_rows(attrs, from) == 1
|
47
|
-
|
56
|
+
|
57
|
+
if success
|
58
|
+
assign_without_changes(attrs)
|
59
|
+
@model.send(:clear_attribute_changes, @model.changes.keys)
|
60
|
+
end
|
61
|
+
|
48
62
|
return success
|
49
63
|
end
|
50
64
|
|
65
|
+
# ==== Parameters
|
66
|
+
#
|
67
|
+
# * +counters+ - A Hash containing the names of the fields
|
68
|
+
# to update as keys and the amount to update the field by as values.
|
69
|
+
def decrement_unsigned_counters(counters)
|
70
|
+
result = open_update_all_scope do
|
71
|
+
counters.each do |field, amount|
|
72
|
+
where("#{field} >= ?", amount).update("#{field} = #{field} - ?", amount) if amount > 0
|
73
|
+
end
|
74
|
+
end
|
75
|
+
return (result == 1)
|
76
|
+
end
|
77
|
+
|
51
78
|
def update_all_and_get_ids(*args)
|
79
|
+
if adapter_check_service.pg?
|
80
|
+
scope = UpdateAllScope::UpdateAllScope.new(model: @model, relation: @relation.where(''))
|
81
|
+
scope.update(*args)
|
82
|
+
return @klass.connection.execute("#{scope.to_sql} RETURNING id", "#{@klass} Update All").map{|s| s['id'].to_i }
|
83
|
+
end
|
84
|
+
|
52
85
|
ids = nil
|
53
|
-
id_column =
|
86
|
+
id_column = quote_column_with_table(:id)
|
54
87
|
@klass.transaction do
|
55
88
|
@relation.connection.execute('SET @ids := NULL')
|
56
89
|
@relation.where("(SELECT @ids := CONCAT_WS(',', #{id_column}, @ids))").update_all(*args) # 撈出有真的被更新的 id,用逗號串在一起
|
57
|
-
ids = @klass.from(nil).pluck('@ids').first
|
90
|
+
ids = @klass.from(nil).pluck(Arel.sql('@ids')).first
|
58
91
|
end
|
59
92
|
return ids.try{|s| s.split(',').map(&:to_i).uniq.sort } || [] # 將 id 從字串取出來 @id 的格式範例: '1,4,12'
|
60
93
|
end
|
61
94
|
|
62
95
|
private
|
63
96
|
|
64
|
-
def
|
65
|
-
|
97
|
+
def adapter_check_service
|
98
|
+
@adapter_check_service ||= Atomically::AdapterCheckService.new(@klass)
|
99
|
+
end
|
100
|
+
|
101
|
+
def on_duplicate_key_plus_sql(columns, conflict_target)
|
102
|
+
service = Atomically::OnDuplicateSqlService.new(@klass, columns)
|
103
|
+
return service.mysql_quote_columns_for_plus.join(', ') if adapter_check_service.mysql?
|
104
|
+
return {
|
105
|
+
conflict_target: conflict_target,
|
106
|
+
columns: service.pg_quote_columns_for_plus.join(', '),
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
def quote_column_with_table(column)
|
111
|
+
"#{@klass.quoted_table_name}.#{quote_column(column)}"
|
66
112
|
end
|
67
113
|
|
68
114
|
def quote_column(column)
|
@@ -77,12 +123,17 @@ class Atomically::QueryService
|
|
77
123
|
query.where("(#{@klass.from(query.where('')).select('COUNT(*)').to_sql}) = ?", expected_size)
|
78
124
|
end
|
79
125
|
|
80
|
-
def update_and_return_number_of_updated_rows(attrs,
|
126
|
+
def update_and_return_number_of_updated_rows(attrs, from_value)
|
81
127
|
model = @model
|
82
128
|
return open_update_all_scope do
|
83
129
|
update(updated_at: Time.now)
|
130
|
+
|
131
|
+
model.changes.each do |column, (_old_value, new_value)|
|
132
|
+
update(column => new_value)
|
133
|
+
end
|
134
|
+
|
84
135
|
attrs.each do |column, value|
|
85
|
-
old_value = (
|
136
|
+
old_value = (from_value == :not_set ? model[column] : from_value)
|
86
137
|
where(column => old_value).update(column => value) if old_value != value
|
87
138
|
end
|
88
139
|
end
|
@@ -90,7 +141,7 @@ class Atomically::QueryService
|
|
90
141
|
|
91
142
|
def open_update_all_scope(&block)
|
92
143
|
return 0 if @model == nil
|
93
|
-
scope = UpdateAllScope.new(model: @model)
|
144
|
+
scope = UpdateAllScope::UpdateAllScope.new(model: @model)
|
94
145
|
scope.instance_exec(&block)
|
95
146
|
return scope.do_query!
|
96
147
|
end
|
data/lib/atomically/version.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,35 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: atomically
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- khiav reoy
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-09-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
19
|
+
version: '1.17'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 3.x
|
20
23
|
type: :development
|
21
24
|
prerelease: false
|
22
25
|
version_requirements: !ruby/object:Gem::Requirement
|
23
26
|
requirements:
|
24
|
-
- - "
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.17'
|
30
|
+
- - "<"
|
25
31
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
32
|
+
version: 3.x
|
27
33
|
- !ruby/object:Gem::Dependency
|
28
34
|
name: rake
|
29
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +86,20 @@ dependencies:
|
|
80
86
|
- - ">="
|
81
87
|
- !ruby/object:Gem::Version
|
82
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'
|
83
103
|
- !ruby/object:Gem::Dependency
|
84
104
|
name: pluck_all
|
85
105
|
requirement: !ruby/object:Gem::Requirement
|
@@ -150,7 +170,22 @@ dependencies:
|
|
150
170
|
- - ">="
|
151
171
|
- !ruby/object:Gem::Version
|
152
172
|
version: 1.1.8
|
153
|
-
|
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
|
187
|
+
description: An ActiveRecord extension for writing commonly useful atomic SQL statements
|
188
|
+
to avoid race condition.
|
154
189
|
email:
|
155
190
|
- mrtmrt15xn@yahoo.com.tw
|
156
191
|
executables: []
|
@@ -162,7 +197,6 @@ files:
|
|
162
197
|
- ".travis.yml"
|
163
198
|
- CHANGELOG.md
|
164
199
|
- CODE_OF_CONDUCT.md
|
165
|
-
- Gemfile.gemfile
|
166
200
|
- LICENSE
|
167
201
|
- LICENSE.txt
|
168
202
|
- README.md
|
@@ -175,18 +209,25 @@ files:
|
|
175
209
|
- gemfiles/5.0.gemfile
|
176
210
|
- gemfiles/5.1.gemfile
|
177
211
|
- gemfiles/5.2.gemfile
|
212
|
+
- gemfiles/6.0.gemfile
|
178
213
|
- lib/atomically.rb
|
179
214
|
- lib/atomically/active_record/extension.rb
|
215
|
+
- lib/atomically/adapter_check_service.rb
|
216
|
+
- lib/atomically/on_duplicate_sql_service.rb
|
180
217
|
- lib/atomically/patches/clear_attribute_changes.rb
|
181
218
|
- lib/atomically/patches/from.rb
|
182
219
|
- lib/atomically/patches/none.rb
|
183
220
|
- lib/atomically/query_service.rb
|
184
|
-
- lib/atomically/update_all_scope.rb
|
185
221
|
- lib/atomically/version.rb
|
186
222
|
homepage: https://github.com/khiav223577/atomically
|
187
223
|
licenses:
|
188
224
|
- MIT
|
189
|
-
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
|
190
231
|
post_install_message:
|
191
232
|
rdoc_options: []
|
192
233
|
require_paths:
|
@@ -202,9 +243,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
202
243
|
- !ruby/object:Gem::Version
|
203
244
|
version: '0'
|
204
245
|
requirements: []
|
205
|
-
|
206
|
-
rubygems_version: 2.7.6
|
246
|
+
rubygems_version: 3.0.3
|
207
247
|
signing_key:
|
208
248
|
specification_version: 4
|
209
|
-
summary:
|
249
|
+
summary: An ActiveRecord extension for writing commonly useful atomic SQL statements
|
250
|
+
to avoid race condition.
|
210
251
|
test_files: []
|
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
|