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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b01678cb29f0661418c62699e1278e500a9704d174f114a63118823798361721
4
- data.tar.gz: d850a9160cc1b7fe9c6fa2c57843cd983734747689cb2b956d2a357714c1944d
3
+ metadata.gz: d08fb14b2c2b66a287518710472aba53e095a4cc54c581a5c1dcf5ecd86088f6
4
+ data.tar.gz: 02b6d45767b9ecfeb8bcd1b27dcef517470d7f7747f522d3c14db445725ab3fc
5
5
  SHA512:
6
- metadata.gz: 66ea79bd414e4d03f0a2ef54d309fb65092a2e40170ee399d28df47fd5fbbe3a0c657a4c16d7615d03dd5d235444c72f8478542249ea66279a36f57076d2b8d4
7
- data.tar.gz: c35cc444d6abac21f43e49d0c588b65347b45d6692d0f2528846f02d05a28cd2241486f9496312ccaa2621966afc5a2b4babb84c305463f02d223644a9336fcb
6
+ metadata.gz: fefab82228fbfc68fbdd96912617ec7bdea1547be32b02800d46d2507bc0ba62c78001c21bf1cccb8b689ef1d5426432b8d49d40379dac8a044a326bcc158c9e
7
+ data.tar.gz: 249d793b70c0855b907a1036ca887a7c0c4b1ab68ee28a276d22b14bbdabd1ee6cf3973b11f6f818d1507fb0df7bf27475aca0c4d1e2e1f0e613be53ab85ecf0
@@ -5,7 +5,7 @@ gemspec
5
5
  group :development, :test do
6
6
  gem 'bundler', '>= 1.13'
7
7
  gem 'database_cleaner', '~> 1.6'
8
- gem 'pg', '~> 0.18'
8
+ gem 'pg', '~> 1.1'
9
9
  gem 'pry', '> 0'
10
10
  gem 'rake', '>= 10.0'
11
11
  gem 'rspec', '>= 3.0', '< 4'
@@ -1,5 +1,5 @@
1
1
  group :development, :test do
2
- gem 'rails', '>= 5.2.0.rc1', '< 6.0'
2
+ gem 'rails', '>= 5.2.1', '< 6.0'
3
3
  end
4
4
 
5
5
  eval_gemfile "#{__dir__}/Gemfile.base"
@@ -0,0 +1,5 @@
1
+ group :development, :test do
2
+ gem 'rails', '>= 6.0.0.rc1', '< 6.1'
3
+ end
4
+
5
+ eval_gemfile "#{__dir__}/Gemfile.base"
@@ -0,0 +1,5 @@
1
+ group :development, :test do
2
+ gem 'rails', '>= 6.1.0-rc1', '< 6.2'
3
+ end
4
+
5
+ eval_gemfile "#{__dir__}/Gemfile.base"
data/README.md CHANGED
@@ -1,11 +1,10 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/active_record_upsert.svg)](https://badge.fury.io/rb/active_record_upsert)
2
2
  [![Build Status](https://travis-ci.org/jesjos/active_record_upsert.svg?branch=master)](https://travis-ci.org/jesjos/active_record_upsert)
3
3
  [![Code Climate](https://codeclimate.com/github/jesjos/active_record_upsert/badges/gpa.svg)](https://codeclimate.com/github/jesjos/active_record_upsert)
4
- [![Dependency Status](https://gemnasium.com/badges/github.com/jesjos/active_record_upsert.svg)](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. Uses [ON CONFLICT DO UPDATE](http://www.postgresql.org/docs/9.5/static/sql-insert.html).
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 ~> 5
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.0'
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
@@ -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
 
@@ -9,7 +9,7 @@ module ActiveRecordUpsert
9
9
  end
10
10
 
11
11
  def exec_upsert(sql, name, binds)
12
- exec_query("#{sql} RETURNING *", name, binds)
12
+ exec_query("#{sql} RETURNING *, (xmax = 0) AS _upsert_created_record", name, binds)
13
13
  end
14
14
  end
15
15
  end
@@ -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
- values = run_callbacks(:save) {
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
- existing_attributes = attributes_with_values_for_create(self.attributes.keys)
35
- values = self.class._upsert_record(existing_attributes, upsert_attribute_names, [arel_condition].compact)
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
- existing_attributes = arel_attributes_with_values_for_create(self.attributes.keys)
6
- values = self.class.unscoped.upsert(existing_attributes, upsert_attribute_names, [arel_condition].compact)
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 = arel_table[upsert_keys.join(',')]
25
- on_conflict_do_update.target_condition = self.klass.upsert_options[:where]
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, "#{self} Upsert", binds + on_conflict_binds)
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
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordUpsert
2
- VERSION = "0.9.1"
2
+ VERSION = "0.10.0"
3
3
  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.9.1
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: 2018-05-24 00:00:00.000000000 Z
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.0'
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.0'
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
- rubyforge_project:
133
- rubygems_version: 2.7.7
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: []