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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec69d82cbee452fb2b61bd973aa1a885e5cffd5cdd8c5b5e6bb96ea066c8f7b1
4
- data.tar.gz: be3720c0644d7c8228ea3b8ae88fd05c1850c66f629819977cc4758b8ec0d3d9
3
+ metadata.gz: c08c4d945a90102861c424a610fab072472cf17e31497936987ec842db6be426
4
+ data.tar.gz: c8c2b834fb5a652bdf3f31eb16fd0996ff2b3762613a31be57b038a7afcf9bd0
5
5
  SHA512:
6
- metadata.gz: 1b88cc95978a5c3cf4913d812e99e32aa4ef6b2bee12572e7e9eccea51edeadae8c78cdd42ad2f436ea9f3749832c1cacec0dd380a64900b0caa1936446b9a7a
7
- data.tar.gz: ea519708c2c2d24b7876f810095adf0a9b599d713b3e7cb4091981d567886ce9c28644d42bca688790ada4d0e9e5b153bc3f9c6149fc060818768ec12493d437
6
+ metadata.gz: 6d4ab1744413db5f7c14d3982b94289c836c7d007206fdc4f7cc40c2398ce165b97b65d95d12a095a915724e9fff3a59f2f35957f8fb6f72ccd7557ef12255fb
7
+ data.tar.gz: 8016f81e284201892faa8f8cad82302c8b674d50fc1181906b970d7e73533a886de33be9b22a61df8d180cf667292560ad28e595ece0893d45d63aeb798f55fb
@@ -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/HandleExceptions:
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/StringConversionInInterpolation:
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/UnneededCopDisableDirective:
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
- Metrics/LineLength:
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
- Performance/Sample:
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: true
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/AlignArray:
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/AlignHash:
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/AlignParameters:
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: true
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/FirstParameterIndentation:
616
+ Layout/FirstArgumentIndentation:
620
617
  Description: 'Checks the indentation of the first parameter in a method call.'
621
618
  Enabled: true
622
619
 
623
- Style/FlipFlop:
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: true
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: sprintf
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/IndentArray:
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/IndentHash:
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
- Enabled: false
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: true
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: true
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: true
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
- - acc
942
- - elem
942
+ - sum
943
+ - v
943
944
  - inject:
944
- - acc
945
- - elem
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/TrailingBlankLines:
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/UnneededCapitalW:
1180
+ Style/RedundantCapitalW:
1180
1181
  Description: 'Checks for %W when interpolation is not needed.'
1181
1182
  Enabled: true
1182
1183
 
1183
- Style/UnneededPercentQ:
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
@@ -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
5
+ - 2.6
6
+ - 2.7
7
+ services:
8
+ - mysql
9
+ addons:
10
+ postgresql: "9.6"
9
11
  env:
