atomically 1.0.4 → 1.1.2
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 +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
|
-

|
75
102
|
|
76
103
|
after
|
77
104
|
|
78
|
-

|
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
|