active_record_upsert 0.9.1 → 0.10.0
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/Gemfile.base +1 -1
- data/Gemfile.rails-5-2 +1 -1
- data/Gemfile.rails-6-0 +5 -0
- data/Gemfile.rails-6-1 +5 -0
- data/README.md +59 -4
- data/active_record_upsert.gemspec +1 -2
- data/lib/active_record_upsert.rb +1 -0
- data/lib/active_record_upsert/active_record/connection_adapters/postgresql/database_statements.rb +1 -1
- data/lib/active_record_upsert/active_record/persistence.rb +38 -16
- data/lib/active_record_upsert/compatibility/rails51.rb +21 -8
- data/lib/active_record_upsert/compatibility/rails60.rb +18 -0
- data/lib/active_record_upsert/version.rb +1 -1
- metadata +12 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d08fb14b2c2b66a287518710472aba53e095a4cc54c581a5c1dcf5ecd86088f6
|
4
|
+
data.tar.gz: 02b6d45767b9ecfeb8bcd1b27dcef517470d7f7747f522d3c14db445725ab3fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fefab82228fbfc68fbdd96912617ec7bdea1547be32b02800d46d2507bc0ba62c78001c21bf1cccb8b689ef1d5426432b8d49d40379dac8a044a326bcc158c9e
|
7
|
+
data.tar.gz: 249d793b70c0855b907a1036ca887a7c0c4b1ab68ee28a276d22b14bbdabd1ee6cf3973b11f6f818d1507fb0df7bf27475aca0c4d1e2e1f0e613be53ab85ecf0
|
data/Gemfile.base
CHANGED
data/Gemfile.rails-5-2
CHANGED
data/Gemfile.rails-6-0
ADDED
data/Gemfile.rails-6-1
ADDED
data/README.md
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
[](https://badge.fury.io/rb/active_record_upsert)
|
2
2
|
[](https://travis-ci.org/jesjos/active_record_upsert)
|
3
3
|
[](https://codeclimate.com/github/jesjos/active_record_upsert)
|
4
|
-
[](https://gemnasium.com/github.com/jesjos/active_record_upsert)
|
5
4
|
|
6
5
|
# ActiveRecordUpsert
|
7
6
|
|
8
|
-
Real upsert for PostgreSQL 9.5+ and Rails 5 / ActiveRecord 5
|
7
|
+
Real upsert for PostgreSQL 9.5+ and Rails 5+ / ActiveRecord 5+. Uses [ON CONFLICT DO UPDATE](http://www.postgresql.org/docs/9.5/static/sql-insert.html).
|
9
8
|
|
10
9
|
## Main points
|
11
10
|
|
@@ -15,12 +14,17 @@ Real upsert for PostgreSQL 9.5+ and Rails 5 / ActiveRecord 5. Uses [ON CONFLICT
|
|
15
14
|
|
16
15
|
## Prerequisites
|
17
16
|
|
18
|
-
- PostgreSQL 9.5+
|
19
|
-
- ActiveRecord
|
17
|
+
- PostgreSQL 9.5+ (that's when UPSERT support was added; see Wikipedia's [PostgreSQL Release History](https://en.wikipedia.org/wiki/PostgreSQL#Release_history))
|
18
|
+
- ActiveRecord >= 5
|
20
19
|
- For MRI: pg
|
21
20
|
|
22
21
|
- For JRuby: No support
|
23
22
|
|
23
|
+
### NB: Releases to avoid
|
24
|
+
|
25
|
+
Due to a broken build matrix, v0.9.2 and v0.9.3 are incompatible with Rails
|
26
|
+
< 5.2.1. [v0.9.4](https://github.com/jesjos/active_record_upsert/releases/tag/v0.9.4) fixed this issue.
|
27
|
+
|
24
28
|
## Installation
|
25
29
|
|
26
30
|
Add this line to your application's Gemfile:
|
@@ -119,6 +123,37 @@ r = MyRecord.new(id: 1, name: 'bar')
|
|
119
123
|
r.upsert!
|
120
124
|
```
|
121
125
|
|
126
|
+
### Gotcha with database defaults
|
127
|
+
|
128
|
+
When a table is defined with a database default for a field, this gotcha can occur when trying to explicitly upsert a record _to_ the default value (from a non-default value).
|
129
|
+
|
130
|
+
**Example**: a table called `hardwares` has a `prio` column with a default value.
|
131
|
+
|
132
|
+
┌─────────┬─────────┬───────┬
|
133
|
+
│ Column │ Type │Default│
|
134
|
+
├─────────┼─────────┼───────┼
|
135
|
+
│ id │ integer │ ...
|
136
|
+
│ prio │ integer │ 999
|
137
|
+
|
138
|
+
And `hardwares` has a record with a non-default value for `prio`. Say, the record with `id` 1 has a `prio` of `998`.
|
139
|
+
|
140
|
+
In this situation, upserting like:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
hw = { id: 1, prio: 999 }
|
144
|
+
Hardware.new(prio: hw[:prio]).upsert
|
145
|
+
```
|
146
|
+
|
147
|
+
will not mention the `prio` column in the `ON CONFLICT` clause, resulting in no update.
|
148
|
+
|
149
|
+
However, upserting like so:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
Hardware.upsert(prio: hw[:prio]).id
|
153
|
+
```
|
154
|
+
|
155
|
+
will indeed update the record in the database back to its default value, `999`.
|
156
|
+
|
122
157
|
### Conflict Clauses
|
123
158
|
|
124
159
|
It's possible to specify which columns should be used for the conflict clause. **These must comprise a unique index in Postgres.**
|
@@ -156,6 +191,24 @@ class Account < ApplicationRecord
|
|
156
191
|
end
|
157
192
|
```
|
158
193
|
|
194
|
+
Overriding the models' `upsert_keys` when calling `#upsert` or `.upsert`:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
Account.upsert(attrs, opts: { upsert_keys: [:foo, :bar] })
|
198
|
+
# Or, on an instance:
|
199
|
+
account = Account.new(attrs)
|
200
|
+
account.upsert(opts: { upsert_keys: [:foo, :bar] })
|
201
|
+
```
|
202
|
+
|
203
|
+
Overriding the models' `upsert_options` (partial index) when calling `#upsert` or `.upsert`:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
Account.upsert(attrs, opts: { upsert_options: { where: 'foo IS NOT NULL' } })
|
207
|
+
# Or, on an instance:
|
208
|
+
account = Account.new(attrs)
|
209
|
+
account.upsert(opts: { upsert_options: { where: 'foo IS NOT NULLL } })
|
210
|
+
```
|
211
|
+
|
159
212
|
## Tests
|
160
213
|
|
161
214
|
Make sure to have an upsert_test database:
|
@@ -186,3 +239,5 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/jesjos
|
|
186
239
|
- Daniel Cooper ([@danielcooper](https://github.com/danielcooper))
|
187
240
|
- Laurent Vallar ([@val](https://github.com/val))
|
188
241
|
- Emmanuel Quentin ([@manuquentin](https://github.com/manuquentin))
|
242
|
+
- Jeff Wallace ([@tjwallace](https://github.com/tjwallace))
|
243
|
+
- Kirill Zaitsev ([@Bugagazavr](https://github.com/Bugagazavr))
|
@@ -21,7 +21,6 @@ Gem::Specification.new do |spec|
|
|
21
21
|
|
22
22
|
spec.platform = Gem::Platform::RUBY
|
23
23
|
|
24
|
-
spec.add_runtime_dependency 'activerecord', '>= 5.0', '< 6.
|
25
|
-
spec.add_runtime_dependency 'arel', '> 7.0', '< 10.0'
|
24
|
+
spec.add_runtime_dependency 'activerecord', '>= 5.0', '< 6.2'
|
26
25
|
spec.add_runtime_dependency 'pg', '>= 0.18', '< 2.0'
|
27
26
|
end
|
data/lib/active_record_upsert.rb
CHANGED
@@ -13,6 +13,7 @@ require 'active_record_upsert/active_record'
|
|
13
13
|
|
14
14
|
version = defined?(Rails) ? Rails.version : ActiveRecord.version.to_s
|
15
15
|
|
16
|
+
require 'active_record_upsert/compatibility/rails60.rb' if version >= '6.0.0' && version < '6.2.0'
|
16
17
|
require 'active_record_upsert/compatibility/rails51.rb' if version >= '5.1.0' && version < '5.2.0'
|
17
18
|
require 'active_record_upsert/compatibility/rails50.rb' if version >= '5.0.0' && version < '5.1.0'
|
18
19
|
|
@@ -1,26 +1,20 @@
|
|
1
1
|
module ActiveRecordUpsert
|
2
2
|
module ActiveRecord
|
3
3
|
module PersistenceExtensions
|
4
|
-
def upsert!(attributes: nil, arel_condition: nil, validate: true)
|
4
|
+
def upsert!(attributes: nil, arel_condition: nil, validate: true, opts: {})
|
5
5
|
raise ::ActiveRecord::ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
|
6
6
|
raise ::ActiveRecord::RecordSavedError, "Can't upsert a record that has already been saved" if persisted?
|
7
7
|
validate == false || perform_validations || raise_validation_error
|
8
|
-
|
8
|
+
run_callbacks(:save) {
|
9
9
|
run_callbacks(:create) {
|
10
10
|
attributes ||= changed
|
11
11
|
attributes = attributes +
|
12
12
|
timestamp_attributes_for_create_in_model +
|
13
13
|
timestamp_attributes_for_update_in_model
|
14
|
-
_upsert_record(attributes.map(&:to_s).uniq, arel_condition)
|
14
|
+
_upsert_record(attributes.map(&:to_s).uniq, arel_condition, opts)
|
15
15
|
}
|
16
16
|
}
|
17
17
|
|
18
|
-
# When a migration adds a column to a table, the upsert will start
|
19
|
-
# returning the new attribute, and assign_attributes will fail,
|
20
|
-
# because Rails doesn't know about it yet (until the app is restarted).
|
21
|
-
#
|
22
|
-
# This checks that only known attributes are being assigned.
|
23
|
-
assign_attributes(values.first.to_h.slice(*self.attributes.keys))
|
24
18
|
self
|
25
19
|
end
|
26
20
|
|
@@ -30,20 +24,28 @@ module ActiveRecordUpsert
|
|
30
24
|
false
|
31
25
|
end
|
32
26
|
|
33
|
-
def _upsert_record(upsert_attribute_names = changed, arel_condition = nil)
|
34
|
-
|
35
|
-
|
27
|
+
def _upsert_record(upsert_attribute_names = changed, arel_condition = nil, opts = {})
|
28
|
+
existing_attribute_names = attributes_for_create(attributes.keys)
|
29
|
+
existing_attributes = attributes_with_values(existing_attribute_names)
|
30
|
+
values = self.class._upsert_record(existing_attributes, upsert_attribute_names, [arel_condition].compact, opts)
|
31
|
+
@attributes = self.class.attributes_builder.build_from_database(values.first.to_h)
|
36
32
|
@new_record = false
|
37
33
|
values
|
38
34
|
end
|
39
35
|
|
36
|
+
def upsert_operation
|
37
|
+
created_record = self['_upsert_created_record']
|
38
|
+
return if created_record.nil?
|
39
|
+
created_record ? :create : :update
|
40
|
+
end
|
41
|
+
|
40
42
|
module ClassMethods
|
41
|
-
def upsert!(attributes, arel_condition: nil, validate: true, &block)
|
43
|
+
def upsert!(attributes, arel_condition: nil, validate: true, opts: {}, &block)
|
42
44
|
if attributes.is_a?(Array)
|
43
45
|
attributes.collect { |hash| upsert(hash, &block) }
|
44
46
|
else
|
45
47
|
new(attributes, &block).upsert!(
|
46
|
-
attributes: attributes.keys, arel_condition: arel_condition, validate: validate
|
48
|
+
attributes: attributes.keys, arel_condition: arel_condition, validate: validate, opts: opts
|
47
49
|
)
|
48
50
|
end
|
49
51
|
end
|
@@ -54,9 +56,19 @@ module ActiveRecordUpsert
|
|
54
56
|
false
|
55
57
|
end
|
56
58
|
|
57
|
-
def _upsert_record(existing_attributes, upsert_attributes_names, wheres) # :nodoc:
|
58
|
-
upsert_keys = self.upsert_keys || [primary_key]
|
59
|
+
def _upsert_record(existing_attributes, upsert_attributes_names, wheres, opts) # :nodoc:
|
60
|
+
upsert_keys = opts[:upsert_keys] || self.upsert_keys || [primary_key]
|
61
|
+
upsert_options = opts[:upsert_options] || self.upsert_options
|
59
62
|
upsert_attributes_names = upsert_attributes_names - [*upsert_keys, 'created_at']
|
63
|
+
|
64
|
+
existing_attributes = existing_attributes
|
65
|
+
.transform_keys { |name| _prepare_column(name) }
|
66
|
+
.reject { |key, _| key.nil? }
|
67
|
+
|
68
|
+
upsert_attributes_names = upsert_attributes_names
|
69
|
+
.map { |name| _prepare_column(name) }
|
70
|
+
.compact
|
71
|
+
|
60
72
|
values_for_upsert = existing_attributes.select { |(name, _value)| upsert_attributes_names.include?(name) }
|
61
73
|
|
62
74
|
insert_manager = arel_table.compile_upsert(
|
@@ -70,6 +82,16 @@ module ActiveRecordUpsert
|
|
70
82
|
connection.upsert(insert_manager, "#{self} Upsert")
|
71
83
|
end
|
72
84
|
|
85
|
+
def _prepare_column(column)
|
86
|
+
column = attribute_alias(column) if attribute_alias?(column)
|
87
|
+
|
88
|
+
if columns_hash.key?(column)
|
89
|
+
column
|
90
|
+
elsif reflections.key?(column)
|
91
|
+
reflections[column].foreign_key
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
73
95
|
def upsert_keys(*keys)
|
74
96
|
return @_upsert_keys if keys.empty?
|
75
97
|
options = keys.extract_options!
|
@@ -1,18 +1,29 @@
|
|
1
1
|
module ActiveRecordUpsert
|
2
2
|
module ActiveRecord
|
3
3
|
module PersistenceExtensions
|
4
|
-
def _upsert_record(upsert_attribute_names = changed, arel_condition = nil)
|
5
|
-
|
6
|
-
|
4
|
+
def _upsert_record(upsert_attribute_names = changed, arel_condition = nil, opts = {})
|
5
|
+
upsert_attribute_names = upsert_attribute_names.map { |name| _prepare_column(name) } & self.class.column_names
|
6
|
+
existing_attributes = arel_attributes_with_values_for_create(self.class.column_names)
|
7
|
+
values = self.class.unscoped.upsert(existing_attributes, upsert_attribute_names, [arel_condition].compact, opts)
|
7
8
|
@new_record = false
|
9
|
+
@attributes = self.class.attributes_builder.build_from_database(values.first.to_h)
|
8
10
|
values
|
9
11
|
end
|
12
|
+
|
13
|
+
def _prepare_column(name)
|
14
|
+
if self.class.reflections.key?(name)
|
15
|
+
self.class.reflections[name].foreign_key
|
16
|
+
else
|
17
|
+
name
|
18
|
+
end
|
19
|
+
end
|
10
20
|
end
|
11
21
|
|
12
22
|
module RelationExtensions
|
13
|
-
def upsert(existing_attributes, upsert_attributes, wheres) # :nodoc:
|
23
|
+
def upsert(existing_attributes, upsert_attributes, wheres, opts) # :nodoc:
|
14
24
|
substitutes, binds = substitute_values(existing_attributes)
|
15
|
-
upsert_keys = self.klass.upsert_keys || [primary_key]
|
25
|
+
upsert_keys = opts[:upsert_keys] || self.klass.upsert_keys || [primary_key]
|
26
|
+
upsert_options = opts[:upsert_options] || self.klass.upsert_options
|
16
27
|
|
17
28
|
upsert_attributes = upsert_attributes - [*upsert_keys, 'created_at']
|
18
29
|
upsert_keys_filter = ->(o) { upsert_attributes.include?(o.name) }
|
@@ -20,9 +31,11 @@ module ActiveRecordUpsert
|
|
20
31
|
on_conflict_binds = binds.select(&upsert_keys_filter)
|
21
32
|
vals_for_upsert = substitutes.select { |s| upsert_keys_filter.call(s.first) }
|
22
33
|
|
34
|
+
target = arel_table[upsert_options.key?(:literal) ? ::Arel::Nodes::SqlLiteral.new(upsert_options[:literal]) : upsert_keys.join(',')]
|
35
|
+
|
23
36
|
on_conflict_do_update = ::Arel::OnConflictDoUpdateManager.new
|
24
|
-
on_conflict_do_update.target =
|
25
|
-
on_conflict_do_update.target_condition =
|
37
|
+
on_conflict_do_update.target = target
|
38
|
+
on_conflict_do_update.target_condition = upsert_options[:where]
|
26
39
|
on_conflict_do_update.wheres = wheres
|
27
40
|
on_conflict_do_update.set(vals_for_upsert)
|
28
41
|
|
@@ -31,7 +44,7 @@ module ActiveRecordUpsert
|
|
31
44
|
insert_manager.on_conflict = on_conflict_do_update.to_node
|
32
45
|
insert_manager.insert substitutes
|
33
46
|
|
34
|
-
@klass.connection.upsert(insert_manager, "#{
|
47
|
+
@klass.connection.upsert(insert_manager, "#{@klass.name} Upsert", binds + on_conflict_binds)
|
35
48
|
end
|
36
49
|
|
37
50
|
::ActiveRecord::Relation.include(self)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module ActiveRecordUpsert
|
2
|
+
module ActiveRecord
|
3
|
+
module TransactionsExtensions
|
4
|
+
def upsert(*args)
|
5
|
+
with_transaction_returning_status { super }
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module ConnectAdapterExtension
|
10
|
+
def upsert(*args)
|
11
|
+
::ActiveRecord::Base.clear_query_caches_for_current_thread
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record_upsert
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jesper Josefsson
|
8
8
|
- Olle Jonsson
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2020-12-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -20,7 +20,7 @@ dependencies:
|
|
20
20
|
version: '5.0'
|
21
21
|
- - "<"
|
22
22
|
- !ruby/object:Gem::Version
|
23
|
-
version: '6.
|
23
|
+
version: '6.2'
|
24
24
|
type: :runtime
|
25
25
|
prerelease: false
|
26
26
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -30,27 +30,7 @@ dependencies:
|
|
30
30
|
version: '5.0'
|
31
31
|
- - "<"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '6.
|
34
|
-
- !ruby/object:Gem::Dependency
|
35
|
-
name: arel
|
36
|
-
requirement: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '7.0'
|
41
|
-
- - "<"
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
version: '10.0'
|
44
|
-
type: :runtime
|
45
|
-
prerelease: false
|
46
|
-
version_requirements: !ruby/object:Gem::Requirement
|
47
|
-
requirements:
|
48
|
-
- - ">"
|
49
|
-
- !ruby/object:Gem::Version
|
50
|
-
version: '7.0'
|
51
|
-
- - "<"
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: '10.0'
|
33
|
+
version: '6.2'
|
54
34
|
- !ruby/object:Gem::Dependency
|
55
35
|
name: pg
|
56
36
|
requirement: !ruby/object:Gem::Requirement
|
@@ -71,7 +51,7 @@ dependencies:
|
|
71
51
|
- - "<"
|
72
52
|
- !ruby/object:Gem::Version
|
73
53
|
version: '2.0'
|
74
|
-
description:
|
54
|
+
description:
|
75
55
|
email:
|
76
56
|
- jesper.josefsson@gmail.com
|
77
57
|
- olle.jonsson@gmail.com
|
@@ -83,6 +63,8 @@ files:
|
|
83
63
|
- Gemfile.rails-5-0
|
84
64
|
- Gemfile.rails-5-1
|
85
65
|
- Gemfile.rails-5-2
|
66
|
+
- Gemfile.rails-6-0
|
67
|
+
- Gemfile.rails-6-1
|
86
68
|
- Gemfile.rails-master
|
87
69
|
- LICENSE
|
88
70
|
- README.md
|
@@ -109,12 +91,13 @@ files:
|
|
109
91
|
- lib/active_record_upsert/arel/visitors/to_sql.rb
|
110
92
|
- lib/active_record_upsert/compatibility/rails50.rb
|
111
93
|
- lib/active_record_upsert/compatibility/rails51.rb
|
94
|
+
- lib/active_record_upsert/compatibility/rails60.rb
|
112
95
|
- lib/active_record_upsert/version.rb
|
113
96
|
homepage: https://github.com/jesjos/active_record_upsert/
|
114
97
|
licenses:
|
115
98
|
- MIT
|
116
99
|
metadata: {}
|
117
|
-
post_install_message:
|
100
|
+
post_install_message:
|
118
101
|
rdoc_options: []
|
119
102
|
require_paths:
|
120
103
|
- lib
|
@@ -129,9 +112,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
129
112
|
- !ruby/object:Gem::Version
|
130
113
|
version: '0'
|
131
114
|
requirements: []
|
132
|
-
|
133
|
-
|
134
|
-
signing_key:
|
115
|
+
rubygems_version: 3.2.1
|
116
|
+
signing_key:
|
135
117
|
specification_version: 4
|
136
118
|
summary: Real PostgreSQL 9.5+ upserts using ON CONFLICT for ActiveRecord
|
137
119
|
test_files: []
|