occams-record 1.7.0 → 1.8.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
2
  SHA256:
3
- metadata.gz: 5f2590ba264934763d86310d4cd9b92459cb053589afa23fb4ea4c4b423f2eb7
4
- data.tar.gz: abb8aad716399e6b87d351e33a8f9ff629256964aed2f131795255cec3353b2a
3
+ metadata.gz: 846af974c8bc267fe8e901995d0aa3341b018d135eeb267c11d9add447cd0155
4
+ data.tar.gz: 763f8a038908cb9a990bcf305644661736dd9133e51a9d8fb8ece2b8ff018fd2
5
5
  SHA512:
6
- metadata.gz: 2aa46cce2b26cd5a6e0219651dcdace5391a7a1b67ae5d4e6c3a9a849c6b12a63a2c913208af0ae58d5e7c7ce6880fdc2c1081a3a4b7582b2a8300f76548c0e3
7
- data.tar.gz: c3cdc38d42b7c97894bbe34215e9a8143209732c78fd8b5d8351932d5e7290783ec8239c8f5c21e6fa4f2f36325b97a76088953a9b0a28a7fd71c697aaa91993
6
+ metadata.gz: 24e1c5dfb09cb35b2b7116cb40aafcb1eb215ac96e3c8bab7ff6558598b31e8abb77b49c2b711392567862b182e1aed705888e939f8b66f15285f2ca352dbc89
7
+ data.tar.gz: 04f78d14765b6ae4d9ccee5f01633132ed1e39662071dfc39f6e33583e9cd3e6214841225eb35ce2f678472ce34bf45e27e23660114f28cede6a27cdb38613b0
data/README.md CHANGED
@@ -11,120 +11,33 @@ OccamsRecord is a high-efficiency, advanced query library for use alongside Acti
11
11
  * 3x-5x faster than ActiveRecord queries, *minimum*.
12
12
  * Uses 1/3 the memory of ActiveRecord query results.
13
13
  * Eliminates the N+1 query problem. (This often exceeds the baseline 3x-5x gain.)
14
- * Support for cursors (Postgres only, new in v1.4.0)
15
14
 
16
15
  ### 2) Supercharged querying & eager loading
17
16
 
18
- Continue using ActiveRecord's query builder, but let Occams take over running them, eager loading, and raw SQL calls. None of the examples below are possible with ActiveRecord, but OccamsRecord makes them trivial. (More complete examples are shown later, but these should whet your appetite.)
19
-
20
- **Customize the SQL used to eager load associations**
21
-
22
- ```ruby
23
- OccamsRecord
24
- .query(User.active)
25
- .eager_load(:orders) { |l|
26
- l.scope { |q| q.where("created_at >= ?", date).order("created_at DESC") }
27
- }
28
- ```
29
-
30
- **Use `ORDER BY` with `find_each`/`find_in_batches`**
31
-
32
- ```ruby
33
- OccamsRecord
34
- .query(Order.order("created_at DESC"))
35
- .find_each { |order|
36
- ...
37
- }
38
- ```
39
-
40
- **Use cursors**
41
-
42
- ```ruby
43
- OccamsRecord
44
- .query(Order.order("created_at DESC"))
45
- .find_each_with_cursor { |order|
46
- ...
47
- }
48
- ```
49
-
50
- **Use `find_each`/`find_in_batches` with raw SQL**
51
-
52
- ```ruby
53
- OccamsRecord
54
- .sql("
55
- SELECT * FROM orders
56
- WHERE created_at >= %{date}
57
- LIMIT %{batch_limit}
58
- OFFSET %{batch_offset}",
59
- {date: 10.years.ago}
60
- )
61
- .find_each { |order|
62
- ...
63
- }
64
- ```
65
-
66
- **Eager load associations when you're writing raw SQL**
67
-
68
- ```ruby
69
- OccamsRecord
70
- .sql("
71
- SELECT * FROM users
72
- LEFT OUTER JOIN ...
73
- ")
74
- .model(User)
75
- .eager_load(:orders)
76
- ```
77
-
78
- **Use `pluck` with raw SQL**
79
-
80
- ```ruby
81
- OccamsRecord
82
- .sql("
83
- SELECT some_col FROM users
84
- INNER JOIN ...
85
- ")
86
- .pluck(:some_col)
87
- ```
88
-
89
- **Eager load "ad hoc associations" using raw SQL**
90
-
91
- Relationships are complicated, and sometimes they can't be expressed in ActiveRecord models. Define your relationship on the fly!
92
- (Don't worry, there's a full explanation later on.)
93
-
94
- ```ruby
95
- OccamsRecord
96
- .query(User.all)
97
- .eager_load_many(:orders, {:id => :user_id}, "
98
- SELECT user_id, orders.*
99
- FROM orders INNER JOIN ...
100
- WHERE user_id IN (%{ids})
101
- ")
102
- ```
17
+ * Customize the SQL used to eager load associations (order them, apply filters, etc)
18
+ * Use cursors (Postgres only)
19
+ * Use `ORDER BY` with `find_each`/`find_in_batches`
20
+ * Use `find_each`/`find_in_batches` with raw SQL
21
+ * Eager load associations off of raw SQL queries
22
+ * Use `pluck` with raw SQL queries
103
23
 
