active_record_upsert 0.7.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 861dbd4abac80eae38c3644d5a982f3b4c7d95a8
4
- data.tar.gz: fe700280c47b2cff78e3f8eeed2a14d966a10b72
2
+ SHA256:
3
+ metadata.gz: b6c3d046d77a439d261ab965fc32e843c070959b40300db46e299fe025bdbe2e
4
+ data.tar.gz: 21849093ff6d5ca811aa039d83da437cb9d2cb995e2e8e2c74248352f7b069ff
5
5
  SHA512:
6
- metadata.gz: 3254084380ac0b9a14189ed071f53ec45e74d2701962b892720839d93b2855156e684f26e025742d19e8575031b3062e662c034bc19705caacabfafddde1a6ad
7
- data.tar.gz: 859f1a55e2f6f2b64f192f18f305ae634fc50b4d24475a01bd5d0049cf4ad6d70cb931bd7215d4cfae492941046c04b4f15ca7a907fc2bbe844635bec5a0e340
6
+ metadata.gz: fe9a96f9703a0a3086bc83052d97093cf1aa377a3425e9e03d054e9248aa62fb0eabc9098f8483938ba1b343464470ebb015f8016a5df8bdc6564c193fb9400a
7
+ data.tar.gz: 17ef7f689953232f9ce646f8d3ff24708482d4009ea47bf9ffd26b5170928aadbf6b8d37c557a7e45995c73071eda076447b320db9a7d3bfcdc5bfd04c9acd5c
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Jesper Josefsson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -38,8 +38,10 @@ Or install it yourself as:
38
38
  $ gem install active_record_upsert
39
39
 
40
40
  ## Usage
41
- Just use `ActiveRecord.upsert` or `ActiveRecord#upsert`.
42
- *ActiveRecordUpsert* respects timestamps.
41
+
42
+ ### Create
43
+
44
+ Use `ActiveRecord.upsert` or `ActiveRecord#upsert`. *ActiveRecordUpsert* respects timestamps.
43
45
 
44
46
  ```ruby
45
47
  class MyRecord < ActiveRecord::Base
@@ -57,7 +59,69 @@ r.upsert
57
59
  # => #<MyRecord id: 1, name: "bar", created_at: "2016-02-20 14:15:55", updated_at: "2016-02-20 14:18:49", wisdom: 3>
58
60
  ```
59
61
 
60
- Also, it's possible to specify which columns should be used for the conflict clause. **These must comprise a unique index in Postgres.**
62
+ ### Update
63
+
64
+ If you need to specify a condition for the update, pass it as an Arel query:
65
+
66
+ ```ruby
67
+ MyRecord.upsert({id: 1, wisdom: 3}, arel_condition: MyRecord.arel_table[:updated_at].lt(1.day.ago))
68
+ ```
69
+
70
+ The instance method `#upsert` can also take keyword arguments to specify a condition, or to limit which attributes to upsert
71
+ (by default, all `changed` attributes will be passed to the upsert):
72
+
73
+ ```ruby
74
+ r = MyRecord.new(id: 1)
75
+ r.name = 'bar'
76
+ r.color = 'blue'
77
+ r.upsert(attributes: [:name], arel_condition: MyRecord.arel_table[:updated_at].lt(1.day.ago))
78
+ # will only update :name, and only if the record is older than 1 day;
79
+ # but if the record does not exist, will insert with both :name and :colors
80
+ ```
81
+
82
+ ### Create with specific Attributes
83
+
84
+ If you want to create a record with the specific attributes, but update only a limited set of attributes,
85
+ similar to how `ActiveRecord::Base.create_with` works, you can do the following:
86
+
87
+ ```ruby
88
+ existing_record = MyRecord.create(id: 1, name: 'lemon', color: 'green')
89
+ r = MyRecord.new(id: 1, name: 'banana', color: 'yellow')
90
+ r.upsert(attributes: [:color])
91
+ # => #<MyRecord id: 1, name: "lemon", color: "yellow", ...>
92
+
93
+ r = MyRecord.new(id: 2, name: 'banana', color: 'yellow')
94
+ r.upsert(attributes: [:color])
95
+
96
+ # => #<MyRecord id: 2, name: "banana", color: "yellow", ...>
97
+
98
+ # This is similar to:
99
+
100
+ MyRecord.create_with(name: 'banana').find_or_initialize_by(id: 2).update(color: 'yellow')
101
+
102
+ ```
103
+
104
+ ### Validations
105
+
106
+ Upsert will perform validation on the object, and return false if it is not valid. To skip validation, pass `validate: false`:
107
+ ```ruby
108
+ MyRecord.upsert({id: 1, wisdom: 3}, validate: false)
109
+ ```
110
+
111
+ If you want validations to raise `ActiveRecord::RecordInvalid`, use `upsert!`:
112
+ ```ruby
113
+ MyRecord.upsert!(id: 1, wisdom: 3)
114
+ ```
115
+
116
+ Or using the instance method:
117
+ ```ruby
118
+ r = MyRecord.new(id: 1, name: 'bar')
119
+ r.upsert!
120
+ ```
121
+
122
+ ### Conflict Clauses
123
+
124
+ It's possible to specify which columns should be used for the conflict clause. **These must comprise a unique index in Postgres.**
61
125
 
