atomically 1.1.2 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,153 +1,154 @@
1
- # frozen_string_literal: true
2
-
3
- require 'activerecord-import'
4
- require 'rails_or'
5
- require 'update_all_scope'
6
- require 'atomically/on_duplicate_sql_service'
7
- require 'atomically/adapter_check_service'
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)
9
- require 'atomically/patches/none' if not ActiveRecord::Base.respond_to?(:none)
10
- require 'atomically/patches/from' if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new('4.0.0')
11
-
12
- class Atomically::QueryService
13
- DEFAULT_CONFLICT_TARGETS = [:id].freeze
14
-
15
- def initialize(klass, relation: nil, model: nil)
16
- @klass = klass
17
- @relation = relation || @klass
18
- @model = model
19
- end
20
-
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))
23
- end
24
-
25
- def pay_all(hash, update_columns, primary_key: :id) # { id => pay_count }
26
- return 0 if hash.blank?
27
-
28
- update_columns = update_columns.map(&method(:quote_column))
29
-
30
- query = hash.inject(@klass.none) do |relation, (id, pay_count)|
31
- condition = @relation.where(primary_key => id)
32
- update_columns.each{|s| condition = condition.where("#{s} >= ?", pay_count) }
33
- next relation.or(condition)
34
- end
35
-
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?
38
- update_sqls = update_columns.map.with_index do |column, idx|
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
44
- next "#{column} = #{column} + #{value}"
45
- end
46
-
47
- return where_all_can_be_updated(query, hash.size).update_all(update_sqls.join(', '))
48
- end
49
-
50
- def update_all(expected_size, *args)
51
- where_all_can_be_updated(@relation, expected_size).update_all(*args)
52
- end
53
-
54
- def update(attrs, from: :not_set)
55
- success = update_and_return_number_of_updated_rows(attrs, from) == 1
56
-
57
- if success
58
- assign_without_changes(attrs)
59
- @model.send(:clear_attribute_changes, @model.changes.keys)
60
- end
61
-
62
- return success
63
- end
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
-
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
-
85
- ids = nil
86
- id_column = quote_column_with_table(:id)
87
- @klass.transaction do
88
- @relation.connection.execute('SET @ids := NULL')
89
- @relation.where("(SELECT @ids := CONCAT_WS(',', #{id_column}, @ids))").update_all(*args) # 撈出有真的被更新的 id,用逗號串在一起
90
- ids = @klass.from(nil).pluck(Arel.sql('@ids')).first
91
- end
92
- return ids.try{|s| s.split(',').map(&:to_i).uniq.sort } || [] # 將 id 從字串取出來 @id 的格式範例: '1,4,12'
93
- end
94
-
95
- private
96
-
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)}"
112
- end
113
-
114
- def quote_column(column)
115
- @klass.connection.quote_column_name(column)
116
- end
117
-
118
- def sanitize(value)
119
- @klass.connection.quote(value)
120
- end
121
-
122
- def where_all_can_be_updated(query, expected_size)
123
- query.where("(#{@klass.from(query.where('')).select('COUNT(*)').to_sql}) = ?", expected_size)
124
- end
125
-
126
- def update_and_return_number_of_updated_rows(attrs, from_value)
127
- model = @model
128
- return open_update_all_scope do
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
-
135
- attrs.each do |column, value|
136
- old_value = (from_value == :not_set ? model[column] : from_value)
137
- where(column => old_value).update(column => value) if old_value != value
138
- end
139
- end
140
- end
141
-
142
- def open_update_all_scope(&block)
143
- return 0 if @model == nil
144
- scope = UpdateAllScope::UpdateAllScope.new(model: @model)
145
- scope.instance_exec(&block)
146
- return scope.do_query!
147
- end
148
-
149
- def assign_without_changes(attributes)
150
- @model.assign_attributes(attributes)
151
- @model.send(:clear_attribute_changes, attributes.keys)
152
- end
153
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'activerecord-import'
4
+ require 'rails_or'
5
+ require 'update_all_scope'
6
+ require 'atomically/on_duplicate_sql_service'
7
+ require 'atomically/adapter_check_service'
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)
9
+ require 'atomically/patches/none' if not ActiveRecord::Base.respond_to?(:none)
10
+ require 'atomically/patches/from' if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new('4.0.0')
11
+
12
+ class Atomically::QueryService
13
+ DEFAULT_CONFLICT_TARGETS = [:id].freeze
14
+
15
+ def initialize(klass, relation: nil, model: nil)
16
+ @klass = klass
17
+ @relation = relation || @klass
18
+ @model = model
19
+ end
20
+
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))
23
+ end
24
+
25
+ def pay_all(hash, update_columns, primary_key: :id) # { id => pay_count }
26
+ return 0 if hash.blank?
27
+
28
+ update_columns = update_columns.map(&method(:quote_column))
29
+
30
+ query = hash.inject(@klass.none) do |relation, (id, pay_count)|
31
+ condition = @relation.where(primary_key => id)
32
+ update_columns.each{|s| condition = condition.where("#{s} >= ?", pay_count) }
33
+ next relation.or(condition)
34
+ end
35
+
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?
38
+ update_sqls = update_columns.map.with_index do |column, idx|
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
44
+ next "#{column} = #{column} + #{value}"
45
+ end
46
+
47
+ return where_all_can_be_updated(query, hash.size).update_all(update_sqls.join(', '))
48
+ end
49
+
50
+ def update_all(expected_size, *args)
51
+ where_all_can_be_updated(@relation, expected_size).update_all(*args)
52
+ end
53
+
54
+ def update(attrs, options = {})
55
+ from = options[:from] || :not_set
56
+ success = update_and_return_number_of_updated_rows(attrs, from) == 1
57
+
58
+ if success
59
+ assign_without_changes(attrs)
60
+ @model.send(:clear_attribute_changes, @model.changes.keys)
61
+ end
62
+
63
+ return success
64
+ end
65
+
66
+ # ==== Parameters
67
+ #
68
+ # * +counters+ - A Hash containing the names of the fields
69
+ # to update as keys and the amount to update the field by as values.
70
+ def decrement_unsigned_counters(counters)
71
+ result = open_update_all_scope do
72
+ counters.each do |field, amount|
73
+ where("#{field} >= ?", amount).update("#{field} = #{field} - ?", amount) if amount > 0
74
+ end
75
+ end
76
+ return (result == 1)
77
+ end
78
+
79
+ def update_all_and_get_ids(*args)
80
+ if adapter_check_service.pg?
81
+ scope = UpdateAllScope::UpdateAllScope.new(model: @model, relation: @relation.where(''))
82
+ scope.update(*args)
83
+ return @klass.connection.execute("#{scope.to_sql} RETURNING id", "#{@klass} Update All").map{|s| s['id'].to_i }
84
+ end
85
+
86
+ ids = nil
87
+ id_column = quote_column_with_table(:id)
88
+ @klass.transaction do
89
+ @relation.connection.execute('SET @ids := NULL')
90
+ @relation.where("(SELECT @ids := CONCAT_WS(',', #{id_column}, @ids))").update_all(*args) # 撈出有真的被更新的 id,用逗號串在一起
91
+ ids = @klass.from(nil).pluck(Arel.sql('@ids')).first
92
+ end
93
+ return ids.try{|s| s.split(',').map(&:to_i).uniq.sort } || [] # 將 id 從字串取出來 @id 的格式範例: '1,4,12'
94
+ end
95
+
96
+ private
97
+
98
+ def adapter_check_service
99
+ @adapter_check_service ||= Atomically::AdapterCheckService.new(@klass)
100
+ end
101
+
102
+ def on_duplicate_key_plus_sql(columns, conflict_target)
103
+ service = Atomically::OnDuplicateSqlService.new(@klass, columns)
104
+ return service.mysql_quote_columns_for_plus.join(', ') if adapter_check_service.mysql?
105
+ return {
106
+ conflict_target: conflict_target,
107
+ columns: service.pg_quote_columns_for_plus.join(', '),
108
+ }
109
+ end
110
+
111
+ def quote_column_with_table(column)
112
+ "#{@klass.quoted_table_name}.#{quote_column(column)}"
113
+ end
114
+
115
+ def quote_column(column)
116
+ @klass.connection.quote_column_name(column)
117
+ end
118
+
119
+ def sanitize(value)
120
+ @klass.connection.quote(value)
121
+ end
122
+
123
+ def where_all_can_be_updated(query, expected_size)
124
+ query.where("(#{@klass.from(query.where('')).select('COUNT(*)').to_sql}) = ?", expected_size)
125
+ end
126
+
127
+ def update_and_return_number_of_updated_rows(attrs, from_value)
128
+ model = @model
129
+ return open_update_all_scope do
130
+ update(updated_at: Time.now)
131
+
132
+ model.changes.each do |column, (_old_value, new_value)|
133
+ update(column => new_value)
134
+ end
135
+
136
+ attrs.each do |column, value|
137
+ old_value = (from_value == :not_set ? model[column] : from_value)
138
+ where(column => old_value).update(column => value) if old_value != value
139
+ end
140
+ end
141
+ end
142
+
143
+ def open_update_all_scope(&block)
144
+ return 0 if @model == nil
145
+ scope = UpdateAllScope::UpdateAllScope.new(model: @model)
146
+ scope.instance_exec(&block)
147
+ return scope.do_query!
148
+ end
149
+
150
+ def assign_without_changes(attributes)
151
+ @model.assign_attributes(attributes)
152
+ @model.send(:clear_attribute_changes, attributes.keys)
153
+ end
154
+ end
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
- module Atomically
4
- VERSION = '1.1.2'
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Atomically
4
+ VERSION = '1.1.3'
5
+ end
data/lib/atomically.rb CHANGED
@@ -1,7 +1,7 @@
1
- # frozen_string_literal: true
2
-
3
- require 'atomically/version'
4
- require 'atomically/active_record/extension'
5
-
6
- module Atomically
7
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'atomically/version'
4
+ require 'atomically/active_record/extension'
5
+
6
+ module Atomically
7
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomically
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - khiav reoy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-01 00:00:00.000000000 Z
11
+ date: 2023-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -192,9 +192,9 @@ executables: []
192
192
  extensions: []