10
- - DB=mysql
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.5
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
- - gem i rubygems-update -v '<3' && update_rubygems
23
- - gem install bundler
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:
@@ -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
- Supports Rails 3.2, 4.2, 5.0, 5.1, 5.2.
11
+ All methods are defined in `Atomically::QueryService` instead of defining in `ActiveRecord` directly, in order not to pollute the model instance.
12
+
13
+ ## Supports
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
- - [create_or_plus](#create_or_plus-columns-values-on_duplicate_update_columns)
18
- - [pay_all](#pay_all-hash-update_columns-primary_key-id)
19
- - [update_all](#update_all-expected_number-updates)
20
- - [update](#update-attrs-from-not_set)
21
- - [update_all_and_get_ids](#update_all_and_get_ids-updates)
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
- UserItem.atomically.create_or_plus(columns, values, on_duplicate_update_columns)
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/48998921-ff430600-f18f-11e8-8eeb-e8a71bbf5802.png)
101
+ ![before](https://user-images.githubusercontent.com/4011729/67365648-95e89480-f5a4-11e9-8147-279385c6f442.png)
75
102
 
76
103
  after
77
104
 
78
- ![image](https://user-images.githubusercontent.com/4011729/48999092-8d1ef100-f190-11e8-8372-86e2e99cbe08.png)
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 `quantity` = `quantity` + VALUES(`quantity`)
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 is enough.
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) refer to.
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: [1, 2]).atomically.update_all(2, name: '')
142
- # => 2
180
+ User.where(id: [5, 6]).atomically.update_all(2, name: '')
181
+ # => 2 (success)
143
182
 
144
- User.where(id: [1, 2, 3]).atomically.update_all(2, name: '')
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: [1, 2, 3]).atomically.update_all(2, name: '')
152
- UPDATE `users` SET `users`.`name` = '' WHERE `users`.`id` IN (1, 2, 3) AND (
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 (1, 2, 3)
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. 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.
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
- ### update_all_and_get_ids _(updates)_
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
- Behaves like [ActiveRecord::Relation#update_all](https://apidock.com/rails/ActiveRecord/Relation/update_all), but return the ids array of updated records instead of the number of updated records.
297
+ Return true if update successfully, false otherwise.
201
298
 
202
299
 
203
300
  #### Parameters
204
301
 
205
- - `updates` - A string, array, or hash representing the SET part of an SQL statement.
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
- User.where(account: ['moon', 'wolf']).atomically.update_all_and_get_ids('money = money + 1')
211
- # => [254, 371]
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
- BEGIN
218
- SET @ids := NULL
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
@@ -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', '~> 1.11'
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
@@ -3,14 +3,14 @@ source 'https://rubygems.org'
3
3
  gem 'activerecord', '~> 3.2.0'
4
4
 
5
5
  group :test do
6
- case ENV['DB']
7
- when 'mysql' ; gem 'mysql2', '0.3.21'
8
- when 'postgres' ; gem 'pg', '~> 0.18'
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 'codeclimate-test-reporter', '~> 1.0.0'
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: '../'
@@ -3,14 +3,14 @@ source 'https://rubygems.org'
3
3
  gem 'activerecord', '~> 4.2.0'
4
4
 
5
5
  group :test do
6
- case ENV['DB']
7
- when 'mysql' ; gem 'mysql2', '0.3.21'
8
- when 'postgres' ; gem 'pg', '~> 0.18'
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 'codeclimate-test-reporter', '~> 1.0.0'
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: '../'
@@ -3,14 +3,14 @@ source 'https://rubygems.org'
3
3
  gem 'activerecord', '~> 5.0.0'
4
4
 
5
5
  group :test do
6
- case ENV['DB']
7
- when 'mysql' ; gem 'mysql2', '0.3.21'
8
- when 'postgres' ; gem 'pg', '~> 0.18'
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 'codeclimate-test-reporter', '~> 1.0.0'
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: '../'
@@ -3,14 +3,14 @@ source 'https://rubygems.org'
3
3
  gem 'activerecord', '~> 5.1.0'
4
4
 
5
5
  group :test do
6
- case ENV['DB']
7
- when 'mysql' ; gem 'mysql2', '0.3.21'
8
- when 'postgres' ; gem 'pg', '~> 0.18'
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 'codeclimate-test-reporter', '~> 1.0.0'
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: '../'
@@ -3,14 +3,14 @@ source 'https://rubygems.org'
3
3
  gem 'activerecord', '~> 5.2.0'
4
4
 
5
5
  group :test do
6
- case ENV['DB']
7
- when 'mysql' ; gem 'mysql2', '0.5.1'
8
- when 'postgres' ; gem 'pg', '~> 0.18'
9
- end
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 'codeclimate-test-reporter', '~> 1.0.0'
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 'atomically/update_all_scope'
5
+ require 'update_all_scope'
6
+ require 'atomically/on_duplicate_sql_service'
7
+ require 'atomically/adapter_check_service'
6
8
  require 'atomically/patches/clear_attribute_changes' if not ActiveModel::Dirty.method_defined?(:clear_attribute_changes) and not ActiveModel::Dirty.private_method_defined?(:clear_attribute_changes)
7
9
  require 'atomically/patches/none' if not ActiveRecord::Base.respond_to?(:none)
8
10
  require 'atomically/patches/from' if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new('4.0.0')
9
11
 
10
12
  class Atomically::QueryService
13
+ DEFAULT_CONFLICT_TARGETS = [:id].freeze
14
+
11
15
  def initialize(klass, relation: nil, model: nil)
12
16
  @klass = klass
13
17
  @relation = relation || @klass
14
18
  @model = model
15
19
  end
16
20
 
17
- def create_or_plus(columns, data, update_columns)
18
- @klass.import(columns, data, on_duplicate_key_update: on_duplicate_key_plus_sql(update_columns))
21
+ def create_or_plus(columns, data, update_columns, conflict_target: DEFAULT_CONFLICT_TARGETS)
22
+ @klass.import(columns, data, on_duplicate_key_update: on_duplicate_key_plus_sql(update_columns, conflict_target))
19
23
  end
20
24
 
21
25
  def pay_all(hash, update_columns, primary_key: :id) # { id => pay_count }
@@ -30,8 +34,13 @@ class Atomically::QueryService
30
34
  end
31
35
 
32
36
  raw_when_sql = hash.map{|id, pay_count| "WHEN #{sanitize(id)} THEN #{sanitize(-pay_count)}" }.join("\n")
37
+ no_var_in_sql = true if update_columns.size == 1 or adapter_check_service.pg?
33
38
  update_sqls = update_columns.map.with_index do |column, idx|
34
- value = idx == 0 ? "(@change := \nCASE #{quote_column(primary_key)}\n#{raw_when_sql}\nEND)" : '@change'
39
+ if no_var_in_sql
40
+ value = "(\nCASE #{quote_column(primary_key)}\n#{raw_when_sql}\nEND)"
41
+ else
42
+ value = idx == 0 ? "(@change := \nCASE #{quote_column(primary_key)}\n#{raw_when_sql}\nEND)" : '@change'
43
+ end
35
44
  next "#{column} = #{column} + #{value}"
36
45
  end
37
46
 
@@ -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
- assign_without_changes(attrs) if success
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 = "#{@klass.quoted_table_name}.#{quote_column(:id)}"
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 on_duplicate_key_plus_sql(columns)
65
- columns.lazy.map(&method(:quote_column)).map{|s| "#{s} = #{s} + VALUES(#{s})" }.force.join(', ')
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, from)
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 = (from == :not_set ? model[column] : from)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Atomically
4
- VERSION = '1.0.4'
4
+ VERSION = '1.1.2'
5
5
  end
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.0.4
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: 2018-12-21 00:00:00.000000000 Z
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.11'
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: '1.11'
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
- description: ''
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
- rubyforge_project:
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: []
@@ -1,12 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in rails_or.gemspec
4
-
5
- gem 'activerecord', '~> 5.0'
6
-
7
- group :test do
8
- gem 'simplecov'
9
- gem 'codeclimate-test-reporter', '~> 1.0.0'
10
- end
11
-
12
- gemspec path: '../'
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class UpdateAllScope
4
- def initialize(model: nil, relation: nil)
5
- @queries = []
6
- @relation = relation || model.class.where(id: model.id)
7
- end
8
-
9
- def where(*args)
10
- @relation = @relation.where(*args)
11
- return self
12
- end
13
-
14
- def update(query, *binding_values)
15
- args = binding_values.size > 0 ? [[query, *binding_values]] : [query]
16
- @queries << klass.send(:sanitize_sql_for_assignment, *args)
17
- return self
18
- end
19
-
20
- def do_query!
21
- return 0 if @queries.empty?
22
- return @relation.update_all(@queries.join(','))
23
- end
24
-
25
- def klass
26
- @relation.klass
27
- end
28
- end