atomically 1.1.2 → 1.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +119 -0
- data/.gitignore +50 -50
- data/.rubocop.yml +1227 -1227
- data/CHANGELOG.md +37 -37
- data/CODE_OF_CONDUCT.md +48 -48
- data/LICENSE +21 -21
- data/LICENSE.txt +21 -21
- data/README.md +342 -342
- data/Rakefile +10 -10
- data/bin/console +14 -14
- data/bin/setup +8 -8
- data/gemfiles/3.2.gemfile +16 -16
- data/gemfiles/4.2.gemfile +16 -16
- data/gemfiles/5.0.gemfile +16 -16
- data/gemfiles/5.1.gemfile +16 -16
- data/gemfiles/5.2.gemfile +16 -16
- data/gemfiles/6.0.gemfile +15 -15
- data/lib/atomically/active_record/extension.rb +20 -20
- data/lib/atomically/adapter_check_service.rb +30 -30
- data/lib/atomically/on_duplicate_sql_service.rb +28 -28
- data/lib/atomically/patches/clear_attribute_changes.rb +17 -17
- data/lib/atomically/patches/from.rb +10 -10
- data/lib/atomically/patches/none.rb +9 -9
- data/lib/atomically/query_service.rb +154 -153
- data/lib/atomically/version.rb +5 -5
- data/lib/atomically.rb +7 -7
- metadata +4 -4
- data/.travis.yml +0 -65
@@ -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,
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
scope.
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
@relation.
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
return
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
scope.
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
@model.
|
152
|
-
|
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
|
data/lib/atomically/version.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Atomically
|
4
|
-
VERSION = '1.1.
|
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.
|
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:
|
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.
|
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
|
-
|