193
193
  extra_rdoc_files: []
194
194
  files:
195
+ - ".github/workflows/ruby.yml"
195
196
  - ".gitignore"
196
197
  - ".rubocop.yml"
197
- - ".travis.yml"
198
198
  - CHANGELOG.md
199
199
  - CODE_OF_CONDUCT.md
200
200
  - LICENSE
@@ -243,7 +243,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
243
243
  - !ruby/object:Gem::Version
244
244
  version: '0'
245
245
  requirements: []
246
- rubygems_version: 3.0.3
246
+ rubygems_version: 3.2.14
247
247
  signing_key:
248
248
  specification_version: 4
249
249
  summary: An ActiveRecord extension for writing commonly useful atomic SQL statements
data/.travis.yml DELETED
@@ -1,65 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- rvm:
4
- - 2.2
5
- - 2.6
6
- - 2.7
7
- services:
8
- - mysql
9
- addons:
10
- postgresql: "9.6"
11
- env:
12
- global:
13
- - CC_TEST_REPORTER_ID=12e1facab2e8910c9b9d6b9e6870c5544a5c44a1bef25cc6638fd132aa4af6b4
14
- matrix:
15
- - DB=mysql
16
- - DB=pg
17
- gemfile:
18
- - gemfiles/3.2.gemfile
19
- - gemfiles/4.2.gemfile
20
- - gemfiles/5.0.gemfile
21
- - gemfiles/5.1.gemfile
22
- - gemfiles/5.2.gemfile
23
- - gemfiles/6.0.gemfile
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
38
- exclude:
39
- - gemfile: gemfiles/3.2.gemfile
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
47
- before_install:
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
52
- - gem --version
53
- - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
54
- - chmod +x ./cc-test-reporter
55
- - ./cc-test-reporter before-build
56
- before_script:
57
- - mysql -V
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
61
- script:
62
- - bundle exec rake test
63
- after_script:
64
- - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
65
-