relation_to_struct 1.5.1 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 7c0ecf02a9aab05f81adc8704ad9fcf8b71ec339859ba9ef491e0a2ea9eb75e9
4
- data.tar.gz: 2cd15cd6e481026b2b9294d621db0ea7d257168a5288e4316cc9735d09b3bcfd
2
+ SHA1:
3
+ metadata.gz: d1ffdbfc2950c798cd223660370ca62ee6a8128b
4
+ data.tar.gz: 246947abe7a8f85d6c2558a3b33a0b027547ada4
5
5
  SHA512:
6
- metadata.gz: b38fe58cf3b14d889789526b85dab4afa03e29c7d3825d39c92ae5dcac23eeea2dab7cdacf95a320438996d8cd8e4b434ea4b0d40198f5eab3539d157b76a54c
7
- data.tar.gz: f3c565ca306f0120c9081626c4a9ee07569f68a75382b2409d0714fb603ae9010f4903789a50568482469a97bf96fb64e16044252bd6fb249261fab2baf4f4bc
6
+ metadata.gz: 25ef491ea1f470beea129c8e1d5f92308773092ef470afaadedd56ec9046ad1950d4192e1e61efa816966ecf0dc5eafe40c23bc33ed0572424d0c82368d6cbdf
7
+ data.tar.gz: 8a8f2eebe3192ae4d3947a309adfd476e903af1bc754496d1f53f03d8040afb8ad12f12f640a4f69dcc54179b015b02399a19b2d712420b0dabed21fff561c2a
data/README.md CHANGED
@@ -36,6 +36,8 @@ relation.to_structs(UserPostsSummary) # => array of structs
36
36
 
37
37
  ### From raw SQL
38
38
 
39
+ Note: In order to provide a consistent user experience regardless of the abstraction level used by your code, all of the following methods are available on both `ActiveRecord::Base` and `ActiveRecord::Base.connection`.
40
+
39
41
  ```
40
42
  UserPostsSummary = Struct.new(:user_name, :post_count)
41
43
  sql = <<-eos
@@ -1,4 +1,5 @@
1
1
  require "relation_to_struct/version"
2
+ require "relation_to_struct/active_record_connection_adapter_extension"
2
3
  require "relation_to_struct/active_record_base_extension"
3
4
  require "relation_to_struct/active_record_relation_extension"
4
5
 
@@ -1,88 +1,18 @@
1
1
  module RelationToStruct::ActiveRecordBaseExtension
2
- extend ::ActiveSupport::Concern
3
-
4
2
  module ClassMethods
5
3
  def _sanitize_sql_for_relation_to_struct(sql)
6
4
  sanitized_sql = ActiveRecord::VERSION::MAJOR >= 5 ? sanitize_sql(sql) : sanitize_sql(sql, nil)
7
5
  end
8
6
 
9
- def structs_from_sql(struct_class, sql, binds=[])
10
- sanitized_sql = _sanitize_sql_for_relation_to_struct(sql)
11
- result = ActiveRecord::Base.uncached do
12
- connection.select_all(sanitized_sql, "Structs SQL Load", binds)
13
- end
14
-
15
- if result.columns.size != result.columns.uniq.size
16
- raise ArgumentError, 'Expected column names to be unique'
17
- end
18
-
19
- if result.columns != struct_class.members.collect(&:to_s)
20
- raise ArgumentError, 'Expected column names (and their order) to match struct attribute names'
21
- end
22
-
23
- if result.columns.size == 1
24
- result.cast_values().map do |tuple|
25
- struct_class.new(tuple)
26
- end
27
- else
28
- result.cast_values().map do |tuple|
29
- struct_class.new(*tuple)
30
- end
31
- end
32
- end
33
-
34
- def pluck_from_sql(sql, binds=[])
35
- sanitized_sql = _sanitize_sql_for_relation_to_struct(sql)
36
- result = ActiveRecord::Base.uncached do
37
- connection.select_all(sanitized_sql, "Pluck SQL Load", binds)
38
- end
39
- result.cast_values()
40
- end
41
-
42
- def value_from_sql(sql, binds=[])
43
- sanitized_sql = _sanitize_sql_for_relation_to_struct(sql)
44
- result = ActiveRecord::Base.uncached do
45
- connection.select_all(sanitized_sql, "Value SQL Load", binds)
46
- end
47
- raise ArgumentError, 'Expected exactly one column to be selected' unless result.columns.size == 1
48
-
49
- values = result.cast_values()
50
- case values.size
51
- when 0
52
- nil
53
- when 1
54
- values[0]
55
- else
56
- raise ArgumentError, 'Expected only a single result to be returned'
57
- end
58
- end
59
-
60
- def tuple_from_sql(sql, binds=[])
61
- sanitized_sql = _sanitize_sql_for_relation_to_struct(sql)
62
- result = ActiveRecord::Base.uncached do
63
- connection.select_all(sanitized_sql, "Value SQL Load", binds)
64
- end
65
- values = result.cast_values()
66
-
67
- case values.size
68
- when 0
69
- nil
70
- when 1
71
- result.columns.size == 1 ? values : values[0]
72
- else
73
- raise ArgumentError, 'Expected only a single result to be returned'
74
- end
75
- end
76
-
77
- def run_sql(sql, binds=[])
78
- sanitized_sql = _sanitize_sql_for_relation_to_struct(sql)
79
- # We don't need to build a result set unnecessarily; using
80
- # interface this also ensures we're clearing the result set
81
- # for manually memory managed object (e.g., when using the
82
- # PostgreSQL adaptor).
83
- connection.exec_update(sanitized_sql, "Run SQL", binds)
84
- end
7
+ delegate(
8
+ :structs_from_sql,
9
+ :pluck_from_sql,
10
+ :value_from_sql,
11
+ :tuple_from_sql,
12
+ :run_sql,
13
+ :to => :connection
14
+ )
85
15
  end
86
16
  end
87
17
 
88
- ::ActiveRecord::Base.send(:include, RelationToStruct::ActiveRecordBaseExtension)
18
+ ::ActiveRecord::Base.singleton_class.send(:prepend, RelationToStruct::ActiveRecordBaseExtension::ClassMethods)
@@ -0,0 +1,80 @@
1
+ module RelationToStruct::ActiveRecordConnectionAdapterExtension
2
+ def structs_from_sql(struct_class, sql, binds=[])
3
+ sanitized_sql = ActiveRecord::Base._sanitize_sql_for_relation_to_struct(sql)
4
+ result = ActiveRecord::Base.uncached do
5
+ select_all(sanitized_sql, "Structs SQL Load", binds)
6
+ end
7
+
8
+ if result.columns.size != result.columns.uniq.size
9
+ raise ArgumentError, 'Expected column names to be unique'
10
+ end
11
+
12
+ if result.columns != struct_class.members.collect(&:to_s)
13
+ raise ArgumentError, 'Expected column names (and their order) to match struct attribute names'
14
+ end
15
+
16
+ if result.columns.size == 1
17
+ result.cast_values().map do |tuple|
18
+ struct_class.new(tuple)
19
+ end
20
+ else
21
+ result.cast_values().map do |tuple|
22
+ struct_class.new(*tuple)
23
+ end
24
+ end
25
+ end
26
+
27
+ def pluck_from_sql(sql, binds=[])
28
+ sanitized_sql = ActiveRecord::Base._sanitize_sql_for_relation_to_struct(sql)
29
+ result = ActiveRecord::Base.uncached do
30
+ select_all(sanitized_sql, "Pluck SQL Load", binds)
31
+ end
32
+ result.cast_values()
33
+ end
34
+
35
+ def value_from_sql(sql, binds=[])
36
+ sanitized_sql = ActiveRecord::Base._sanitize_sql_for_relation_to_struct(sql)
37
+ result = ActiveRecord::Base.uncached do
38
+ select_all(sanitized_sql, "Value SQL Load", binds)
39
+ end
40
+ raise ArgumentError, 'Expected exactly one column to be selected' unless result.columns.size == 1
41
+
42
+ values = result.cast_values()
43
+ case values.size
44
+ when 0
45
+ nil
46
+ when 1
47
+ values[0]
48
+ else
49
+ raise ArgumentError, 'Expected only a single result to be returned'
50
+ end
51
+ end
52
+
53
+ def tuple_from_sql(sql, binds=[])
54
+ sanitized_sql = ActiveRecord::Base._sanitize_sql_for_relation_to_struct(sql)
55
+ result = ActiveRecord::Base.uncached do
56
+ select_all(sanitized_sql, "Value SQL Load", binds)
57
+ end
58
+ values = result.cast_values()
59
+
60
+ case values.size
61
+ when 0
62
+ nil
63
+ when 1
64
+ result.columns.size == 1 ? values : values[0]
65
+ else
66
+ raise ArgumentError, 'Expected only a single result to be returned'
67
+ end
68
+ end
69
+
70
+ def run_sql(sql, binds=[])
71
+ sanitized_sql = ActiveRecord::Base._sanitize_sql_for_relation_to_struct(sql)
72
+ # We don't need to build a result set unnecessarily; using
73
+ # interface this also ensures we're clearing the result set
74
+ # for manually memory managed object (e.g., when using the
75
+ # PostgreSQL adaptor).
76
+ exec_update(sanitized_sql, "Run SQL", binds)
77
+ end
78
+ end
79
+
80
+ ::ActiveRecord::ConnectionAdapters::AbstractAdapter.send(:prepend, RelationToStruct::ActiveRecordConnectionAdapterExtension)
@@ -1,6 +1,4 @@
1
1
  module RelationToStruct::ActiveRecordRelationExtension
2
- extend ::ActiveSupport::Concern
3
-
4
2
  def to_structs(struct_class)
5
3
  raise ArgumentError, 'Expected select_values to be present' unless self.select_values.present?
6
4
 
@@ -37,4 +35,4 @@ module RelationToStruct::ActiveRecordRelationExtension
37
35
  end
38
36
  end
39
37
 
40
- ::ActiveRecord::Relation.send(:include, RelationToStruct::ActiveRecordRelationExtension)
38
+ ::ActiveRecord::Relation.send(:prepend, RelationToStruct::ActiveRecordRelationExtension)
@@ -1,3 +1,3 @@
1
1
  module RelationToStruct
2
- VERSION = "1.5.1"
2
+ VERSION = "1.6.0"
3
3
  end
@@ -0,0 +1,270 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecord::ConnectionAdapters::AbstractAdapter do
4
+ before(:each) do
5
+ Economist.delete_all
6
+ EconomicSchool.delete_all
7
+ end
8
+
9
+ let(:connection) { ActiveRecord::Base.connection }
10
+
11
+ describe "#pluck_from_sql" do
12
+ it 'allows plucking with SQL directly' do
13
+ sql = "SELECT 1 * 23"
14
+ expect(connection.pluck_from_sql(sql)).to eq([23])
15
+ end
16
+
17
+ it 'allows plucking multiple columns with SQL directly' do
18
+ sql = "SELECT 1 * 23, 25"
19
+ expect(connection.pluck_from_sql(sql)).to eq([[23, 25]])
20
+ end
21
+
22
+ it 'bypasses the statement cache' do
23
+ sql = "SELECT random()"
24
+ value_1 = value_2 = nil
25
+
26
+ # Simulate a standard web request in Rails, since
27
+ # Rails enabled caching by default.
28
+ ActiveRecord::Base.cache do
29
+ value_1 = connection.pluck_from_sql(sql)
30
+ value_2 = connection.pluck_from_sql(sql)
31
+ end
32
+
33
+ expect(value_1).not_to eq(value_2)
34
+ end
35
+ end
36
+
37
+ describe "#value_from_sql" do
38
+ it 'allows selecting a value with SQL directly' do
39
+ sql = "SELECT 1 * 23"
40
+ expect(connection.value_from_sql(sql)).to eq(23)
41
+ end
42
+
43
+ it 'raises an error when multiple rows are selected' do
44
+ expect do
45
+ sql = "SELECT * FROM (VALUES (1), (2)) t"
46
+ connection.value_from_sql(sql)
47
+ end.to raise_error(ArgumentError, 'Expected only a single result to be returned')
48
+ end
49
+
50
+ it 'raises an error when multiple columns are selected' do
51
+ expect do
52
+ sql = "SELECT 1, 2"
53
+ connection.value_from_sql(sql)
54
+ end.to raise_error(ArgumentError, 'Expected exactly one column to be selected')
55
+ end
56
+
57
+ it 'supports binds' do
58
+ sql = ["SELECT 1 * ?", 5]
59
+ expect(connection.value_from_sql(sql)).to eq(5)
60
+ end
61
+
62
+ it 'supports arrays' do
63
+ if active_record_supports_arrays?
64
+ Economist.create!(name: 'F.A. Hayek')
65
+ Economist.create!(name: 'Ludwig von Mises')
66
+
67
+ result = connection.value_from_sql('SELECT ARRAY_AGG(name ORDER BY id) AS names FROM economists')
68
+ expect(result).to eq(['F.A. Hayek', 'Ludwig von Mises'])
69
+ else
70
+ skip "DB selection doesn't support ARRAY[]"
71
+ end
72
+ end
73
+
74
+ it 'bypasses the statement cache' do
75
+ sql = "SELECT random()"
76
+ value_1 = value_2 = nil
77
+
78
+ # Simulate a standard web request in Rails, since
79
+ # Rails enabled caching by default.
80
+ ActiveRecord::Base.cache do
81
+ value_1 = connection.value_from_sql(sql)
82
+ value_2 = connection.value_from_sql(sql)
83
+ end
84
+
85
+ expect(value_1).not_to eq(value_2)
86
+ end
87
+ end
88
+
89
+ describe "#tuple_from_sql" do
90
+ it 'allows selecting one value with SQL directly' do
91
+ sql = "SELECT 1"
92
+ expect(connection.tuple_from_sql(sql)).to eq([1])
93
+ end
94
+
95
+ it 'allows selecting multiple values with SQL directly' do
96
+ sql = "SELECT 1, 23"
97
+ expect(connection.tuple_from_sql(sql)).to eq([1, 23])
98
+ end
99
+
100
+ it 'raises an error when multiple rows are selected' do
101
+ expect do
102
+ sql = "SELECT * FROM (VALUES (1, 3), (2, 4)) t"
103
+ connection.tuple_from_sql(sql)
104
+ end.to raise_error(ArgumentError, 'Expected only a single result to be returned')
105
+ end
106
+
107
+ it 'supports binds' do
108
+ sql = ["SELECT ?, ?", 5, 6]
109
+ expect(connection.tuple_from_sql(sql)).to eq([5, 6])
110
+ end
111
+
112
+ it 'supports a single array' do
113
+ if active_record_supports_arrays?
114
+ Economist.create!(name: 'F.A. Hayek')
115
+ Economist.create!(name: 'Ludwig von Mises')
116
+
117
+ result = connection.tuple_from_sql(<<-SQL)
118
+ SELECT ARRAY_AGG(name ORDER BY id) AS names
119
+ FROM economists
120
+ SQL
121
+ expected_names = ['F.A. Hayek', 'Ludwig von Mises']
122
+ expect(result).to eq([expected_names])
123
+ else
124
+ skip "DB selection doesn't support ARRAY[]"
125
+ end
126
+ end
127
+
128
+ it 'supports multiple arrays' do
129
+ if active_record_supports_arrays?
130
+ Economist.create!(name: 'F.A. Hayek')
131
+ Economist.create!(name: 'Ludwig von Mises')
132
+
133
+ result = connection.tuple_from_sql(<<-SQL)
134
+ SELECT
135
+ ARRAY_AGG(name ORDER BY id) AS names,
136
+ ARRAY_AGG(CHAR_LENGTH(name) ORDER BY id) AS lengths
137
+ FROM economists
138
+ SQL
139
+ expected_names = ['F.A. Hayek', 'Ludwig von Mises']
140
+ expect(result).to eq([expected_names, expected_names.map(&:size)])
141
+ else
142
+ skip "DB selection doesn't support ARRAY[]"
143
+ end
144
+ end
145
+
146
+ it 'bypasses the statement cache' do
147
+ sql = "SELECT random()"
148
+ value_1 = value_2 = nil
149
+
150
+ # Simulate a standard web request in Rails, since
151
+ # Rails enabled caching by default.
152
+ ActiveRecord::Base.cache do
153
+ value_1 = connection.tuple_from_sql(sql)
154
+ value_2 = connection.tuple_from_sql(sql)
155
+ end
156
+
157
+ expect(value_1).not_to eq(value_2)
158
+ end
159
+ end
160
+
161
+ describe "#structs_from_sql" do
162
+ it 'ActiveRecord::Base should respond to :structs_from_sql' do
163
+ expect(connection.respond_to?(:structs_from_sql)).to eq(true)
164
+ end
165
+
166
+ it 'allows querying with SQL directly' do
167
+ test_struct = Struct.new(:number)
168
+ sql = "SELECT 1 * 23 AS number"
169
+ expect(connection.structs_from_sql(test_struct, sql)).to eq([test_struct.new(23)])
170
+ end
171
+
172
+ it 'properly casts a single array column' do
173
+ if active_record_supports_arrays?
174
+ Economist.create!(name: 'F.A. Hayek')
175
+ Economist.create!(name: 'Ludwig von Mises')
176
+
177
+ test_struct = Struct.new(:names)
178
+ structs_results = connection.structs_from_sql(test_struct, 'SELECT ARRAY_AGG(name ORDER BY id) AS names FROM economists')
179
+ expect(structs_results.first.names).to eq(['F.A. Hayek', 'Ludwig von Mises'])
180
+ else
181
+ skip "DB selection doesn't support ARRAY[]"
182
+ end
183
+ end
184
+
185
+ it 'raises an error when column count does not match struct size' do
186
+ expect do
187
+ test_struct = Struct.new(:id, :name, :extra_field)
188
+ connection.structs_from_sql(test_struct, 'SELECT id, name FROM economists')
189
+ end.to raise_error(ArgumentError, 'Expected column names (and their order) to match struct attribute names')
190
+ end
191
+
192
+ it 'raises an error when column names are not unique' do
193
+ expect do
194
+ test_struct = Struct.new(:id, :id2)
195
+ connection.structs_from_sql(test_struct, 'SELECT id, id FROM economists')
196
+ end.to raise_error(ArgumentError, 'Expected column names to be unique')
197
+ end
198
+
199
+ it 'raises an error when the column names do not match the struct attribute names' do
200
+ Economist.create!(name: 'F.A. Hayek')
201
+ expect do
202
+ test_struct = Struct.new(:value_a, :value_b)
203
+ connection.structs_from_sql(test_struct, 'SELECT 1 AS value_a, 2 AS value_b FROM economists')
204
+ end.not_to raise_error
205
+
206
+ expect do
207
+ test_struct = Struct.new(:value_a, :value_b)
208
+ connection.structs_from_sql(test_struct, 'SELECT 1 AS value_b, 2 AS value_a FROM economists')
209
+ end.to raise_error(ArgumentError, 'Expected column names (and their order) to match struct attribute names')
210
+
211
+ expect do
212
+ test_struct = Struct.new(:value_a, :value_b)
213
+ connection.structs_from_sql(test_struct, 'SELECT 1 AS value_a, 2 AS value_c FROM economists')
214
+ end.to raise_error(ArgumentError, 'Expected column names (and their order) to match struct attribute names')
215
+ end
216
+
217
+ it 'bypasses the statement cache' do
218
+ test_struct = Struct.new(:r)
219
+ sql = "SELECT random() AS r"
220
+ value_1 = value_2 = nil
221
+
222
+ # Simulate a standard web request in Rails, since
223
+ # Rails enabled caching by default.
224
+ ActiveRecord::Base.cache do
225
+ value_1 = connection.structs_from_sql(test_struct, sql)
226
+ value_2 = connection.structs_from_sql(test_struct, sql)
227
+ end
228
+
229
+ expect(value_1).not_to eq(value_2)
230
+ end
231
+ end
232
+
233
+ describe "#run_sql" do
234
+ it 'executes the provided SQL' do
235
+ sql = "INSERT INTO economic_schools(name) VALUES ('Chicago')"
236
+ expect do
237
+ ActiveRecord::Base.run_sql(sql)
238
+ end.to change { connection.value_from_sql("SELECT COUNT(*) FROM economic_schools") }.by(1)
239
+ end
240
+
241
+ it 'supports binds' do
242
+ sql = ["INSERT INTO economic_schools(name) VALUES (?)", "Chicago"]
243
+ expect do
244
+ ActiveRecord::Base.run_sql(sql)
245
+ end.to change { connection.value_from_sql("SELECT COUNT(*) FROM economic_schools") }.by(1)
246
+ end
247
+
248
+ it 'uses the exec_update API to avoid turning things in an ActiveRecord::Result' do
249
+ expect(ActiveRecord::Base.connection).to receive(:exec_update).exactly(:once)
250
+ sql = "SELECT 1"
251
+ connection.run_sql(sql)
252
+ end
253
+
254
+ it 'returns the number of rows modified for an INSERT' do
255
+ sql = "INSERT INTO economic_schools(name) VALUES ('Chicago'), ('Distributism')"
256
+ expect(connection.run_sql(sql)).to eq(2)
257
+ end
258
+
259
+ it 'bypasses the statement cache' do
260
+ # Simulate a standard web request in Rails, since
261
+ # Rails enabled caching by default.
262
+ ActiveRecord::Base.cache do
263
+ expect do
264
+ connection.run_sql("INSERT INTO economic_schools(name) VALUES ('Chicago')")
265
+ connection.run_sql("INSERT INTO economic_schools(name) VALUES ('Distributism')")
266
+ end.to change { connection.value_from_sql("SELECT COUNT(*) FROM economic_schools") }.by(2)
267
+ end
268
+ end
269
+ end
270
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relation_to_struct
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Coleman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-11-04 00:00:00.000000000 Z
11
+ date: 2020-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: appraisal
@@ -168,10 +168,12 @@ files:
168
168
  - gemfiles/rails_6_0.gemfile
169
169
  - lib/relation_to_struct.rb
170
170
  - lib/relation_to_struct/active_record_base_extension.rb
171
+ - lib/relation_to_struct/active_record_connection_adapter_extension.rb
171
172
  - lib/relation_to_struct/active_record_relation_extension.rb
172
173
  - lib/relation_to_struct/version.rb
173
174
  - relation_to_struct.gemspec
174
175
  - spec/active_record_base_spec.rb
176
+ - spec/active_record_connection_adapter_spec.rb
175
177
  - spec/active_record_helper/economic_school.rb
176
178
  - spec/active_record_helper/economist.rb
177
179
  - spec/active_record_helper/schema.rb
@@ -198,12 +200,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
198
200
  - !ruby/object:Gem::Version
199
201
  version: '0'
200
202
  requirements: []
201
- rubygems_version: 3.0.6
203
+ rubyforge_project:
204
+ rubygems_version: 2.5.2.3
202
205
  signing_key:
203
206
  specification_version: 4
204
207
  summary: Return struct results from ActiveRecord relation queries
205
208
  test_files:
206
209
  - spec/active_record_base_spec.rb
210
+ - spec/active_record_connection_adapter_spec.rb
207
211
  - spec/active_record_helper/economic_school.rb
208
212
  - spec/active_record_helper/economist.rb
209
213
  - spec/active_record_helper/schema.rb