Skip to content

Commit c287295

Browse files
committed
Add Returning Attributes for UPDATEs
… and add specs.
1 parent 3a6e0a6 commit c287295

File tree

10 files changed

+245
-81
lines changed

10 files changed

+245
-81
lines changed

Gemfile

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,3 @@ source 'https://rubygems.org'
22

33
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
44
gemspec
5-
6-
gem 'pry'

Gemfile.lock

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ PATH
22
remote: .
33
specs:
44
active_record-returning_attributes (0.1.0)
5-
activerecord
6-
activesupport
5+
activerecord (= 5.2.0)
6+
pg
77

88
GEM
99
remote: https://rubygems.org/
@@ -27,9 +27,13 @@ GEM
2727
concurrent-ruby (~> 1.0)
2828
method_source (0.9.0)
2929
minitest (5.11.3)
30+
pg (1.0.0)
3031
pry (0.11.3)
3132
coderay (~> 1.1.0)
3233
method_source (~> 0.9.0)
34+
pry-doc (0.13.4)
35+
pry (~> 0.11)
36+
yard (~> 0.9.11)
3337
rake (10.5.0)
3438
rspec (3.7.0)
3539
rspec-core (~> 3.7.0)
@@ -47,6 +51,7 @@ GEM
4751
thread_safe (0.3.6)
4852
tzinfo (1.2.5)
4953
thread_safe (~> 0.1)
54+
yard (0.9.12)
5055

5156
PLATFORMS
5257
ruby
@@ -55,6 +60,7 @@ DEPENDENCIES
5560
active_record-returning_attributes!
5661
bundler (~> 1.16)
5762
pry
63+
pry-doc
5864
rake (~> 10.0)
5965
rspec (~> 3.0)
6066

activerecord-returning_attributes.gemspec

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ Gem::Specification.new do |spec|
1818
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
1919
spec.require_paths = ['lib']
2020

21-
spec.add_runtime_dependency 'activesupport'
22-
spec.add_runtime_dependency 'activerecord'
21+
spec.add_runtime_dependency 'activerecord', '5.2.0'
22+
spec.add_runtime_dependency 'pg'
2323

2424
spec.add_development_dependency 'bundler', '~> 1.16'
2525
spec.add_development_dependency 'rake', '~> 10.0'
2626
spec.add_development_dependency 'rspec', '~> 3.0'
2727
spec.add_development_dependency 'pry'
28+
spec.add_development_dependency 'pry-doc'
2829
end
Lines changed: 24 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,43 @@
1-
require_relative 'returning_attributes/accessor'
1+
require_relative 'returning_attributes/patching'
22
require_relative 'returning_attributes/version'
3-
require_relative 'returning_attributes/with_returning'
43

54
require 'active_support/lazy_load_hooks'
65

76
module ActiveRecord
87
module ReturningAttributes
9-
def self.monkey_patch_persistence!
10-
Persistence.module_eval do
11-
def _create_record(attribute_names = self.attribute_names)
12-
attribute_names &= self.class.column_names
13-
attributes_values = attributes_with_values_for_create(attribute_names)
14-
15-
new_id, result = self.class.connection.with_returning(returning) do
16-
self.class._insert_record(attributes_values)
17-
end
18-
19-
self.id ||= new_id if self.class.primary_key
20-
21-
self.class.returning.each do |column|
22-
write_attribute(column, result.to_hash.first[column.to_s])
23-
end
24-
25-
@new_record = false
26-
27-
yield(self) if block_given?
28-
29-
id
30-
end
8+
def self.postgresql_only
9+
if postgresql?
10+
yield
11+
else
12+
warn 'ActiveRecord::ReturningAttributes only works with PostgreSQL, skipping patching ActiveRecord.'
3113
end
3214
end
3315

34-
def self.monkey_patch_database_statements!
35-
ConnectionAdapters::DatabaseStatements.module_eval do
36-
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
37-
sql, binds = to_sql_and_binds(arel, binds)
38-
@_insert_result = exec_insert(sql, name, binds, pk, sequence_name)
39-
id_value || last_inserted_id(@_insert_result)
40-
end
16+
def self.postgresql?
17+
begin
18+
require 'pg'
19+
true
20+
rescue LoadError
21+
false
4122
end
4223
end
4324

44-
def self.monkey_patch_postgresql_database_statements!
45-
require 'active_record/connection_adapters/postgresql/database_statements'
46-
47-
ConnectionAdapters::PostgreSQL::DatabaseStatements.module_eval do
48-
private
49-
50-
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
51-
if pk.nil?
52-
# Extract the table from the insert sql. Yuck.
53-
table_ref = extract_table_ref_from_insert_sql(sql)
54-
pk = primary_key(table_ref) if table_ref
55-
end
56-
57-
if pk = suppress_composite_primary_key(pk)
58-
if @_returning
59-
sql = "#{sql} RETURNING #{[pk, @_returning].flatten.map { |column| quote_column_name(column) }.join(', ')}"
60-
else
61-
raise 'This case shouldn’t happen.'
62-
end
63-
end
64-
65-
super
66-
end
25+
def self.active_record_5_2_tested_only
26+
unless ActiveRecord.version == Gem::Version.new('5.2.0')
27+
warn 'ActiveRecord::ReturningAttributes was only tested on ActiveRecord 5.2.0 and might not work with your version.'
6728
end
6829
end
6930

70-
def self.extend_postgresql_adapter!
71-
require 'active_record/connection_adapters/postgresql_adapter'
72-
ConnectionAdapters::PostgreSQLAdapter.include(WithReturning)
73-
end
74-
7531
ActiveSupport.on_load(:active_record) do
76-
include Accessor
77-
78-
ReturningAttributes.monkey_patch_persistence!
79-
ReturningAttributes.monkey_patch_database_statements!
80-
ReturningAttributes.monkey_patch_postgresql_database_statements!
81-
ReturningAttributes.extend_postgresql_adapter!
32+
ReturningAttributes.postgresql_only do
33+
ReturningAttributes::Patching.patch_base
34+
ReturningAttributes::Patching.patch_persistence
35+
ReturningAttributes::Patching.patch_database_statements
36+
ReturningAttributes::Patching.patch_postgresql_database_statements
37+
ReturningAttributes::Patching.patch_postgresql_adapter
38+
39+
ReturningAttributes.active_record_5_2_tested_only
40+
end
8241
end
8342
end
8443
end

lib/active_record/returning_attributes/accessor.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module Accessor
66
extend ActiveSupport::Concern
77

88
included do
9-
class_attribute :returning, default: []
9+
class_attribute :returning_attributes, default: []
1010
end
1111
end
1212
end
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
require_relative '../returning_attributes/accessor'
2+
require_relative '../returning_attributes/with_returning'
3+
4+
module ActiveRecord
5+
module ReturningAttributes
6+
module Patching
7+
def self.patch_base
8+
Base.include(Accessor)
9+
end
10+
11+
def self.patch_persistence
12+
Persistence.module_eval do
13+
private
14+
15+
def _create_record(attribute_names = self.attribute_names)
16+
attribute_names &= self.class.column_names
17+
attributes_values = attributes_with_values_for_create(attribute_names)
18+
19+
new_id, returned_attributes = self.class.connection.with_returning_attributes(returning_attributes) do
20+
self.class._insert_record(attributes_values)
21+
end
22+
23+
self.id ||= new_id if self.class.primary_key
24+
25+
returning_attributes.each do |attribute|
26+
write_attribute(attribute, returned_attributes[attribute.to_s]) if returned_attributes.key?(attribute.to_s)
27+
end
28+
29+
@new_record = false
30+
31+
yield(self) if block_given?
32+
33+
id
34+
end
35+
36+
def _update_record(attribute_names = self.attribute_names)
37+
attribute_names &= self.class.column_names
38+
attribute_names = attributes_for_update(attribute_names)
39+
40+
if attribute_names.empty?
41+
affected_rows = 0
42+
@_trigger_update_callback = true
43+
else
44+
affected_rows, returned_attributes = self.class.connection.with_returning_attributes(returning_attributes) do
45+
_update_row(attribute_names)
46+
end
47+
48+
returning_attributes.each do |attribute|
49+
write_attribute(attribute, returned_attributes[attribute.to_s]) if returned_attributes.key?(attribute.to_s)
50+
end
51+
52+
@_trigger_update_callback = affected_rows == 1
53+
end
54+
55+
yield(self) if block_given?
56+
57+
affected_rows
58+
end
59+
end
60+
end
61+
62+
def self.patch_database_statements
63+
ConnectionAdapters::DatabaseStatements.module_eval do
64+
def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil)
65+
sql, binds = sql_for_insert(sql, pk, nil, sequence_name, binds)
66+
67+
exec_query(sql, name, binds).tap do |result|
68+
@_returned_attributes = result.first.to_hash
69+
end
70+
end
71+
72+
def update(arel, name = nil, binds = [])
73+
sql, binds = to_sql_and_binds(arel, binds)
74+
75+
if @_returning_attributes && !@_returning_attributes.empty?
76+
sql = "#{sql} RETURNING #{@_returning_attributes.map { |column| quote_column_name(column) }.join(', ')}"
77+
end
78+
79+
exec_update(sql, name, binds)
80+
end
81+
end
82+
end
83+
84+
def self.patch_postgresql_database_statements
85+
require 'active_record/connection_adapters/postgresql/database_statements'
86+
87+
ConnectionAdapters::PostgreSQL::DatabaseStatements.module_eval do
88+
private
89+
90+
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
91+
if pk.nil?
92+
# Extract the table from the insert sql. Yuck.
93+
table_ref = extract_table_ref_from_insert_sql(sql)
94+
pk = primary_key(table_ref) if table_ref
95+
end
96+
97+
if pk = suppress_composite_primary_key(pk)
98+
sql = "#{sql} RETURNING #{[pk, @_returning_attributes].flatten.map { |column| quote_column_name(column) }.join(', ')}"
99+
end
100+
101+
super
102+
end
103+
end
104+
end
105+
106+
def self.patch_postgresql_adapter
107+
require 'active_record/connection_adapters/postgresql_adapter'
108+
109+
ConnectionAdapters::PostgreSQLAdapter.class_eval do
110+
include WithReturning
111+
112+
private
113+
114+
def execute_and_clear(sql, name, binds, prepare: false)
115+
if without_prepared_statement?(binds)
116+
result = exec_no_cache(sql, name, [])
117+
elsif !prepare
118+
result = exec_no_cache(sql, name, binds)
119+
else
120+
result = exec_cache(sql, name, binds)
121+
end
122+
123+
if @_returning_attributes && !@_returning_attributes.empty?
124+
@_returned_attributes = result.to_a.first
125+
end
126+
127+
ret = yield result
128+
result.clear
129+
ret
130+
end
131+
end
132+
end
133+
end
134+
end
135+
end

