relation_to_struct 1.4.1 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +26 -0
- data/lib/relation_to_struct/active_record_base_extension.rb +21 -4
- data/lib/relation_to_struct/version.rb +1 -1
- data/spec/active_record_base_spec.rb +95 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b36da83b17102b8a6f19d1bd7b55ba8486d2bd9b
|
4
|
+
data.tar.gz: 3693bc1c95c15877d4e58a66b5700210e4861ff6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a1a43b34975434afd52f51ddd8ab1f3ba135e976248865a67ffc4ddc7fcfb6d9423c25168f4228c538b6c617d9d5a85559ebeebc99ff460f3a3df368cf973c8b
|
7
|
+
data.tar.gz: 7ea17f61be34643cadeaa7377b0ca481ef574d32c2327a8986bf915a4b6d5e0bdae260477547d51f0ee49c197032f1471d8f28d5e25a386fea0906ffa7f27081
|
data/README.md
CHANGED
@@ -69,6 +69,32 @@ eos
|
|
69
69
|
ActiveRecord::Base.tuple_from_sql(sql) # => [id, name]
|
70
70
|
```
|
71
71
|
|
72
|
+
```
|
73
|
+
sql = <<-eos
|
74
|
+
SELECT 1
|
75
|
+
eos
|
76
|
+
|
77
|
+
ActiveRecord::Base.run_sql(sql) # => <no defined result>
|
78
|
+
```
|
79
|
+
|
80
|
+
```
|
81
|
+
sql = <<-eos
|
82
|
+
INSERT INTO foos(bar) VALUES(1)
|
83
|
+
eos
|
84
|
+
|
85
|
+
ActiveRecord::Base.run_sql(sql) # => <number of rows modified>
|
86
|
+
```
|
87
|
+
|
88
|
+
## Project Policy/Philosophy
|
89
|
+
|
90
|
+
Executing database queries should be clearly explicit in your application code. Implicit queries (e.g., in association accesses) is an anti-pattern that results in problems like N+1 querying.
|
91
|
+
|
92
|
+
### Query Caching
|
93
|
+
|
94
|
+
Query caching is another problem downstream from implicit querying. Because queries are happening "behind the scenes" and there's no obvious place for explicit result caching, it seems desirable to cache at the query level. But this approach applies caching at the wrong level: your application code must still expend all of the effort required to build a SQL query and (potentially) interpret results. Caching queries automatically (as Rails does by default in a web request) can easily lead to gotchas because the framework has no way of determining when caching is actually safe (both from a business logic and query contents perspective).
|
95
|
+
|
96
|
+
For this reason, **all methods added to `ActiveRecord::Base` explicitly disable query caching**. Rails defaults are respected, however, on extensions to `ActiveRecord::Relation` since it's not as obvious that those queries are intended to be explicit.
|
97
|
+
|
72
98
|
## Contributing
|
73
99
|
|
74
100
|
1. Fork it ( https://github.com/jcoleman/relation_to_struct/fork )
|
@@ -8,7 +8,9 @@ module RelationToStruct::ActiveRecordBaseExtension
|
|
8
8
|
|
9
9
|
def structs_from_sql(struct_class, sql, binds=[])
|
10
10
|
sanitized_sql = _sanitize_sql_for_relation_to_struct(sql)
|
11
|
-
result =
|
11
|
+
result = ActiveRecord::Base.uncached do
|
12
|
+
connection.select_all(sanitized_sql, "Structs SQL Load", binds)
|
13
|
+
end
|
12
14
|
|
13
15
|
if result.columns.size != result.columns.uniq.size
|
14
16
|
raise ArgumentError, 'Expected column names to be unique'
|
@@ -31,13 +33,17 @@ module RelationToStruct::ActiveRecordBaseExtension
|
|
31
33
|
|
32
34
|
def pluck_from_sql(sql, binds=[])
|
33
35
|
sanitized_sql = _sanitize_sql_for_relation_to_struct(sql)
|
34
|
-
result =
|
36
|
+
result = ActiveRecord::Base.uncached do
|
37
|
+
connection.select_all(sanitized_sql, "Pluck SQL Load", binds)
|
38
|
+
end
|
35
39
|
result.cast_values()
|
36
40
|
end
|
37
41
|
|
38
42
|
def value_from_sql(sql, binds=[])
|
39
43
|
sanitized_sql = _sanitize_sql_for_relation_to_struct(sql)
|
40
|
-
result =
|
44
|
+
result = ActiveRecord::Base.uncached do
|
45
|
+
connection.select_all(sanitized_sql, "Value SQL Load", binds)
|
46
|
+
end
|
41
47
|
raise ArgumentError, 'Expected exactly one column to be selected' unless result.columns.size == 1
|
42
48
|
|
43
49
|
values = result.cast_values()
|
@@ -53,7 +59,9 @@ module RelationToStruct::ActiveRecordBaseExtension
|
|
53
59
|
|
54
60
|
def tuple_from_sql(sql, binds=[])
|
55
61
|
sanitized_sql = _sanitize_sql_for_relation_to_struct(sql)
|
56
|
-
result =
|
62
|
+
result = ActiveRecord::Base.uncached do
|
63
|
+
connection.select_all(sanitized_sql, "Value SQL Load", binds)
|
64
|
+
end
|
57
65
|
values = result.cast_values()
|
58
66
|
|
59
67
|
case values.size
|
@@ -65,6 +73,15 @@ module RelationToStruct::ActiveRecordBaseExtension
|
|
65
73
|
raise ArgumentError, 'Expected only a single result to be returned'
|
66
74
|
end
|
67
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
|
68
85
|
end
|
69
86
|
end
|
70
87
|
|
@@ -16,6 +16,20 @@ describe ActiveRecord::Base do
|
|
16
16
|
sql = "SELECT 1 * 23, 25"
|
17
17
|
expect(ActiveRecord::Base.pluck_from_sql(sql)).to eq([[23, 25]])
|
18
18
|
end
|
19
|
+
|
20
|
+
it 'bypasses the statement cache' do
|
21
|
+
sql = "SELECT random()"
|
22
|
+
value_1 = value_2 = nil
|
23
|
+
|
24
|
+
# Simulate a standard web request in Rails, since
|
25
|
+
# Rails enabled caching by default.
|
26
|
+
ActiveRecord::Base.cache do
|
27
|
+
value_1 = ActiveRecord::Base.pluck_from_sql(sql)
|
28
|
+
value_2 = ActiveRecord::Base.pluck_from_sql(sql)
|
29
|
+
end
|
30
|
+
|
31
|
+
expect(value_1).not_to eq(value_2)
|
32
|
+
end
|
19
33
|
end
|
20
34
|
|
21
35
|
describe "#value_from_sql" do
|
@@ -54,6 +68,20 @@ describe ActiveRecord::Base do
|
|
54
68
|
skip "DB selection doesn't support ARRAY[]"
|
55
69
|
end
|
56
70
|
end
|
71
|
+
|
72
|
+
it 'bypasses the statement cache' do
|
73
|
+
sql = "SELECT random()"
|
74
|
+
value_1 = value_2 = nil
|
75
|
+
|
76
|
+
# Simulate a standard web request in Rails, since
|
77
|
+
# Rails enabled caching by default.
|
78
|
+
ActiveRecord::Base.cache do
|
79
|
+
value_1 = ActiveRecord::Base.value_from_sql(sql)
|
80
|
+
value_2 = ActiveRecord::Base.value_from_sql(sql)
|
81
|
+
end
|
82
|
+
|
83
|
+
expect(value_1).not_to eq(value_2)
|
84
|
+
end
|
57
85
|
end
|
58
86
|
|
59
87
|
describe "#tuple_from_sql" do
|
@@ -112,6 +140,20 @@ describe ActiveRecord::Base do
|
|
112
140
|
skip "DB selection doesn't support ARRAY[]"
|
113
141
|
end
|
114
142
|
end
|
143
|
+
|
144
|
+
it 'bypasses the statement cache' do
|
145
|
+
sql = "SELECT random()"
|
146
|
+
value_1 = value_2 = nil
|
147
|
+
|
148
|
+
# Simulate a standard web request in Rails, since
|
149
|
+
# Rails enabled caching by default.
|
150
|
+
ActiveRecord::Base.cache do
|
151
|
+
value_1 = ActiveRecord::Base.tuple_from_sql(sql)
|
152
|
+
value_2 = ActiveRecord::Base.tuple_from_sql(sql)
|
153
|
+
end
|
154
|
+
|
155
|
+
expect(value_1).not_to eq(value_2)
|
156
|
+
end
|
115
157
|
end
|
116
158
|
|
117
159
|
describe "#structs_from_sql" do
|
@@ -169,5 +211,58 @@ describe ActiveRecord::Base do
|
|
169
211
|
ActiveRecord::Base.structs_from_sql(test_struct, 'SELECT 1 AS value_a, 2 AS value_c FROM economists')
|
170
212
|
end.to raise_error(ArgumentError, 'Expected column names (and their order) to match struct attribute names')
|
171
213
|
end
|
214
|
+
|
215
|
+
it 'bypasses the statement cache' do
|
216
|
+
test_struct = Struct.new(:r)
|
217
|
+
sql = "SELECT random() AS r"
|
218
|
+
value_1 = value_2 = nil
|
219
|
+
|
220
|
+
# Simulate a standard web request in Rails, since
|
221
|
+
# Rails enabled caching by default.
|
222
|
+
ActiveRecord::Base.cache do
|
223
|
+
value_1 = ActiveRecord::Base.structs_from_sql(test_struct, sql)
|
224
|
+
value_2 = ActiveRecord::Base.structs_from_sql(test_struct, sql)
|
225
|
+
end
|
226
|
+
|
227
|
+
expect(value_1).not_to eq(value_2)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
describe "#run_sql" do
|
232
|
+
it 'executes the provided SQL' do
|
233
|
+
sql = "INSERT INTO economic_schools(name) VALUES ('Chicago')"
|
234
|
+
expect do
|
235
|
+
ActiveRecord::Base.run_sql(sql)
|
236
|
+
end.to change { ActiveRecord::Base.value_from_sql("SELECT COUNT(*) FROM economic_schools") }.by(1)
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'supports binds' do
|
240
|
+
sql = ["INSERT INTO economic_schools(name) VALUES (?)", "Chicago"]
|
241
|
+
expect do
|
242
|
+
ActiveRecord::Base.run_sql(sql)
|
243
|
+
end.to change { ActiveRecord::Base.value_from_sql("SELECT COUNT(*) FROM economic_schools") }.by(1)
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'uses the exec_update API to avoid turning things in an ActiveRecord::Result' do
|
247
|
+
expect(ActiveRecord::Base.connection).to receive(:exec_update).exactly(:once)
|
248
|
+
sql = "SELECT 1"
|
249
|
+
ActiveRecord::Base.run_sql(sql)
|
250
|
+
end
|
251
|
+
|
252
|
+
it 'returns the number of rows modified for an INSERT' do
|
253
|
+
sql = "INSERT INTO economic_schools(name) VALUES ('Chicago'), ('Distributism')"
|
254
|
+
expect(ActiveRecord::Base.run_sql(sql)).to eq(2)
|
255
|
+
end
|
256
|
+
|
257
|
+
it 'bypasses the statement cache' do
|
258
|
+
# Simulate a standard web request in Rails, since
|
259
|
+
# Rails enabled caching by default.
|
260
|
+
ActiveRecord::Base.cache do
|
261
|
+
expect do
|
262
|
+
ActiveRecord::Base.run_sql("INSERT INTO economic_schools(name) VALUES ('Chicago')")
|
263
|
+
ActiveRecord::Base.run_sql("INSERT INTO economic_schools(name) VALUES ('Distributism')")
|
264
|
+
end.to change { ActiveRecord::Base.value_from_sql("SELECT COUNT(*) FROM economic_schools") }.by(2)
|
265
|
+
end
|
266
|
+
end
|
172
267
|
end
|
173
268
|
end
|