relation_to_struct 1.5.1 → 1.6.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
- 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