lib/active_record/returning_attributes/with_returning.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
module ActiveRecord
22
module ReturningAttributes
33
module WithReturning
4-
def with_returning(returning)
4+
def with_returning_attributes(attributes)
55
begin
6-
@_returning = returning
7-
[yield, @_insert_result]
6+
@_returning_attributes = attributes
7+
[yield, @_returned_attributes || {}]
88
ensure
9-
@_returning = nil
10-
@_insert_result = nil
9+
@_returning_attributes = nil
10+
@_returned_attributes = nil
1111
end
1212
end
1313
end
Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,41 @@
11
RSpec.describe ActiveRecord::ReturningAttributes do
2-
it 'has a version number' do
3-
expect(ActiveRecord::ReturningAttributes::VERSION).not_to be nil
2+
let(:project) { Project.create(name: 'Freshly Created') }
3+
4+
context 'without returning attributes' do
5+
before do
6+
Project.returning_attributes = []
7+
end
8+
9+
context 'on insert' do
10+
it 'doesn’t return database-backed default values' do
11+
expect(project.uuid).to be_nil
12+
end
13+
end
14+
15+
context 'on update' do
16+
it 'doesn’t return values set via database triggers' do
17+
project.update(name: 'Just Updated')
18+
expect(project.update_count).to eq(0)
19+
end
20+
end
421
end
522

6-
it 'does something useful' do
7-
expect(false).to eq(true)
23+
context 'with returning attributes' do
24+
before do
25+
Project.returning_attributes = [:uuid, :update_count]
26+
end
27+
28+
context 'on insert' do
29+
it 'returns database-backed default values' do
30+
expect(project.uuid).to be_present
31+
end
32+
end
33+
34+
context 'on update' do
35+
it 'returns values set via database triggers' do
36+
project.update(name: 'Just Updated')
37+
expect(project.update_count).to eq(1)
38+
end
39+
end
840
end
941
end

spec/spec_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
require 'bundler/setup'
2+
require 'active_record'
23
require 'active_record/returning_attributes'
4+
require 'pry'
5+
6+
require_relative 'support/schema_and_models'
37

48
RSpec.configure do |config|
59
# Enable flags like --only-failures and --next-failure
@@ -11,4 +15,6 @@
1115
config.expect_with :rspec do |c|
1216
c.syntax = :expect
1317
end
18+
19+
config.filter_run_when_matching :focus
1420
end

0 commit comments

Comments
 (0)