activerecord-update 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://travis-ci.org/jacob-carlborg/activerecord-update.svg?branch=master)](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: []
|