atomically 1.1.2 → 1.1.3

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