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