activerecord-update 0.0.1
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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.rubocop.yml +16 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +17 -0
- data/Gemfile +3 -0
- data/README.md +59 -0
- data/Rakefile +33 -0
- data/activerecord-update.gemspec +50 -0
- data/bin/console +12 -0
- data/bin/setup +8 -0
- data/lib/activerecord-update.rb +16 -0
- data/lib/activerecord-update/active_record/base.rb +614 -0
- data/lib/activerecord-update/active_record/update/result.rb +59 -0
- data/lib/activerecord-update/version.rb +5 -0
- metadata +270 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ea8267a673e2b98e469272b396c95715d72293a0
|
4
|
+
data.tar.gz: 758e3c003b4711eb333f99dc89488a9f84dbf6e0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6bd413d5d665399e192852db292c1624ea7d6b5e95ab86bc4956aeb5ad56a1aec83f3c1ecb6c4920f1628fdb07892ddbf39e68d46d571bb92552f01fc427bf2a
|
7
|
+
data.tar.gz: 65ffd2ccf31fab618f5cf5e316691321cbeebdaf33a785a495c1db51f3b2a75658c95a9f5d16092c3b4b141bdc4d958a036897b45461172b34cf8a48f1092184
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
Style/Documentation:
|
2
|
+
Enabled: false
|
3
|
+
|
4
|
+
Style/SignalException:
|
5
|
+
Enabled: false
|
6
|
+
|
7
|
+
Metrics/ClassLength:
|
8
|
+
Enabled: false
|
9
|
+
|
10
|
+
Style/MultilineMethodCallIndentation:
|
11
|
+
EnforcedStyle: indented
|
12
|
+
|
13
|
+
Metrics/BlockLength:
|
14
|
+
Exclude:
|
15
|
+
- activerecord-update.gemspec
|
16
|
+
- spec/**/*_spec.rb
|
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
activerecord-update
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.5
|
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
sudo: false
|
2
|
+
language: ruby
|
3
|
+
|
4
|
+
rvm:
|
5
|
+
- 2.3.3
|
6
|
+
- 2.1.5
|
7
|
+
|
8
|
+
cache: bundler
|
9
|
+
before_install: gem install bundler -v 1.13.6
|
10
|
+
|
11
|
+
before_script:
|
12
|
+
- bundle exec rake db:create
|
13
|
+
- bundle exec rake db:migrate
|
14
|
+
|
15
|
+
script:
|
16
|
+
- bundle exec rubocop
|
17
|
+
- bundle exec rspec
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# activerecord-update [](https://travis-ci.org/jacob-carlborg/activerecord-update)
|
2
|
+
|
3
|
+
activerecord-update is a library for doing batch updates using ActiveRecord.
|
4
|
+
Currently it only supports PostgreSQL.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'activerecord-update'
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install activerecord-update
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
All ActiveRecord classes have two new class methods added, `update_records` and
|
25
|
+
`update_records!`. Both expect an array of ActiveRecord models. The difference
|
26
|
+
between these methods is that `update_records!` will raise an error if any
|
27
|
+
validations fail or any stale objects are identified, if optimistic locking is
|
28
|
+
enabled.
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
class Book
|
32
|
+
validates :title, presence: true
|
33
|
+
end
|
34
|
+
|
35
|
+
books = Book.find(1, 2, 3)
|
36
|
+
books.each_with_index { |book, index| book.title = "foo_#{index}" }
|
37
|
+
Book.update_records(books)
|
38
|
+
|
39
|
+
books.each { |book| book.title = nil }
|
40
|
+
Book.update_records!(books) # will raise an ActiveRecord::RecordInvalid error
|
41
|
+
```
|
42
|
+
|
43
|
+
## Development
|
44
|
+
|
45
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then,
|
46
|
+
run `rake spec` to run the tests. You can also run `bin/console` for an
|
47
|
+
interactive prompt that will allow you to experiment.
|
48
|
+
|
49
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
50
|
+
To release a new version, update the version number in `version.rb`, and then
|
51
|
+
run `bundle exec rake release`, which will create a git tag for the version,
|
52
|
+
push git commits and tags, and push the `.gem` file to
|
53
|
+
[rubygems.org](https://rubygems.org).
|
54
|
+
|
55
|
+
## Contributing
|
56
|
+
|
57
|
+
Bug reports and pull requests are welcome on GitHub at
|
58
|
+
https://github.com/jacob-carlborg/activerecord-update.
|
59
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
|
6
|
+
task default: :spec
|
7
|
+
|
8
|
+
require 'activerecord-update'
|
9
|
+
require ActiveRecord::Update.root.join('spec/support/database')
|
10
|
+
|
11
|
+
namespace :db do
|
12
|
+
desc 'Creates the database from spec/db/database.yml'
|
13
|
+
task :create do
|
14
|
+
name = ActiveRecord::Update.database.name
|
15
|
+
sh "psql -c 'create database #{name};' -U postgres"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Drops the database from spec/db/database.yml'
|
19
|
+
task :drop do
|
20
|
+
name = ActiveRecord::Update.database.name
|
21
|
+
sh "psql -c 'drop database #{name};' -U postgres"
|
22
|
+
end
|
23
|
+
|
24
|
+
desc 'Migrate the database'
|
25
|
+
task :migrate do
|
26
|
+
ActiveRecord::Update.database.migrate
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
desc 'Start the console'
|
31
|
+
task :c do
|
32
|
+
exec './bin/console'
|
33
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'activerecord-update/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'activerecord-update'
|
8
|
+
spec.version = ActiveRecord::Update::VERSION
|
9
|
+
spec.authors = ['Jacob Carlborg']
|
10
|
+
spec.email = ['doob@me.se']
|
11
|
+
|
12
|
+
spec.summary = 'Batch updating for ActiveRecord models'
|
13
|
+
spec.description = 'Batch updating for ActiveRecord models'
|
14
|
+
spec.homepage = 'https://github.com/jacob-carlborg/activerecord-update'
|
15
|
+
|
16
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the
|
17
|
+
# 'allowed_push_host' to allow pushing to a single host or delete this section
|
18
|
+
# to allow pushing to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
21
|
+
else
|
22
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
23
|
+
'public gem pushes.'
|
24
|
+
end
|
25
|
+
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
27
|
+
f.match(%r{^(test|spec|features)/})
|
28
|
+
end
|
29
|
+
|
30
|
+
spec.bindir = 'exe'
|
31
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
32
|
+
spec.require_paths = ['lib']
|
33
|
+
|
34
|
+
spec.add_dependency 'activerecord', '4.1.9'
|
35
|
+
spec.add_dependency 'activesupport', '4.1.9'
|
36
|
+
|
37
|
+
spec.add_development_dependency 'database_cleaner', '~> 1.5'
|
38
|
+
spec.add_development_dependency 'bundler', '~> 1.13'
|
39
|
+
spec.add_development_dependency 'pg', '0.18.2'
|
40
|
+
spec.add_development_dependency 'pry-rescue', '~> 1.4'
|
41
|
+
spec.add_development_dependency 'pry-stack_explorer', '~> 0.4.9'
|
42
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
43
|
+
spec.add_development_dependency 'redcarpet', '~> 3.3'
|
44
|
+
spec.add_development_dependency 'rspec', '~> 3.5'
|
45
|
+
spec.add_development_dependency 'rubocop', '0.46.0'
|
46
|
+
spec.add_development_dependency 'ruby-prof', '~> 0.16'
|
47
|
+
spec.add_development_dependency 'simplecov', '~> 0.12'
|
48
|
+
spec.add_development_dependency 'timecop', '~> 0.8'
|
49
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
50
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'pry'
|
5
|
+
require 'activerecord-update'
|
6
|
+
require_relative '../spec/support/database'
|
7
|
+
require_relative '../spec/models/record'
|
8
|
+
|
9
|
+
ActiveRecord::Update.database.connect
|
10
|
+
at_exit { ActiveRecord::Update.database.disconnect }
|
11
|
+
|
12
|
+
Pry.start
|
data/bin/setup
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# rubocop:disable Style/FileName
|
2
|
+
|
3
|
+
require 'active_support/core_ext/string/strip'
|
4
|
+
require 'active_record'
|
5
|
+
|
6
|
+
require 'activerecord-update/version'
|
7
|
+
require 'activerecord-update/active_record/base'
|
8
|
+
require 'activerecord-update/active_record/update/result'
|
9
|
+
|
10
|
+
module ActiveRecord
|
11
|
+
module Update
|
12
|
+
def self.root
|
13
|
+
@root ||= Pathname.new(File.dirname(__FILE__)).join('..')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,614 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class Base
|
3
|
+
UPDATE_RECORDS_SQL_TEMPLATE = <<-SQL.strip_heredoc.strip.freeze
|
4
|
+
UPDATE %{table} SET
|
5
|
+
%{set_columns}
|
6
|
+
FROM (
|
7
|
+
VALUES
|
8
|
+
%{type_casts},
|
9
|
+
%{values}
|
10
|
+
)
|
11
|
+
AS %{alias}(%{columns})
|
12
|
+
WHERE %{table}.%{primary_key} = %{alias}.%{primary_key}
|
13
|
+
SQL
|
14
|
+
|
15
|
+
private_constant :UPDATE_RECORDS_SQL_TEMPLATE
|
16
|
+
|
17
|
+
UPDATE_RECORDS_SQL_LOCKING_CONDITION = <<-SQL.strip_heredoc.strip.freeze
|
18
|
+
AND %{table}.%{locking_column} = %{alias}.%{prev_locking_column}
|
19
|
+
SQL
|
20
|
+
|
21
|
+
private_constant :UPDATE_RECORDS_SQL_LOCKING_CONDITION
|
22
|
+
|
23
|
+
UPDATE_RECORDS_SQL_FOOTER = <<-SQL.strip_heredoc.strip.freeze
|
24
|
+
RETURNING %{table}.%{primary_key}
|
25
|
+
SQL
|
26
|
+
|
27
|
+
private_constant :UPDATE_RECORDS_SQL_FOOTER
|
28
|
+
|
29
|
+
class << self
|
30
|
+
# Updates a list of records in a single batch.
|
31
|
+
#
|
32
|
+
# This is more efficient than calling `ActiveRecord::Base#save` multiple
|
33
|
+
# times.
|
34
|
+
#
|
35
|
+
# * Only records that have changed will be updated
|
36
|
+
# * All new records will be ignored
|
37
|
+
# * Validations will be performed for all the records
|
38
|
+
# * Only the records for which the validations pass will be updated
|
39
|
+
#
|
40
|
+
# * The union of the changed attributes for all the records will be
|
41
|
+
# updated
|
42
|
+
#
|
43
|
+
# * The `updated_at` attribute will be updated for all the records that
|
44
|
+
# where updated
|
45
|
+
#
|
46
|
+
# * All the given records should be of the same type and the same type
|
47
|
+
# as the class this method is called on
|
48
|
+
#
|
49
|
+
# * If the model is using optimistic locking, that is honored
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# Model.update_records(array_of_models)
|
53
|
+
#
|
54
|
+
# @param records [<ActiveRecord::Base>] the records to be updated
|
55
|
+
#
|
56
|
+
# @return [ActiveRecord::Update::Result] the ID's of the records that
|
57
|
+
# were updated and the records that failed to validate
|
58
|
+
#
|
59
|
+
# @see ActiveRecord::Update::Result
|
60
|
+
# @see .update_records!
|
61
|
+
def update_records(records)
|
62
|
+
_update_records(
|
63
|
+
records,
|
64
|
+
raise_on_validation_failure: false,
|
65
|
+
raise_on_stale_objects: false
|
66
|
+
)
|
67
|
+
end
|
68
|
+
|
69
|
+
# (see .update_records)
|
70
|
+
#
|
71
|
+
# The difference compared to {.update_records} is that this method will
|
72
|
+
# raise on validation failures. It will pick the first failing record and
|
73
|
+
# raise the error based that record's failing validations.
|
74
|
+
#
|
75
|
+
# If an `ActiveRecord::RecordInvalid` error is raised none of the records
|
76
|
+
# will be updated, including the valid records.
|
77
|
+
#
|
78
|
+
# If an `ActiveRecord::StaleObjectError` error is raised, some of the
|
79
|
+
# records might have been updated and is reflected in the
|
80
|
+
# {ActiveRecord::Update::Result#ids} and
|
81
|
+
# {ActiveRecord::Update::Result#stale_objects} attributes on the return
|
82
|
+
# value.
|
83
|
+
#
|
84
|
+
# @raise [ActiveRecord::RecordInvalid] if any records failed to validate
|
85
|
+
#
|
86
|
+
# @raise [ActiveRecord::StaleObjectError] if optimistic locking is enabled
|
87
|
+
# and there were stale objects
|
88
|
+
#
|
89
|
+
# @see .update_records
|
90
|
+
def update_records!(records)
|
91
|
+
_update_records(
|
92
|
+
records,
|
93
|
+
raise_on_validation_failure: true,
|
94
|
+
raise_on_stale_objects: true
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# rubocop:disable Metrics/MethodLength
|
101
|
+
|
102
|
+
# (see .update_records)
|
103
|
+
#
|
104
|
+
# @param raise_on_validation_failure [Boolean] if `true`, an error will be
|
105
|
+
# raised for any validation failures
|
106
|
+
#
|
107
|
+
# @param raise_on_stale_objects [Boolean] if `true`, an error will be
|
108
|
+
# raised if optimistic locking is used and there are stale objects
|
109
|
+
#
|
110
|
+
# @see .update_records
|
111
|
+
def _update_records(
|
112
|
+
records,
|
113
|
+
raise_on_validation_failure:,
|
114
|
+
raise_on_stale_objects:
|
115
|
+
)
|
116
|
+
|
117
|
+
changed = changed_records(records)
|
118
|
+
valid, failed = validate_records(changed, raise_on_validation_failure)
|
119
|
+
return build_result(valid, failed, []) if valid.empty?
|
120
|
+
|
121
|
+
timestamp = current_time
|
122
|
+
query = sql_for_update_records(valid, timestamp)
|
123
|
+
ids = perform_update_records_query(query, primary_key)
|
124
|
+
result = build_result(valid, failed, ids)
|
125
|
+
validate_result(result, raise_on_stale_objects)
|
126
|
+
|
127
|
+
update_timestamp(valid, timestamp)
|
128
|
+
mark_changes_applied(valid)
|
129
|
+
result
|
130
|
+
end
|
131
|
+
# rubocop:enable Metrics/MethodLength
|
132
|
+
|
133
|
+
# Returns the given records that are not new records and have changed.
|
134
|
+
#
|
135
|
+
# @param records [<ActiveRecord::Base>] the records to filter
|
136
|
+
# @return that records that are not new records and have changed
|
137
|
+
def changed_records(records)
|
138
|
+
records.reject(&:new_record?).select(&:changed?)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Validates the given records.
|
142
|
+
#
|
143
|
+
# @param records [<ActiveModel::Validations>] the records to validate
|
144
|
+
# @param raise_on_validation_failure [Boolean] if `true`, an error will be
|
145
|
+
# raised for any validation failures
|
146
|
+
#
|
147
|
+
# @return [(<ActiveModel::Validations>, <ActiveModel::Validations>)]
|
148
|
+
# a tuple where the first element is an array of records that are valid.
|
149
|
+
# The second element is an array of the invalid records
|
150
|
+
#
|
151
|
+
# @raise [ActiveRecord::RecordInvalid] if `raise_on_validation_failure`
|
152
|
+
# is `true` and there are validation failures
|
153
|
+
def validate_records(records, raise_on_validation_failure)
|
154
|
+
valid, invalid = records.partition(&:valid?)
|
155
|
+
|
156
|
+
if raise_on_validation_failure && invalid.any?
|
157
|
+
raise RecordInvalid, invalid.first
|
158
|
+
end
|
159
|
+
|
160
|
+
[valid, invalid]
|
161
|
+
end
|
162
|
+
|
163
|
+
# Builds the SQL query used by the {#update_records} method.
|
164
|
+
#
|
165
|
+
# @example
|
166
|
+
# class Model
|
167
|
+
# include ActiveModel::Model
|
168
|
+
# include ActiveModel::Dirty
|
169
|
+
#
|
170
|
+
# attr_accessor :id, :foo, :bar
|
171
|
+
# define_attribute_methods :id, :foo, :bar
|
172
|
+
#
|
173
|
+
# def slice(*keys)
|
174
|
+
# attributes = { id: id, foo: foo, bar: bar }
|
175
|
+
# hash = ActiveSupport::HashWithIndifferentAccess.new(attributes)
|
176
|
+
# hash.slice(*keys)
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# def id=(value)
|
180
|
+
# id_will_change! unless value == @id
|
181
|
+
# @id = value
|
182
|
+
# end
|
183
|
+
#
|
184
|
+
# def foo=(value)
|
185
|
+
# foo_will_change! unless value == @foo
|
186
|
+
# @foo = value
|
187
|
+
# end
|
188
|
+
#
|
189
|
+
# def bar=(value)
|
190
|
+
# bar_will_change! unless value == @bar
|
191
|
+
# @bar = value
|
192
|
+
# end
|
193
|
+
# end
|
194
|
+
#
|
195
|
+
# record1 = Model.new(id: 1, foo: 4, bar: 5)
|
196
|
+
# record2 = Model.new(id: 2, foo: 2, bar: 3)
|
197
|
+
# records = [record1, record2]
|
198
|
+
#
|
199
|
+
# ActiveRecord::Base.send(:sql_for_update_records, records)
|
200
|
+
# # =>
|
201
|
+
# # UPDATE "foos" SET
|
202
|
+
# # "id" = "foos_2"."id", "foo" = "foos_2"."foo", "bar" = "foos_2"."bar
|
203
|
+
# # FROM (
|
204
|
+
# # VALUES (1, 4, 5), (2, 2, 3)
|
205
|
+
# # )
|
206
|
+
# # AS foos_2("id", "foo", "bar")
|
207
|
+
# # WHERE "foos"."id" = foos_2."id"
|
208
|
+
# # RETURNING "foos"."id";
|
209
|
+
#
|
210
|
+
# @param records [<ActiveRecord::Base>] the records that have changed
|
211
|
+
# @param timestamp [Time] the timestamp used for the `updated_at` column
|
212
|
+
#
|
213
|
+
# @return the SQL query for the #{update_records} method
|
214
|
+
#
|
215
|
+
# @see #update_records
|
216
|
+
# rubocop:disable Metrics/MethodLength
|
217
|
+
# rubocop:disable Metrics/AbcSize
|
218
|
+
def sql_for_update_records(records, timestamp)
|
219
|
+
attributes = changed_attributes(records)
|
220
|
+
quoted_changed_attributes = changed_attributes_for_sql(
|
221
|
+
attributes, quoted_table_alias
|
222
|
+
)
|
223
|
+
|
224
|
+
attributes = all_attributes(attributes)
|
225
|
+
casts = type_casts(attributes)
|
226
|
+
values = changed_values(records, attributes, timestamp)
|
227
|
+
quoted_values = values_for_sql(values)
|
228
|
+
quoted_column_names = column_names_for_sql(attributes)
|
229
|
+
template = build_sql_template
|
230
|
+
|
231
|
+
options = build_format_options(
|
232
|
+
table: quoted_table_name,
|
233
|
+
set_columns: quoted_changed_attributes,
|
234
|
+
type_casts: casts,
|
235
|
+
values: quoted_values,
|
236
|
+
alias: quoted_table_alias,
|
237
|
+
columns: quoted_column_names,
|
238
|
+
primary_key: quoted_primary_key
|
239
|
+
)
|
240
|
+
|
241
|
+
format(template, options)
|
242
|
+
end
|
243
|
+
# rubocop:enable Metrics/AbcSize
|
244
|
+
# rubocop:enable Metrics/MethodLength
|
245
|
+
|
246
|
+
# @return [Time] the current time in the ActiveRecord timezone.
|
247
|
+
def current_time
|
248
|
+
default_timezone == :utc ? Time.now.getutc : Time.now
|
249
|
+
end
|
250
|
+
|
251
|
+
# @return [String] the table alias quoted.
|
252
|
+
def quoted_table_alias
|
253
|
+
@quoted_table_alias ||=
|
254
|
+
connection.quote_table_name(arel_table.alias.name)
|
255
|
+
end
|
256
|
+
|
257
|
+
# Quotes/escapes the given column value to prevent SQL injection attacks.
|
258
|
+
#
|
259
|
+
# This is an PostgreSQL specific method which properly handles quoting of
|
260
|
+
# `true` and `false`. The built-in ActiveRecord quote method will convert
|
261
|
+
# these values to `"'t'"` and `"'f'"`, which will not work for
|
262
|
+
# {.update_records}. This method will convert these values to `"TRUE"` and
|
263
|
+
# `"FALSE"` instead. All other values are delegated to
|
264
|
+
# `ActiveRecord::ConnectionAdapters::Quoting#quote`.
|
265
|
+
#
|
266
|
+
# @param value [Object] the value to escape
|
267
|
+
# @return [String] the quoted value
|
268
|
+
def quote(value)
|
269
|
+
case value
|
270
|
+
when true, false
|
271
|
+
value ? 'TRUE' : 'FALSE'
|
272
|
+
else
|
273
|
+
connection.quote(value)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Returns the changed attribute names of the given records.
|
278
|
+
#
|
279
|
+
# When locking is enabled the locking column will be inserted as well in
|
280
|
+
# the return value.
|
281
|
+
#
|
282
|
+
# @param records [<ActiveModel::Dirty>] the records to return the changed
|
283
|
+
# attributes for
|
284
|
+
#
|
285
|
+
# @return [Set<String>] a list of the names of the attributes that have
|
286
|
+
# changed
|
287
|
+
def changed_attributes(records)
|
288
|
+
changed = records.flat_map(&:changed)
|
289
|
+
return changed if changed.empty?
|
290
|
+
|
291
|
+
attrs = changed.dup << 'updated_at'
|
292
|
+
attrs << locking_column if locking_enabled?
|
293
|
+
attrs.tap(&:uniq!)
|
294
|
+
end
|
295
|
+
|
296
|
+
# Returns the given changed attributes formatted for SQL.
|
297
|
+
#
|
298
|
+
# @param changed_attributes [Set<String>] the attributes that have changed
|
299
|
+
# @param table_alias [String] an alias for the table name
|
300
|
+
#
|
301
|
+
# @return [String] the changed attributes formatted for SQL
|
302
|
+
# @raise [ArgumentError] if the given list is `nil` or empty
|
303
|
+
def changed_attributes_for_sql(changed_attributes, table_alias)
|
304
|
+
if changed_attributes.blank?
|
305
|
+
raise ArgumentError, 'No changed attributes given'
|
306
|
+
end
|
307
|
+
|
308
|
+
changed_attributes
|
309
|
+
.map { |e| connection.quote_column_name(e) }
|
310
|
+
.map { |e| "#{e} = #{table_alias}.#{e}" }.join(', ')
|
311
|
+
end
|
312
|
+
|
313
|
+
# @return [<String>] all attributes, that is, the given attributes plus
|
314
|
+
# some extra, like the primary key.
|
315
|
+
def all_attributes(attributes)
|
316
|
+
[primary_key] + attributes
|
317
|
+
end
|
318
|
+
|
319
|
+
# Returns a row used for typecasting the values that will be updated.
|
320
|
+
#
|
321
|
+
# This is needed because many types don't have a specific literal syntax
|
322
|
+
# and are instead using the string literal syntax. This will cause type
|
323
|
+
# mismatches because there's not context, which is otherwise present for
|
324
|
+
# regular inserts, for the values to infer the types from.
|
325
|
+
#
|
326
|
+
# When locking is enabled a virtual prev locking column is inserted,
|
327
|
+
# called `'prev_' + locking_column`, at the second position, after the
|
328
|
+
# primary key. This column is used in the where condition to implement
|
329
|
+
# the optimistic locking feature.
|
330
|
+
#
|
331
|
+
# @example
|
332
|
+
# ActiveRecord::Base.send(:type_casts, %w(id foo bar))
|
333
|
+
# # => (NULL::integer, NULL::character varying(255), NULL::boolean)
|
334
|
+
#
|
335
|
+
# @param column_names [Set<String>] the name of the columns
|
336
|
+
#
|
337
|
+
# @raise [ArgumentError]
|
338
|
+
# * If the given list of column names is `nil` or empty
|
339
|
+
def type_casts(column_names)
|
340
|
+
raise ArgumentError, 'No column names given' if column_names.blank?
|
341
|
+
|
342
|
+
type_casts = column_names.dup
|
343
|
+
|
344
|
+
# This is the virtual prev locking column. We're using the same name as
|
345
|
+
# the locking column since the column is virtual it does not exist in
|
346
|
+
# `columns_hash` hash. That works fine since we're only interested in
|
347
|
+
# the SQL type, which will always be the same for the locking and prev
|
348
|
+
# locking columns.
|
349
|
+
type_casts.insert(1, locking_column) if locking_enabled?
|
350
|
+
type_casts.map! { |n| 'NULL::' + columns_hash[n].sql_type }
|
351
|
+
|
352
|
+
'(' + type_casts.join(', ') + ')'
|
353
|
+
end
|
354
|
+
|
355
|
+
# rubocop:disable Metrics/MethodLength
|
356
|
+
|
357
|
+
# Returns the values of the given records that have changed.
|
358
|
+
#
|
359
|
+
# @example
|
360
|
+
# class Model
|
361
|
+
# include ActiveModel::Model
|
362
|
+
#
|
363
|
+
# attr_accessor :id, :foo, :bar
|
364
|
+
#
|
365
|
+
# def slice(*keys)
|
366
|
+
# attributes = { id: id, foo: foo, bar: bar }
|
367
|
+
# hash = ActiveSupport::HashWithIndifferentAccess.new(attributes)
|
368
|
+
# hash.slice(*keys)
|
369
|
+
# end
|
370
|
+
# end
|
371
|
+
#
|
372
|
+
# record1 = Model.new(id: 1, foo: 3)
|
373
|
+
# record2 = Model.new(id: 2, bar: 4)
|
374
|
+
# records = [record1, record2]
|
375
|
+
#
|
376
|
+
# changed_attributes = Set.new(%w(id foo bar))
|
377
|
+
# ActiveRecord::Base.send(
|
378
|
+
# :changed_values, records, changed_attributes, Time.at(0)
|
379
|
+
# )
|
380
|
+
# # => [
|
381
|
+
# # [1, 3, nil, 1970-01-01 01:00:00 +0100],
|
382
|
+
# # [2, nil, 4, 1970-01-01 01:00:00 +0100]
|
383
|
+
# # ]
|
384
|
+
#
|
385
|
+
# @param records [<ActiveRecord::Base>] the records that have changed
|
386
|
+
#
|
387
|
+
# @param changed_attributes [Set<String>] the attributes that have
|
388
|
+
# changed, including the primary key, and the locking column, if locking
|
389
|
+
# is enabled
|
390
|
+
#
|
391
|
+
# @param updated_at [Time] the value of the updated_at column
|
392
|
+
#
|
393
|
+
# @return [<<Object>>] the changed values
|
394
|
+
#
|
395
|
+
# @raise [ArgumentError]
|
396
|
+
# * if the given list of records or changed attributes is `nil` or empty
|
397
|
+
def changed_values(records, changed_attributes, updated_at)
|
398
|
+
raise ArgumentError, 'No changed records given' if records.blank?
|
399
|
+
|
400
|
+
if changed_attributes.blank?
|
401
|
+
raise ArgumentError, 'No changed attributes given'
|
402
|
+
end
|
403
|
+
|
404
|
+
extract_changed_values = lambda do |record|
|
405
|
+
previous_lock_value = increment_lock(record)
|
406
|
+
|
407
|
+
# We're using `slice` instead of `changed_attributes` because we need
|
408
|
+
# to include all the changed attributes from all the changed records
|
409
|
+
# and not just the changed attributes for a given record.
|
410
|
+
values = record
|
411
|
+
.slice(*changed_attributes)
|
412
|
+
.merge('updated_at' => updated_at)
|
413
|
+
.values
|
414
|
+
|
415
|
+
locking_enabled? ? values.insert(1, previous_lock_value) : values
|
416
|
+
end
|
417
|
+
|
418
|
+
records.map(&extract_changed_values)
|
419
|
+
end
|
420
|
+
# rubocop:enable Metrics/MethodLength
|
421
|
+
|
422
|
+
# Increments the lock column of the given record if locking is enabled.
|
423
|
+
#
|
424
|
+
# @param [ActiveRecord::Base] the record to update the lock column for
|
425
|
+
# @return [void]
|
426
|
+
def increment_lock(record)
|
427
|
+
return unless locking_enabled?
|
428
|
+
|
429
|
+
lock_col = locking_column
|
430
|
+
previous_lock_value = record.send(lock_col).to_i
|
431
|
+
record.send(lock_col + '=', previous_lock_value + 1)
|
432
|
+
previous_lock_value
|
433
|
+
end
|
434
|
+
|
435
|
+
# Returns the values of the given records that have changed, formatted for
|
436
|
+
# SQL.
|
437
|
+
#
|
438
|
+
# @example
|
439
|
+
# changed_values = [
|
440
|
+
# [1, 3, 4, 1970-01-01 01:00:00 +0100],
|
441
|
+
# [2, 5, 6, 1970-01-01 01:00:00 +0100]
|
442
|
+
# ]
|
443
|
+
# ActiveRecord::Base.send(:values_for_sql, records)
|
444
|
+
# # => "(1, 3, NULL), (2, NULL, 4)"
|
445
|
+
#
|
446
|
+
# @param changed_values [<<Object>>] the values that have changed
|
447
|
+
# @return [String] the values formatted for SQL
|
448
|
+
#
|
449
|
+
# @raise [ArgumentError] if the given list of changed values is `nil` or
|
450
|
+
# empty
|
451
|
+
def values_for_sql(changed_values)
|
452
|
+
raise ArgumentError, 'No changed values given' if changed_values.blank?
|
453
|
+
|
454
|
+
changed_values
|
455
|
+
.map { |e| '(' + e.map { |b| quote(b) }.join(', ') + ')' }
|
456
|
+
.join(', ')
|
457
|
+
end
|
458
|
+
|
459
|
+
# Returns the name of the previous locking column.
|
460
|
+
#
|
461
|
+
# This column is used when locking is enabled. It's used in the where
|
462
|
+
# condition when looking for matching rows to update.
|
463
|
+
#
|
464
|
+
# @return [String] the name of the previous locking column
|
465
|
+
def prev_locking_column
|
466
|
+
@prev_locking_column ||= 'prev_' + locking_column
|
467
|
+
end
|
468
|
+
|
469
|
+
# Returns the given column names formatted for SQL.
|
470
|
+
#
|
471
|
+
# @example
|
472
|
+
# ActiveRecord::Base.send(:column_names_for_sql, %w(id foo bar))
|
473
|
+
# # => '"id", "foo", "bar"'
|
474
|
+
#
|
475
|
+
# @param column_names [<String>] the name of the columns
|
476
|
+
#
|
477
|
+
# @return [String] the column names formatted for SQL
|
478
|
+
#
|
479
|
+
# @raise [ArgumentError]
|
480
|
+
# * If the given list of column names is `nil` or empty
|
481
|
+
def column_names_for_sql(column_names)
|
482
|
+
raise ArgumentError, 'No column names given' if column_names.blank?
|
483
|
+
|
484
|
+
names = column_names.dup
|
485
|
+
names.insert(1, prev_locking_column) if locking_enabled?
|
486
|
+
names.map! { |e| connection.quote_column_name(e) }.join(', ')
|
487
|
+
end
|
488
|
+
|
489
|
+
# Builds the SQL template for the query.
|
490
|
+
#
|
491
|
+
# This method will choose the correct template depending on if locking is
|
492
|
+
# enabled or not.
|
493
|
+
#
|
494
|
+
# @return [String] the SQL template
|
495
|
+
def build_sql_template
|
496
|
+
template =
|
497
|
+
if locking_enabled?
|
498
|
+
UPDATE_RECORDS_SQL_TEMPLATE + "\n" +
|
499
|
+
UPDATE_RECORDS_SQL_LOCKING_CONDITION
|
500
|
+
else
|
501
|
+
UPDATE_RECORDS_SQL_TEMPLATE
|
502
|
+
end
|
503
|
+
|
504
|
+
template + "\n" + UPDATE_RECORDS_SQL_FOOTER
|
505
|
+
end
|
506
|
+
|
507
|
+
# Build the option hash used for the call to `format` in
|
508
|
+
# {Base#sql_for_update_records}.
|
509
|
+
#
|
510
|
+
# This method will add the format options to the given hash of options as
|
511
|
+
# necessary if locking is enabled.
|
512
|
+
#
|
513
|
+
# @param options [{ Symbol => String }] the format options
|
514
|
+
#
|
515
|
+
# @return [{ Symbol => String }] the format options
|
516
|
+
def build_format_options(options)
|
517
|
+
if locking_enabled?
|
518
|
+
prev_col_name = connection.quote_column_name(prev_locking_column)
|
519
|
+
col_name = connection.quote_column_name(locking_column)
|
520
|
+
|
521
|
+
options.merge(
|
522
|
+
locking_column: col_name,
|
523
|
+
prev_locking_column: prev_col_name
|
524
|
+
)
|
525
|
+
else
|
526
|
+
options
|
527
|
+
end
|
528
|
+
end
|
529
|
+
|
530
|
+
# Performs the given query and returns the result of the query.
|
531
|
+
#
|
532
|
+
# @param query [String] the query to perform
|
533
|
+
# @param primary_key [String] the primary key
|
534
|
+
#
|
535
|
+
# @return the result of the query, the primary keys of the records what
|
536
|
+
# were updated
|
537
|
+
def perform_update_records_query(query, primary_key)
|
538
|
+
primary_key_column = columns_hash[primary_key]
|
539
|
+
values = connection.execute(query).values.flatten
|
540
|
+
values.map! { |e| primary_key_column.type_cast(e) }
|
541
|
+
end
|
542
|
+
|
543
|
+
# Raises an exception if the given result contain any stale objects.
|
544
|
+
#
|
545
|
+
# @param result [ActiveRecord::Update::Result] the result to check if it
|
546
|
+
# contains stale objects
|
547
|
+
#
|
548
|
+
# @return [void]
|
549
|
+
def validate_result(result, raise_on_stale_objects)
|
550
|
+
return unless result.stale_objects?
|
551
|
+
record = result.stale_objects.first
|
552
|
+
return unless raise_on_stale_objects
|
553
|
+
raise ActiveRecord::StaleObjectError.new(record, 'update')
|
554
|
+
end
|
555
|
+
|
556
|
+
# Builds the result, returned from {#update_records}, based on the given
|
557
|
+
# arguments.
|
558
|
+
#
|
559
|
+
# @param valid [<ActiveRecord::Base>] the list of records which was
|
560
|
+
# successfully validate
|
561
|
+
#
|
562
|
+
# @param failed [<ActiveRecord::Base>] the list of records which failed to
|
563
|
+
# validate
|
564
|
+
#
|
565
|
+
# @param primary_keys [<Integer>] the list of primary keys that were
|
566
|
+
# update
|
567
|
+
def build_result(valid, failed, primary_keys)
|
568
|
+
stale_objects = extract_stale_objects(valid, primary_keys)
|
569
|
+
ActiveRecord::Update::Result.new(primary_keys, failed, stale_objects)
|
570
|
+
end
|
571
|
+
|
572
|
+
# Extracts the stale objects from the given list of records.
|
573
|
+
#
|
574
|
+
# Will always return an empty list if locking is not enabled for this
|
575
|
+
# class.
|
576
|
+
#
|
577
|
+
# @param records [<ActiveRecord::Base>] the list of records to extract
|
578
|
+
# the stale objects from
|
579
|
+
#
|
580
|
+
# @param primary_keys [<Integer>] the list of primary keys that were
|
581
|
+
# updated
|
582
|
+
#
|
583
|
+
# @return [<ActiveRecord::Base>] the stale objects
|
584
|
+
def extract_stale_objects(records, primary_keys)
|
585
|
+
return [] unless locking_enabled?
|
586
|
+
primary_key_set = primary_keys.to_set
|
587
|
+
records.reject { |e| primary_key_set.include?(e.send(primary_key)) }
|
588
|
+
end
|
589
|
+
|
590
|
+
# Updates the `updated_at` attribute for the given records.
|
591
|
+
#
|
592
|
+
# This will only updated the actual Ruby objects, the database should
|
593
|
+
# already have been updated by this point.
|
594
|
+
#
|
595
|
+
# @param records [<ActiveRecord::Base>] the records that should have their
|
596
|
+
# timestamp updated
|
597
|
+
#
|
598
|
+
# @return [void]
|
599
|
+
def update_timestamp(records, timestamp)
|
600
|
+
records.each { |e| e.updated_at = timestamp }
|
601
|
+
end
|
602
|
+
|
603
|
+
# Mark changes applied for the given records.
|
604
|
+
#
|
605
|
+
# @param records [<ActiveModel::Dirty>] the records to mark changes
|
606
|
+
# applied for
|
607
|
+
#
|
608
|
+
# @return [void]
|
609
|
+
def mark_changes_applied(records)
|
610
|
+
records.each { |e| e.send(:changes_applied) }
|
611
|
+
end
|
612
|
+
end
|
613
|
+
end
|
614
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Update
|
3
|
+
# This class represents the result return by the {Base#update_records}
|
4
|
+
# method. It contains the ID's of the records that were updated and the
|
5
|
+
# records that failed to validate and
|
6
|
+
#
|
7
|
+
# @see Base#update_records
|
8
|
+
class Result
|
9
|
+
# @return [<Integer>] the ID's of the records that were updated.
|
10
|
+
attr_reader :ids
|
11
|
+
|
12
|
+
# @return [<ActiveRecord::Base>] the records that failed to validate.
|
13
|
+
attr_reader :failed_records
|
14
|
+
|
15
|
+
# The records that failed to update due to being stale.
|
16
|
+
#
|
17
|
+
# Can only contain objects if optimistic locking is used.
|
18
|
+
#
|
19
|
+
# @return [<ActiveRecord::Base>] the stale objects
|
20
|
+
attr_reader :stale_objects
|
21
|
+
|
22
|
+
# Initialize the receiver.
|
23
|
+
#
|
24
|
+
# @param ids [<Integer>] the ID's of the records that were updated.
|
25
|
+
#
|
26
|
+
# @param failed_records [<ActiveRecord::Base>] the records that failed to
|
27
|
+
# validate
|
28
|
+
#
|
29
|
+
# @param stale_objects [<ActiveRecord::Base>] the records that failed to
|
30
|
+
# update to due being stale
|
31
|
+
def initialize(ids, failed_records, stale_objects)
|
32
|
+
@ids = ids
|
33
|
+
@failed_records = failed_records
|
34
|
+
@stale_objects = stale_objects
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Boolean] `true` if there were no failed records or stale
|
38
|
+
# objects.
|
39
|
+
def success?
|
40
|
+
!failed_records? && !stale_objects?
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Boolean] `true` if there were records that failed to validate.
|
44
|
+
def failed_records?
|
45
|
+
failed_records.any?
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Boolean] `true` if there were any updated records.
|
49
|
+
def updates?
|
50
|
+
ids.any?
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Boolean] `true` if there were any stale objects.
|
54
|
+
def stale_objects?
|
55
|
+
stale_objects.any?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,270 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-update
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jacob Carlborg
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-01-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.1.9
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.1.9
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 4.1.9
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 4.1.9
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: database_cleaner
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.5'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.5'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.13'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.13'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pg
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.18.2
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.18.2
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry-rescue
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.4'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.4'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry-stack_explorer
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.4.9
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.4.9
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rake
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '10.0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '10.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: redcarpet
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '3.3'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '3.3'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rspec
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '3.5'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '3.5'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rubocop
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - '='
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: 0.46.0
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - '='
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: 0.46.0
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: ruby-prof
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0.16'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0.16'
|
181
|
+
- !ruby/object:Gem::Dependency
|
182
|
+
name: simplecov
|
183
|
+
requirement: !ruby/object:Gem::Requirement
|
184
|
+
requirements:
|
185
|
+
- - "~>"
|
186
|
+
- !ruby/object:Gem::Version
|
187
|
+
version: '0.12'
|
188
|
+
type: :development
|
189
|
+
prerelease: false
|
190
|
+
version_requirements: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - "~>"
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0.12'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: timecop
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - "~>"
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0.8'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - "~>"
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0.8'
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: yard
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - "~>"
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0.9'
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - "~>"
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0.9'
|
223
|
+
description: Batch updating for ActiveRecord models
|
224
|
+
email:
|
225
|
+
- doob@me.se
|
226
|
+
executables: []
|
227
|
+
extensions: []
|
228
|
+
extra_rdoc_files: []
|
229
|
+
files:
|
230
|
+
- ".gitignore"
|
231
|
+
- ".rspec"
|
232
|
+
- ".rubocop.yml"
|
233
|
+
- ".ruby-gemset"
|
234
|
+
- ".ruby-version"
|
235
|
+
- ".travis.yml"
|
236
|
+
- Gemfile
|
237
|
+
- README.md
|
238
|
+
- Rakefile
|
239
|
+
- activerecord-update.gemspec
|
240
|
+
- bin/console
|
241
|
+
- bin/setup
|
242
|
+
- lib/activerecord-update.rb
|
243
|
+
- lib/activerecord-update/active_record/base.rb
|
244
|
+
- lib/activerecord-update/active_record/update/result.rb
|
245
|
+
- lib/activerecord-update/version.rb
|
246
|
+
homepage: https://github.com/jacob-carlborg/activerecord-update
|
247
|
+
licenses: []
|
248
|
+
metadata:
|
249
|
+
allowed_push_host: https://rubygems.org
|
250
|
+
post_install_message:
|
251
|
+
rdoc_options: []
|
252
|
+
require_paths:
|
253
|
+
- lib
|
254
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
255
|
+
requirements:
|
256
|
+
- - ">="
|
257
|
+
- !ruby/object:Gem::Version
|
258
|
+
version: '0'
|
259
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
260
|
+
requirements:
|
261
|
+
- - ">="
|
262
|
+
- !ruby/object:Gem::Version
|
263
|
+
version: '0'
|
264
|
+
requirements: []
|
265
|
+
rubyforge_project:
|
266
|
+
rubygems_version: 2.2.2
|
267
|
+
signing_key:
|
268
|
+
specification_version: 4
|
269
|
+
summary: Batch updating for ActiveRecord models
|
270
|
+
test_files: []
|