62
126
  ```ruby
63
127
  class Vehicle < ActiveRecord::Base
@@ -97,3 +161,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/jesjos
97
161
  - Olle Jonsson
98
162
  - Simon Dahlbacka
99
163
  - Paul Hoffer
164
+ - Ivan ([@me](https://github.com/me))
165
+ - Leon Miller-Out ([@sbleon](https://github.com/sbleon))
166
+ - Andrii Dmytrenko ([@Antti](https://github.com/Antti))
167
+ - Alexia McDonald ([@alexiamcdonald](https://github.com/alexiamcdonald))
@@ -9,6 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Jesper Josefsson", "Olle Jonsson"]
10
10
  spec.email = ["jesper.josefsson@gmail.com", "olle.jonsson@gmail.com"]
11
11
  spec.homepage = "https://github.com/jesjos/active_record_upsert/"
12
+ spec.license = 'MIT'
12
13
 
13
14
  spec.summary = %q{Real PostgreSQL 9.5+ upserts using ON CONFLICT for ActiveRecord}
14
15
 
@@ -2,39 +2,60 @@ module ActiveRecordUpsert
2
2
  module ActiveRecord
3
3
  module PersistenceExtensions
4
4
 
5
- def upsert(attribute_names=nil)
5
+ def upsert!(attributes: nil, arel_condition: nil, validate: true)
6
6
  raise ::ActiveRecord::ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
7
7
  raise ::ActiveRecord::RecordSavedError, "Can't upsert a record that has already been saved" if persisted?
8
+ validate == false || perform_validations || raise_validation_error
8
9
  values = run_callbacks(:save) {
9
10
  run_callbacks(:create) {
10
- attribute_names ||= changed
11
- attribute_names = attribute_names.map(&:to_s) +
11
+ attributes ||= changed
12
+ attributes = attributes +
12
13
  timestamp_attributes_for_create_in_model +
13
14
  timestamp_attributes_for_update_in_model
14
- _upsert_record(attribute_names.uniq)
15
+ _upsert_record(attributes.map(&:to_s).uniq, arel_condition)
15
16
  }
16
17
  }
17
- assign_attributes(values.first.to_h)
18
+
19
+ # When a migration adds a column to a table, the upsert will start
20
+ # returning the new attribute, and assign_attributes will fail,
21
+ # because Rails doesn't know about it yet (until the app is restarted).
22
+ #
23
+ # This checks that only known attributes are being assigned.
24
+ assign_attributes(values.first.to_h.slice(*self.attributes.keys))
18
25
  self
26
+ end
27
+
28
+ def upsert(*args)
29
+ upsert!(*args)
19
30
  rescue ::ActiveRecord::RecordInvalid
20
31
  false
21
32
  end
22
33
 
23
- def _upsert_record(attribute_names = changed)
24
- attributes_values = arel_attributes_with_values_for_create(attribute_names)
25
- values = self.class.unscoped.upsert attributes_values
34
+
35
+ def _upsert_record(upsert_attribute_names = changed, arel_condition = nil)
36
+ existing_attributes = arel_attributes_with_values_for_create(self.attributes.keys)
37
+ values = self.class.unscoped.upsert(existing_attributes, upsert_attribute_names, [arel_condition].compact)
26
38
  @new_record = false
27
39
  values
28
40
  end
29
41
 
30
42
  module ClassMethods
31
- def upsert(attributes, &block)
43
+ def upsert!(attributes, arel_condition: nil, &block)
32
44
  if attributes.is_a?(Array)
33
45
  attributes.collect { |hash| upsert(hash, &block) }
34
46
  else
35
- new(attributes, &block).upsert(attributes.keys)
47
+ new(attributes, &block).upsert!(
48
+ attributes: attributes.keys, arel_condition: arel_condition, validate: true
49
+ )
36
50
  end
37
51
  end
52
+
53
+ def upsert(*args)
54
+ upsert!(*args)
55
+ rescue ::ActiveRecord::RecordInvalid
56
+ false
57
+ end
58
+
38
59
  def upsert_keys(*keys)
39
60
  return @_upsert_keys if keys.empty?
40
61
  keys = keys.first if keys.size == 1 # support single string/symbol, multiple string/symbols, and array
@@ -1,38 +1,31 @@
1
1
  module ActiveRecordUpsert
2
2
  module ActiveRecord
3
3
  module RelationExtensions
4
- def upsert(values) # :nodoc:
5
- primary_key_value = nil
4
+ def upsert(existing_attributes, upsert_attributes, wheres) # :nodoc:
5
+ substitutes, binds = substitute_values(existing_attributes)
6
+ upsert_keys = self.klass.upsert_keys || [primary_key]
6
7
 
7
- if primary_key && Hash === values
8
- primary_key_value = values[values.keys.find { |k|
9
- k.name == primary_key
10
- }]
11
- end
8
+ upsert_attributes = upsert_attributes - [*upsert_keys, 'created_at']
9
+ upsert_keys_filter = ->(o) { upsert_attributes.include?(o.name) }
12
10
 
13
- im = arel_table.create_insert
14
- im.into arel_table
11
+ on_conflict_binds = binds.select(&upsert_keys_filter)
12
+ vals_for_upsert = substitutes.select { |s| upsert_keys_filter.call(s.first) }
15
13
 
16
- substitutes, binds = substitute_values values
17
- column_arr = self.klass.upsert_keys || [primary_key]
18
- column_name = column_arr.join(',')
14
+ on_conflict_do_update = arel_table.create_on_conflict_do_update
15
+ on_conflict_do_update.target = arel_table[upsert_keys.join(',')]
16
+ on_conflict_do_update.wheres = wheres
17
+ on_conflict_do_update.set(vals_for_upsert)
19
18
 
20
- cm = arel_table.create_on_conflict_do_update
21
- cm.target = arel_table[column_name]
22
- filter = ->(o) { [*column_arr, 'created_at'].include?(o.name) }
23
-
24
- cm.set(substitutes.reject { |s| filter.call(s.first) })
25
- on_conflict_binds = binds.reject(&filter)
26
-
27
- im.on_conflict = cm.to_node
28
-
29
- im.insert substitutes
19
+ insert_manager = arel_table.create_insert
20
+ insert_manager.into arel_table
21
+ insert_manager.on_conflict = on_conflict_do_update.to_node
22
+ insert_manager.insert substitutes
30
23
 
31
24
  @klass.connection.upsert(
32
- im,
25
+ insert_manager,
33
26
  'SQL',
34
- primary_key, # not used
35
- primary_key_value, # not used
27
+ nil, # primary key (not used)
28
+ nil, # primary key value (not used)
36
29
  nil,
37
30
  binds + on_conflict_binds)
38
31
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordUpsert
2
- VERSION = "0.7.0"
2
+ VERSION = "0.7.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_upsert
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jesper Josefsson
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2017-08-22 00:00:00.000000000 Z
12
+ date: 2018-01-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -73,6 +73,7 @@ executables: []
73
73
  extensions: []
74
74
  extra_rdoc_files: []
75
75
  files:
76
+ - LICENSE
76
77
  - README.md
77
78
  - Rakefile
78
79
  - active_record_upsert.gemspec
@@ -98,7 +99,8 @@ files:
98
99
  - lib/active_record_upsert/arel/visitors/to_sql.rb
99
100
  - lib/active_record_upsert/version.rb
100
101
  homepage: https://github.com/jesjos/active_record_upsert/
101
- licenses: []
102
+ licenses:
103
+ - MIT
102
104
  metadata: {}
103
105
  post_install_message:
104
106
  rdoc_options: []
@@ -116,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
118
  version: '0'
117
119
  requirements: []
118
120
  rubyforge_project:
119
- rubygems_version: 2.6.12
121
+ rubygems_version: 2.7.4
120
122
  signing_key:
121
123
  specification_version: 4
122
124
  summary: Real PostgreSQL 9.5+ upserts using ON CONFLICT for ActiveRecord