24
+ ### How does OccamsRecord do all this?
104
25
  [Look over the speed and memory measurements yourself!](https://github.com/jhollinger/occams-record/wiki/Measurements) OccamsRecord achieves all of this by making some **very specific trade-offs:**
105
26
 
106
27
  * OccamsRecord results are *read-only*.
107
28
  * OccamsRecord results are *purely database rows* - they don't have any instance methods from your Rails models.
108
- * You *must eager load* each assocation you intend to use. If you forget one, an exception will be raised.
29
+ * You *must eager load* each assocation you intend to use. If you try to use one you didn't eager load, an exception will be raised.
109
30
 
110
- ---
31
+ # Overview
111
32
 
112
- # Installation
33
+ Full documentation is available at [rubydoc.info/gems/occams-record](http://www.rubydoc.info/gems/occams-record). Code lives at at [github.com/jhollinger/occams-record](https://github.com/jhollinger/occams-record). Contributions welcome!
113
34
 
114
- Simply add it to your `Gemfile`:
35
+ Simply add `occams-record` to your `Gemfile`:
115
36
 
116
37
  ```ruby
117
38
  gem 'occams-record'
118
39
  ```
119
40
 
120
- ---
121
-
122
- # Overview
123
-
124
- Full documentation is available at [rubydoc.info/gems/occams-record](http://www.rubydoc.info/gems/occams-record).
125
-
126
- Code lives at at [github.com/jhollinger/occams-record](https://github.com/jhollinger/occams-record). Contributions welcome!
127
-
128
41
  Build your queries like normal, using ActiveRecord's excellent query builder. Then pass them off to Occams Record.
129
42
 
130
43
  ```ruby
@@ -371,7 +284,6 @@ The following ActiveRecord features are under consideration, but not high priori
371
284
  The following ActiveRecord features are not supported, and likely never will be. Pull requests are still welcome, though.
372
285
 
373
286
  * Eager loading `through` associations that involve a polymorphic association.
374
- * ActiveRecord enum types
375
287
  * ActiveRecord serialized types
376
288
 
377
289
  ---
@@ -2,13 +2,54 @@ module OccamsRecord
2
2
  module Pluck
3
3
  private
4
4
 
5
- def pluck_results(results, cols)
5
+ def pluck_results(results, cols, model: nil)
6
6
  if cols.size == 1
7
- col = cols[0].to_s
7
+ pluck_results_single(results, cols[0].to_s, model: model)
8
+ else
9
+ pluck_results_multi(results, cols.map(&:to_s), model: model)
10
+ end
11
+ end
12
+
13
+ # returns an array of values
14
+ def pluck_results_single(results, col, model: nil)
15
+ enum = model&.defined_enums&.[](col)
16
+ inv_enum = enum&.invert
17
+ if enum
18
+ results.map { |r|
19
+ val = r[col]
20
+ enum.has_key?(val) ? val : inv_enum[val]
21
+ }
22
+ else
23
+ # micro-optimization for when there are no enums
8
24
  results.map { |r| r[col] }
25
+ end
26
+ end
27
+
28
+ # returns an array of arrays
29
+ def pluck_results_multi(results, cols, model: nil)
30
+ any_enums = false
31
+ cols_with_enums = cols.map { |col|
32
+ enum = model&.defined_enums&.[](col)
33
+ any_enums ||= !!enum
34
+ [col, enum, enum&.invert]
35
+ }
36
+
37
+ if any_enums
38
+ results.map { |row|
39
+ cols_with_enums.map { |(col, enum, inv_enum)|
40
+ if enum
41
+ val = row[col]
42
+ enum.has_key?(val) ? val : inv_enum[val]
43
+ else
44
+ row[col]
45
+ end
46
+ }
47
+ }
9
48
  else
10
- cols = cols.map(&:to_s)
11
- results.map { |r| r.values_at(*cols) }
49
+ # micro-optimization for when there are no enums
50
+ results.map { |row|
51
+ cols.map { |col| row[col] }
52
+ }
12
53
  end
13
54
  end
14
55
  end
@@ -96,6 +96,8 @@ module OccamsRecord
96
96
  #
97
97
  def run
98
98
  sql = block_given? ? yield(scope).to_sql : scope.to_sql
99
+ return [] if sql.blank? # return early in case ActiveRecord::QueryMethods#none was used
100
+
99
101
  @query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
100
102
  result = if measure?
101
103
  record_start_time!
@@ -237,6 +239,8 @@ module OccamsRecord
237
239
  #
238
240
  def pluck(*cols)
239
241
  sql = (block_given? ? yield(scope).to_sql : scope).select(*cols).to_sql
242
+ return [] if sql.blank? # return early in case ActiveRecord::QueryMethods#none was used
243
+
240
244
  @query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
241
245
  result = if measure?
242
246
  record_start_time!
@@ -246,7 +250,7 @@ module OccamsRecord
246
250
  else
247
251
  model.connection.exec_query sql
248
252
  end
249
- pluck_results result, cols
253
+ pluck_results result, cols, model: @model
250
254
  end
251
255
  end
252
256
  end
@@ -229,7 +229,7 @@ module OccamsRecord
229
229
  else
230
230
  conn.exec_query _escaped_sql
231
231
  end
232
- pluck_results result, cols
232
+ pluck_results result, cols, model: @eager_loaders.model
233
233
  end
234
234
 
235
235
  private
@@ -54,16 +54,41 @@ module OccamsRecord
54
54
  # * MySQL ?
55
55
  #
56
56
  type = column_types[col] || model_column_types&.[](col)
57
+
58
+ #
59
+ # NOTE is also some variation in when enum values are mapped in different AR versions.
60
+ # In >=5.0, <=7.0, ActiveRecord::Result objects contain the human-readable values. In 4.2 and
61
+ # pre-release versions of 7.1, they instead have the RAW values (e.g. integers) which we must map ourselves.
62
+ #
63
+ enum = model&.defined_enums&.[](col)
64
+ inv_enum = enum&.invert
65
+
57
66
  case type&.type
58
67
  when nil
59
- define_method(col) { @raw_values[idx] }
68
+ if enum
69
+ define_method(col) {
70
+ val = @raw_values[idx]
71
+ enum.has_key?(val) ? val : inv_enum[val]
72
+ }
73
+ else
74
+ define_method(col) { @raw_values[idx] }
75
+ end
60
76
  when :datetime
61
77
  define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx])&.in_time_zone }
62
78
  when :boolean
63
79
  define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx]) }
64
80
  define_method("#{col}?") { !!send(col) }
65
81
  else
66
- define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx]) }
82
+ if enum
83
+ define_method(col) {
84
+ @cast_values[idx] ||= (
85
+ val = type.send(CASTER, @raw_values[idx])
86
+ enum.has_key?(val) ? val : inv_enum[val]
87
+ )
88
+ }
89
+ else
90
+ define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx]) }
91
+ end
67
92
  end
68
93
  end
69
94
  end
@@ -40,7 +40,7 @@ module OccamsRecord
40
40
  #
41
41
  # Hash-like accessor for attributes and associations.
42
42
  #
43
- # @param attr [String|Symbol\
43
+ # @param attr [String|Symbol]
44
44
  # @return [Object]
45
45
  #
46
46
  def [](attr)
@@ -3,5 +3,5 @@
3
3
  #
4
4
  module OccamsRecord
5
5
  # @private
6
- VERSION = "1.7.0".freeze
6
+ VERSION = "1.8.0".freeze
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: occams-record
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Hollinger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-28 00:00:00.000000000 Z
11
+ date: 2023-07-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord