occams-record 1.2.1 → 1.4.0.pre.beta1

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: a1e01657287159a2aa1a27ff66c1932ae83235ad0140dbaafd58103862ddaa8f
4
- data.tar.gz: ec9530c3844cd79739d1a2387031654e85f4a2a127d948df0c537431aa235ed5
3
+ metadata.gz: 662bd65e909a1d957ebdbbd2d3bec3fe96fb45aac2117a786c9a3ad02b03259b
4
+ data.tar.gz: abeb69ff706dbb11f3fae3672ae60c407080f418fce7330779a7b550915aa886
5
5
  SHA512:
6
- metadata.gz: a0303147fa894e42d0642020a4a207c67b3a79fa3fc2dfa1b9be819224c8eee6bb261bfa2079a073cd1894f744b4995f307afff594f458c5b7ae7574b26b0d32
7
- data.tar.gz: bf81dc3eeb78cbf9ed99740a1c4c8e1c71287f4af278c2a623d9220384460f952ccc50fa835393ef3fd416fa0019fccc6a6e41d0c8dbd77c637f08689ca5c585
6
+ metadata.gz: 8981d9c820b64ebeca28dcbe3ab7ac44e35d8ec7e14df201e890ccf6cf1cf1888d6f78ba6cac001f1a9c458f604189b1aafaf7ff644288499faf5d5afeb896c3
7
+ data.tar.gz: cdd61c50313a964db4ebdb271dbb875fc4ea888dd0b57df78be78d043e655e04e821e0cef8d9b60034e90866289a0d764f72a7728a18f46c778e511101b72f4a
data/README.md CHANGED
@@ -17,17 +17,17 @@ Continue using ActiveRecord's query builder, but let Occams take over running th
17
17
  **Customize the SQL used to eager load associations**
18
18
 
19
19
  ```ruby
20
- OccamsRecord.
21
- query(User.active).
22
- eager_load(:orders, ->(q) { q.where("created_at >= ?", date).order("created_at DESC") })
20
+ OccamsRecord
21
+ .query(User.active)
22
+ .eager_load(:orders, ->(q) { q.where("created_at >= ?", date).order("created_at DESC") })
23
23
  ```
24
24
 
25
25
  **Use `ORDER BY` with `find_each`/`find_in_batches`**
26
26
 
27
27
  ```ruby
28
- OccamsRecord.
29
- query(Order.order("created_at DESC")).
30
- find_each { |order|
28
+ OccamsRecord
29
+ .query(Order.order("created_at DESC"))
30
+ .find_each { |order|
31
31
  ...
32
32
  }
33
33
  ```
@@ -35,15 +35,15 @@ OccamsRecord.
35
35
  **Use `find_each`/`find_in_batches` with raw SQL**
36
36
 
37
37
  ```ruby
38
- OccamsRecord.
39
- sql("
38
+ OccamsRecord
39
+ .sql("
40
40
  SELECT * FROM orders
41
41
  WHERE created_at >= %{date}
42
42
  LIMIT %{batch_limit}
43
43
  OFFSET %{batch_offset}",
44
44
  {date: 10.years.ago}
45
- ).
46
- find_each { |order|
45
+ )
46
+ .find_each { |order|
47
47
  ...
48
48
  }
49
49
  ```
@@ -51,13 +51,13 @@ OccamsRecord.
51
51
  **Eager load associations when you're writing raw SQL**
52
52
 
53
53
  ```ruby
54
- OccamsRecord.
55
- sql("
54
+ OccamsRecord
55
+ .sql("
56
56
  SELECT * FROM users
57
57
  LEFT OUTER JOIN ...
58
- ").
59
- model(User).
60
- eager_load(:orders)
58
+ ")
59
+ .model(User)
60
+ .eager_load(:orders)
61
61
  ```
62
62
 
63
63
  **Eager load "ad hoc associations" using raw SQL**
@@ -66,9 +66,9 @@ Relationships are complicated, and sometimes they can't be expressed in ActiveRe
66
66
  (Don't worry, there's a full explanation later on.)
67
67
 
68
68
  ```ruby
69
- OccamsRecord.
70
- query(User.all).
71
- eager_load_many(:orders, {:id => :user_id}, "
69
+ OccamsRecord
70
+ .query(User.all)
71
+ .eager_load_many(:orders, {:id => :user_id}, "
72
72
  SELECT user_id, orders.*
73
73
  FROM orders INNER JOIN ...
74
74
  WHERE user_id IN (%{ids})
@@ -102,14 +102,14 @@ Code lives at at [github.com/jhollinger/occams-record](https://github.com/jholli
102
102
  Build your queries like normal, using ActiveRecord's excellent query builder. Then pass them off to Occams Record.
103
103
 
104
104
  ```ruby
105
- q = Order.
106
- completed.
107
- where("order_date > ?", 30.days.ago).
108
- order("order_date DESC")
109
-
110
- orders = OccamsRecord.
111
- query(q).
112
- run
105
+ q = Order
106
+ .completed
107
+ .where("order_date > ?", 30.days.ago)
108
+ .order("order_date DESC")
109
+
110
+ orders = OccamsRecord
111
+ .query(q)
112
+ .run
113
113
  ````
114
114
 
115
115
  `each`, `map`, `reduce`, and other Enumerable methods may be used instead of *run*. `find_each` and `find_in_batches` are also supported, and unlike in ActiveRecord, `ORDER BY` works as you'd expect.
@@ -121,14 +121,14 @@ Occams Record has great support for raw SQL queries too, but we'll get to those
121
121
  Eager loading is similiar to ActiveRecord's `preload`: each association is loaded in a separate query. Unlike ActiveRecord, nested associations use blocks instead of Hashes. More importantly, if you try to use an association you didn't eager load *an exception will be raised*. In other words, the N+1 query problem simply doesn't exist.
122
122
 
123
123
  ```ruby
124
- OccamsRecord.
125
- query(q).
126
- eager_load(:customer).
127
- eager_load(:line_items) {
124
+ OccamsRecord
125
+ .query(q)
126
+ .eager_load(:customer)
127
+ .eager_load(:line_items) {
128
128
  eager_load(:product)
129
129
  eager_load(:something_else)
130
- }.
131
- find_each { |order|
130
+ }
131
+ .find_each { |order|
132
132
  puts order.customer.name
133
133
  order.line_items.each { |line_item|
134
134
  puts line_item.product.name
@@ -143,17 +143,17 @@ OccamsRecord.
143
143
  Occams Record allows you to tweak the SQL of any eager load. Pull back only the columns you need, change the order, add a `WHERE` clause, etc.
144
144
 
145
145
  ```ruby
146
- orders = OccamsRecord.
147
- query(q).
146
+ orders = OccamsRecord
147
+ .query(q)
148
148
  # Only SELECT the columns you need. Your DBA will thank you.
149
- eager_load(:customer, select: "id, name").
149
+ .eager_load(:customer, select: "id, name")
150
150
 
151
151
  # A Proc can use ActiveRecord's query builder
152
- eager_load(:line_items, ->(q) { q.active.order("created_at") }) {
152
+ .eager_load(:line_items, ->(q) { q.active.order("created_at") }) {
153
153
  eager_load(:product)
154
154
  eager_load(:something_else)
155
- }.
156
- run
155
+ }
156
+ .run
157
157
  ```
158
158
 
159
159
  Occams Record also supports loading ad hoc associations using raw SQL. We'll get to that in the next section.
@@ -167,8 +167,8 @@ ActiveRecord has raw SQL escape hatches like `find_by_sql` and `exec_query`, but
167
167
  To use `find_each`/`find_in_batches` you must provide the limit and offset statements yourself; Occams will provide the values. Also, notice that the binding syntax is a bit different (it uses Ruby's built-in named string substitution).
168
168
 
169
169
  ```ruby
170
- OccamsRecord.
171
- sql("
170
+ OccamsRecord
171
+ .sql("
172
172
  SELECT * FROM orders
173
173
  WHERE order_date > %{date}
174
174
  ORDER BY order_date DESC, id
@@ -176,8 +176,8 @@ OccamsRecord.
176
176
  OFFSET %{batch_offset}
177
177
  ", {
178
178
  date: 10.years.ago
179
- }).
180
- find_each(batch_size: 1000) do |order|
179
+ })
180
+ .find_each(batch_size: 1000) do |order|
181
181
  ...
182
182
  end
183
183
  ```
@@ -187,17 +187,17 @@ OccamsRecord.
187
187
  To use `eager_load` with a raw SQL query you must tell Occams what the base model is. (That doesn't apply if you're loading an ad hoc, raw SQL association. We'll get to those next.)
188
188
 
189
189
  ```ruby
190
- orders = OccamsRecord.
191
- sql("
190
+ orders = OccamsRecord
191
+ .sql("
192
192
  SELECT * FROM orders
193
193
  WHERE order_date > %{date}
194
194
  ORDER BY order_date DESC, id
195
195
  ", {
196
196
  date: 30.days.ago
197
- }).
198
- model(Order).
199
- eager_load(:customer).
200
- run
197
+ })
198
+ .model(Order)
199
+ .eager_load(:customer)
200
+ .run
201
201
  ```
202
202
 
203
203
  ## Raw SQL eager loading
@@ -205,14 +205,14 @@ orders = OccamsRecord.
205
205
  Let's say we want to load each product with an array of all customers who've ordered it. We *could* do that by loading various nested associations:
206
206
 
207
207
  ```ruby
208
- products_with_orders = OccamsRecord.
209
- query(Product.all).
210
- eager_load(:line_items) {
208
+ products_with_orders = OccamsRecord
209
+ .query(Product.all)
210
+ .eager_load(:line_items) {
211
211
  eager_load(:order) {
212
212
  eager_load(:customer)
213
213
  }
214
- }.
215
- map { |product|
214
+ }
215
+ .map { |product|
216
216
  customers = product.line_items.map(&:order).map(&:customer).uniq
217
217
  [product, customers]
218
218
  }
@@ -221,9 +221,9 @@ products_with_orders = OccamsRecord.
221
221
  But that's very wasteful. Occams gives us better options: `eager_load_many` and `eager_load_one`.
222
222
 
223
223
  ```ruby
224
- products = OccamsRecord.
225
- query(Product.all).
226
- eager_load_many(:customers, {:id => :product_id}, "
224
+ products = OccamsRecord
225
+ .query(Product.all)
226
+ .eager_load_many(:customers, {:id => :product_id}, "
227
227
  SELECT DISTINCT product_id, customers.*
228
228
  FROM line_items
229
229
  INNER JOIN orders ON line_items.order_id = orders.id
@@ -231,8 +231,8 @@ products = OccamsRecord.
231
231
  WHERE line_items.product_id IN (%{ids})
232
232
  ", binds: {
233
233
  # additional bind values (ids will be passed in for you)
234
- }).
235
- run
234
+ })
235
+ .run
236
236
  ```
237
237
 
238
238
  `eager_load_many` is declaring an ad hoc *has_many* association called *customers*. The `{:id => :product_id}` Hash defines the mapping: *id* in the parent record maps to *product_id* in the child records.
@@ -258,12 +258,12 @@ module MyProductMethods
258
258
  end
259
259
  end
260
260
 
261
- orders = OccamsRecord.
262
- query(Order.all, use: MyOrderMethods).
263
- eager_load(:line_items) {
261
+ orders = OccamsRecord
262
+ .query(Order.all, use: MyOrderMethods)
263
+ .eager_load(:line_items) {
264
264
  eager_load(:product, use: [MyProductMethods, OtherMethods])
265
- }.
266
- run
265
+ }
266
+ .run
267
267
  ```
268
268
 
269
269
  ---
@@ -300,6 +300,9 @@ bundle exec rake test
300
300
 
301
301
  # test against Postgres
302
302
  TEST_DATABASE_URL=postgres://postgres@localhost:5432/occams_record bundle exec rake test
303
+
304
+ # test against MySQL
305
+ TEST_DATABASE_URL=mysql2://root:@127.0.0.1:3306/occams_record bundle exec rake test
303
306
  ```
304
307
 
305
308
  **Test against all supported ActiveRecord versions**
@@ -307,11 +310,14 @@ TEST_DATABASE_URL=postgres://postgres@localhost:5432/occams_record bundle exec r
307
310
  ```bash
308
311
  bundle exec appraisal install
309
312
 
310
- # test against SQLite
313
+ # test against all supported AR versions (defaults to SQLite)
311
314
  bundle exec appraisal rake test
312
315
 
316
+ # test against a specific AR version
317
+ bundle exec appraisal ar-7.0 rake test
318
+
313
319
  # test against Postgres
314
- TEST_DATABASE_URL=postgres://postgres@localhost:5432/occams_record bundle exec appraisal rake test
320
+ TEST_DATABASE_URL=postgresql://postgres@localhost:5432/occams_record bundle exec appraisal rake test
315
321
  ```
316
322
 
317
323
  # License
@@ -0,0 +1,59 @@
1
+ module OccamsRecord
2
+ module Batches
3
+ module CursorHelpers
4
+ #
5
+ # Loads records in batches of N and yields each record to a block (if given). If no block is given,
6
+ # returns an Enumerator.
7
+ #
8
+ # NOTE Unlike find_each, batches are loaded using a cursor, which offers better performance.
9
+ # Postgres only. See the docs for OccamsRecord::Cursor for more details.
10
+ #
11
+ # @param batch_size [Integer] fetch this many rows at once
12
+ # @param use_transaction [Boolean] Ensure it runs inside of a database transaction
13
+ # @yield [OccamsRecord::Results::Row]
14
+ # @return [Enumerator] will yield each record
15
+ #
16
+ def find_each_with_cursor(batch_size: 1000, use_transaction: true)
17
+ enum = Enumerator.new { |y|
18
+ cursor.open(use_transaction: use_transaction) { |c|
19
+ c.each(batch_size: batch_size) { |record|
20
+ y.yield record
21
+ }
22
+ }
23
+ }
24
+ if block_given?
25
+ enum.each { |record| yield record }
26
+ else
27
+ enum
28
+ end
29
+ end
30
+
31
+ #
32
+ # Loads records in batches of N and yields each batch to a block (if given). If no block is given,
33
+ # returns an Enumerator.
34
+ #
35
+ # NOTE Unlike find_in_batches, batches are loaded using a cursor, which offers better performance.
36
+ # Postgres only. See the docs for OccamsRecord::Cursor for more details.
37
+ #
38
+ # @param batch_size [Integer] fetch this many rows at once
39
+ # @param use_transaction [Boolean] Ensure it runs inside of a database transaction
40
+ # @yield [OccamsRecord::Results::Row]
41
+ # @return [Enumerator] will yield each record
42
+ #
43
+ def find_in_batches_with_cursor(batch_size: 1000, use_transaction: true)
44
+ enum = Enumerator.new { |y|
45
+ cursor.open(use_transaction: use_transaction) { |c|
46
+ c.each_batch(batch_size: batch_size) { |batch|
47
+ y.yield batch
48
+ }
49
+ }
50
+ }
51
+ if block_given?
52
+ enum.each { |batch| yield batch }
53
+ else
54
+ enum
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,53 @@
1
+ module OccamsRecord
2
+ module Batches
3
+ module OffsetLimit
4
+ class RawQuery
5
+ def initialize(conn, sql, binds, use: nil, query_logger: nil, eager_loaders: nil)
6
+ @conn, @sql, @binds = conn, sql, binds
7
+ @use, @query_logger, @eager_loaders = use, query_logger, eager_loaders
8
+
9
+ unless @sql =~ /LIMIT\s+%\{batch_limit\}/i and @sql =~ /OFFSET\s+%\{batch_offset\}/i
10
+ raise ArgumentError, "When using find_each/find_in_batches you must specify 'LIMIT %{batch_limit} OFFSET %{batch_offset}'. SQL statement: #{@sql}"
11
+ end
12
+ end
13
+
14
+ #
15
+ # Returns an Enumerator that yields batches of records, of size "of".
16
+ # The SQL string must include 'LIMIT %{batch_limit} OFFSET %{batch_offset}'.
17
+ # The bind values will be provided by OccamsRecord.
18
+ #
19
+ # @param batch_size [Integer] batch size
20
+ # @param use_transaction [Boolean] Ensure it runs inside of a database transaction
21
+ # @return [Enumerator] yields batches
22
+ #
23
+ def enum(batch_size:, use_transaction: true)
24
+ Enumerator.new do |y|
25
+ if use_transaction and @conn.open_transactions == 0
26
+ @conn.transaction {
27
+ run_batches y, batch_size
28
+ }
29
+ else
30
+ run_batches y, batch_size
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def run_batches(y, of)
38
+ offset = 0
39
+ loop do
40
+ results = ::OccamsRecord::RawQuery.new(@sql, @binds.merge({
41
+ batch_limit: of,
42
+ batch_offset: offset,
43
+ }), use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders).run
44
+
45
+ y.yield results if results.any?
46
+ break if results.size < of
47
+ offset += results.size
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,72 @@
1
+ module OccamsRecord
2
+ #
3
+ # Methods for building batch finding methods. It expects "model" and "scope" methods to be present.
4
+ #
5
+ module Batches
6
+ module OffsetLimit
7
+ class Scoped
8
+ def initialize(model, scope, use: nil, query_logger: nil, eager_loaders: nil)
9
+ @model, @scope = model, scope
10
+ @use, @query_logger, @eager_loaders = use, query_logger, eager_loaders
11
+ end
12
+
13
+ #
14
+ # Returns an Enumerator that yields batches of records, of size "of".
15
+ # NOTE ActiveRecord 5+ provides the 'in_batches' method to do something
16
+ # similiar, although 4.2 does not. Also it does not respect ORDER BY,
17
+ # whereas this does.
18
+ #
19
+ # @param batch_size [Integer] batch size
20
+ # @param use_transaction [Boolean] Ensure it runs inside of a database transaction
21
+ # @param append_order_by [String] Append this column to ORDER BY to ensure consistent results. Defaults to the primary key. Pass false to disable.
22
+ # @return [Enumerator] yields batches
23
+ #
24
+ def enum(batch_size:, use_transaction: true, append_order_by: nil)
25
+ append_order =
26
+ case append_order_by
27
+ when false then nil
28
+ when nil then @model.primary_key
29
+ else append_order_by
30
+ end
31
+
32
+ Enumerator.new do |y|
33
+ if use_transaction and @model.connection.open_transactions == 0
34
+ @model.connection.transaction {
35
+ run_batches y, batch_size, append_order
36
+ }
37
+ else
38
+ run_batches y, batch_size, append_order
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def run_batches(y, of, append_order_by = nil)
46
+ limit = @scope.limit_value
47
+ batch_size = limit && limit < of ? limit : of
48
+
49
+ offset = @scope.offset_value || 0
50
+ out_of_records, count = false, 0
51
+ order_by =
52
+ if append_order_by
53
+ append_order_by.to_s == @model.primary_key.to_s ? append_order_by.to_sym : append_order_by
54
+ end
55
+
56
+ until out_of_records
57
+ l = limit && batch_size > limit - count ? limit - count : batch_size
58
+ q = @scope
59
+ q = q.order(order_by) if order_by
60
+ q = q.offset(offset).limit(l)
61
+ results = ::OccamsRecord::Query.new(q, use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders).run
62
+
63
+ y.yield results if results.any?
64
+ count += results.size
65
+ offset += results.size
66
+ out_of_records = results.size < batch_size || (limit && count >= limit)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,237 @@
1
+ require 'securerandom'
2
+
3
+ module OccamsRecord
4
+ #
5
+ # An interface to database cursors. Supported databases:
6
+ # * PostgreSQL
7
+ #
8
+ class Cursor
9
+ # @private
10
+ SCROLL = {
11
+ true => "SCROLL",
12
+ false => "NO SCROLL",
13
+ nil => "",
14
+ }.freeze
15
+
16
+ # @private
17
+ HOLD = {
18
+ true => "WITH HOLD",
19
+ false => "WITHOUT HOLD",
20
+ nil => "",
21
+ }.freeze
22
+
23
+ # @private
24
+ DIRECTIONS = {
25
+ next: "NEXT",
26
+ prior: "PRIOR",
27
+ first: "FIRST",
28
+ last: "LAST",
29
+ absolute: "ABSOLUTE",
30
+ relative: "RELATIVE",
31
+ forward: "FORWARD",
32
+ backward: "BACKWARD",
33
+ }.freeze
34
+
35
+ # @return [ActiveRecord::Connection]
36
+ attr_reader :conn
37
+
38
+ # Name of the cursor
39
+ # @return [String]
40
+ attr_reader :name
41
+
42
+ # Name of the cursor (safely SQL-escaped)
43
+ # @return [String]
44
+ attr_reader :quoted_name
45
+
46
+ #
47
+ # Initializes a new Cursor. NOTE all operations must be performed within a block passed to #open.
48
+ #
49
+ # While you CAN manually initialize a cursor, it's more common to get one via OccamsRecord::Query#cursor
50
+ # or OccamsRecord::RawQuery#cursor.
51
+ #
52
+ # @param conn [ActiveRecord::Connection]
53
+ # @param sql [String] The query to run
54
+ # @param name [String] Specify a name for the cursor (defaults to a random name)
55
+ # @param scroll [Boolean] true = SCROLL, false = NO SCROLL, nil = default behavior of DB
56
+ # @param hold [Boolean] true = WITH HOLD, false = WITHOUT HOLD, nil = default behavior of DB
57
+ # @param use [Array<Module>] optional Module to include in the result class (single or array)
58
+ # @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
59
+ # @param eager_loaders [OccamsRecord::EagerLoaders::Context]
60
+ #
61
+ def initialize(conn, sql, name: nil, scroll: nil, hold: nil, use: nil, query_logger: nil, eager_loaders: nil)
62
+ @conn, @sql = conn, sql
63
+ @scroll = SCROLL.fetch(scroll)
64
+ @hold = HOLD.fetch(hold)
65
+ @use, @query_logger, @eager_loaders = use, query_logger, eager_loaders
66
+ @name = name || "occams_cursor_#{SecureRandom.hex 4}"
67
+ @quoted_name = conn.quote_table_name(@name)
68
+ end
69
+
70
+ #
71
+ # Declares and opens the cursor, runs the given block (yielding self), and closes the cursor.
72
+ #
73
+ # cursor.open do |c|
74
+ # c.fetch :forward, 100
75
+ # end
76
+ #
77
+ # @param use_transaction [Boolean] When true, ensures it's wrapped in a transaction
78
+ # @yield [self]
79
+ # @return the value returned by the block
80
+ #
81
+ def open(use_transaction: true)
82
+ raise ArgumentError, "A block is required" unless block_given?
83
+ if use_transaction and conn.open_transactions == 0
84
+ conn.transaction {
85
+ perform { yield self }
86
+ }
87
+ else
88
+ perform { yield self }
89
+ end
90
+ end
91
+
92
+ #
93
+ # Loads records in batches of N and yields each record to a block (if given). If no block is given,
94
+ # returns an Enumerator.
95
+ #
96
+ # cursor.open do |c|
97
+ # c.each do |record|
98
+ # ...
99
+ # end
100
+ # end
101
+ #
102
+ # @param batch_size [Integer] fetch this many rows at once
103
+ #
104
+ def each(batch_size: 1000)
105
+ enum = Enumerator.new { |y|
106
+ each_batch(batch_size: batch_size).each { |batch|
107
+ batch.each { |record| y.yield record }
108
+ }
109
+ }
110
+ if block_given?
111
+ enum.each { |record| yield record }
112
+ else
113
+ enum
114
+ end
115
+ end
116
+
117
+ #
118
+ # Loads records in batches of N and yields each batch to a block (if given). If no block is given,
119
+ # returns an Enumerator.
120
+ #
121
+ # cursor.open do |c|
122
+ # c.each_batch do |batch|
123
+ # ...
124
+ # end
125
+ # end
126
+ #
127
+ # @param batch_size [Integer] fetch this many rows at once
128
+ #
129
+ def each_batch(batch_size: 1000)
130
+ enum = Enumerator.new { |y|
131
+ out_of_records = false
132
+ until out_of_records
133
+ results = fetch :forward, batch_size
134
+ y.yield results if results.any?
135
+ out_of_records = results.size < batch_size
136
+ end
137
+ }
138
+ if block_given?
139
+ enum.each { |batch| yield batch }
140
+ else
141
+ enum
142
+ end
143
+ end
144
+
145
+ #
146
+ # Fetch records in the given direction.
147
+ #
148
+ # cursor.open do |c|
149
+ # c.fetch :forward, 100
150
+ # ...
151
+ # end
152
+ #
153
+ # @param direction [Symbol] :next, :prior, :first, :last, :absolute, :relative, :forward or :backward
154
+ # @param num [Integer] number of rows to fetch (optional for some directions)
155
+ # @return [OccamsRecord::Results::Row]
156
+ #
157
+ def fetch(direction, num = nil)
158
+ query "FETCH %{dir} %{num} FROM %{name}".freeze % {
159
+ dir: DIRECTIONS.fetch(direction),
160
+ num: num&.to_i,
161
+ name: @quoted_name,
162
+ }
163
+ end
164
+
165
+ #
166
+ # Move the cursor the given direction.
167
+ #
168
+ # cursor.open do |c|
169
+ # ...
170
+ # c.move :backward, 100
171
+ # ...
172
+ # end
173
+ #
174
+ # @param direction [Symbol] :next, :prior, :first, :last, :absolute, :relative, :forward or :backward
175
+ # @param num [Integer] number of rows to move (optional for some directions)
176
+ #
177
+ def move(direction, num = nil)
178
+ query "MOVE %{dir} %{num} FROM %{name}".freeze % {
179
+ dir: DIRECTIONS.fetch(direction),
180
+ num: num&.to_i,
181
+ name: @quoted_name,
182
+ }
183
+ end
184
+
185
+ #
186
+ # Run an arbitrary query on the cursor. Use 'binds' to escape inputs.
187
+ #
188
+ # cursor.open do |c|
189
+ # c.query("FETCH FORWARD %{num} FOR #{c.quoted_name}", {num: 100})
190
+ # ...
191
+ # end
192
+ #
193
+ def query(sql, binds = {})
194
+ ::OccamsRecord::RawQuery.new(sql, binds, use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders, connection: conn).run
195
+ end
196
+
197
+ #
198
+ # Run an arbitrary command on the cursor. Use 'binds' to escape inputs.
199
+ #
200
+ # cursor.open do |c|
201
+ # c.execute("MOVE FORWARD %{num} FOR #{c.quoted_name}", {num: 100})
202
+ # ...
203
+ # end
204
+ #
205
+ def execute(sql, binds = {})
206
+ conn.execute(sql % binds.reduce({}) { |acc, (key, val)|
207
+ acc[key] = conn.quote(val)
208
+ acc
209
+ })
210
+ end
211
+
212
+ private
213
+
214
+ def perform
215
+ ex = nil
216
+ conn.execute "DECLARE %{name} %{scroll} CURSOR %{hold} FOR %{query}".freeze % {
217
+ name: @quoted_name,
218
+ scroll: @scroll,
219
+ hold: @hold,
220
+ query: @sql,
221
+ }
222
+ yield
223
+ rescue => e
224
+ ex = e
225
+ raise ex
226
+ ensure
227
+ begin
228
+ conn.execute "CLOSE %{name}".freeze % {name: @quoted_name}
229
+ rescue => e
230
+ # Don't let an error from CLOSE (like a dead transaction) hide what lead to the error with CLOSE (like bad SQL that raised an error and aborted the transaction)
231
+ raise ex || e
232
+ else
233
+ raise ex if ex
234
+ end
235
+ end
236
+ end
237
+ end
@@ -56,14 +56,14 @@ module OccamsRecord
56
56
  #
57
57
  # The %{category_ids} bind param will be provided for you, and in this case will be all the category_id values from the Widget query.
58
58
  #
59
- # res = OccamsRecord.
60
- # query(Widget.order("name")).
61
- # eager_load_one(:category, {:category_id => :id}, "
59
+ # res = OccamsRecord
60
+ # .query(Widget.order("name"))
61
+ # .eager_load_one(:category, {:category_id => :id}, "
62
62
  # SELECT * FROM categories WHERE id IN (%{category_ids}) AND name != %{bad_name}
63
63
  # ", binds: {
64
64
  # bad_name: "Bad Category"
65
- # }).
66
- # run
65
+ # })
66
+ # .run
67
67
  #
68
68
  # @param name [Symbol] name of attribute to load records into
69
69
  # @param mapping [Hash] a Hash that defines the key mapping of the parent (widgets.category_id) to the child (categories.id).
@@ -89,14 +89,14 @@ module OccamsRecord
89
89
  # The %{ids} bind param will be provided for you, and in this case will be all the id values from the Widget
90
90
  # query.
91
91
  #
92
- # res = OccamsRecord.
93
- # query(Widget.order("name")).
94
- # eager_load_many(:parts, {:id => :widget_id}, "
92
+ # res = OccamsRecord
93
+ # .query(Widget.order("name"))
94
+ # .eager_load_many(:parts, {:id => :widget_id}, "
95
95
  # SELECT * FROM parts WHERE widget_id IN (%{ids}) AND sku NOT IN (%{bad_skus})
96
96
  # ", binds: {
97
97
  # bad_skus: ["G90023ASDf0"]
98
- # }).
99
- # run
98
+ # })
99
+ # .run
100
100
  #
101
101
  # @param name [Symbol] name of attribute to load records into
102
102
  # @param mapping [Hash] a Hash that defines the key mapping of the parent (widgets.id) to the children (parts.widget_id).
@@ -9,7 +9,7 @@ module OccamsRecord
9
9
  #
10
10
  # See documentation for OccamsRecord::EagerLoaders::Base.
11
11
  #
12
- def initialize(*args)
12
+ def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, &builder)
13
13
  super
14
14
 
15
15
  unless @ref.macro == :has_one or @ref.macro == :has_many
@@ -5,18 +5,18 @@ module OccamsRecord
5
5
  #
6
6
  # Measure the time each query takes. Useful for figuring out which query is the slow one when you're doing a bunch of eager loads.
7
7
  #
8
- # orders = OccamsRecord.
9
- # query(Order.all).
10
- # eager_load(:customer).
8
+ # orders = OccamsRecord
9
+ # .query(Order.all)
10
+ # .eager_load(:customer)
11
11
  # ...
12
- # measure { |x|
12
+ # .measure { |x|
13
13
  # puts "Total time: #{x.total_time} sec"
14
14
  # x.queries.each { |q|
15
15
  # puts "Table: #{q.table_name} (#{q.time} sec)"
16
16
  # puts q.sql
17
17
  # }
18
- # }.
19
- # run
18
+ # }
19
+ # .run
20
20
  #
21
21
  module Measureable
22
22
  #
@@ -1,5 +1,3 @@
1
- require 'occams-record/batches'
2
-
3
1
  module OccamsRecord
4
2
  #
5
3
  # Starts building a OccamsRecord::Query. Pass it a scope from any of ActiveRecord's query builder
@@ -7,15 +5,15 @@ module OccamsRecord
7
5
  # Instead use OccamsRecord::Query#eager_load. Finally, call `run` (or any Enumerable method) to run
8
6
  # the query and get back an array of objects.
9
7
  #
10
- # results = OccamsRecord.
11
- # query(Widget.order("name")).
12
- # eager_load(:category).
13
- # eager_load(:order_items, ->(q) { q.select("widget_id, order_id") }) {
8
+ # results = OccamsRecord
9
+ # .query(Widget.order("name"))
10
+ # .eager_load(:category)
11
+ # .eager_load(:order_items, ->(q) { q.select("widget_id, order_id") }) {
14
12
  # eager_load(:orders) {
15
13
  # eager_load(:customer, ->(q) { q.select("name") })
16
14
  # }
17
- # }.
18
- # run
15
+ # }
16
+ # .run
19
17
  #
20
18
  # @param scope [ActiveRecord::Relation]
21
19
  # @param use [Module] optional Module to include in the result class
@@ -36,7 +34,7 @@ module OccamsRecord
36
34
  # @return [ActiveRecord::Relation] scope for building the main SQL query
37
35
  attr_reader :scope
38
36
 
39
- include Batches
37
+ include OccamsRecord::Batches::CursorHelpers
40
38
  include EagerLoaders::Builder
41
39
  include Enumerable
42
40
  include Measureable
@@ -154,5 +152,75 @@ module OccamsRecord
154
152
  to_a.each
155
153
  end
156
154
  end
155
+
156
+ #
157
+ # Load records in batches of N and yield each record to a block if given. If no block is given,
158
+ # returns an Enumerator.
159
+ #
160
+ # NOTE Unlike ActiveRecord's find_each, ORDER BY is respected. The primary key will be appended
161
+ # to the ORDER BY clause to help ensure consistent batches. Additionally, it will be run inside
162
+ # of a transaction.
163
+ #
164
+ # @param batch_size [Integer]
165
+ # @param use_transaction [Boolean] Ensure it runs inside of a database transaction
166
+ # @param append_order_by [String] Append this column to ORDER BY to ensure consistent results. Defaults to the primary key. Pass false to disable.
167
+ # @yield [OccamsRecord::Results::Row]
168
+ # @return [Enumerator] will yield each record
169
+ #
170
+ def find_each(batch_size: 1000, use_transaction: true, append_order_by: nil)
171
+ enum = Enumerator.new { |y|
172
+ find_in_batches(batch_size: 1000, use_transaction: use_transaction, append_order_by: append_order_by).each { |batch|
173
+ batch.each { |record| y.yield record }
174
+ }
175
+ }
176
+ if block_given?
177
+ enum.each { |record| yield record }
178
+ else
179
+ enum
180
+ end
181
+ end
182
+
183
+ #
184
+ # Load records in batches of N and yield each batch to a block if given.
185
+ # If no block is given, returns an Enumerator.
186
+ #
187
+ # NOTE Unlike ActiveRecord's find_each, ORDER BY is respected. The primary key will be appended
188
+ # to the ORDER BY clause to help ensure consistent batches. Additionally, it will be run inside
189
+ # of a transaction.
190
+ #
191
+ # @param batch_size [Integer]
192
+ # @param use_transaction [Boolean] Ensure it runs inside of a database transaction
193
+ # @param append_order_by [String] Append this column to ORDER BY to ensure consistent results. Defaults to the primary key. Pass false to disable.
194
+ # @yield [OccamsRecord::Results::Row]
195
+ # @return [Enumerator] will yield each batch
196
+ #
197
+ def find_in_batches(batch_size: 1000, use_transaction: true, append_order_by: nil)
198
+ enum = Batches::OffsetLimit::Scoped
199
+ .new(model, scope, use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders)
200
+ .enum(batch_size: batch_size, use_transaction: use_transaction, append_order_by: append_order_by)
201
+ if block_given?
202
+ enum.each { |batch| yield batch }
203
+ else
204
+ enum
205
+ end
206
+ end
207
+
208
+ #
209
+ # Returns a cursor you can open and perform operations on. A lower-level alternative to
210
+ # find_each_with_cursor and find_in_batches_with_cursor.
211
+ #
212
+ # NOTE Postgres only. See the docs for OccamsRecord::Cursor for more details.
213
+ #
214
+ # @param name [String] Specify a name for the cursor (defaults to a random name)
215
+ # @param scroll [Boolean] true = SCROLL, false = NO SCROLL, nil = default behavior of DB
216
+ # @param hold [Boolean] true = WITH HOLD, false = WITHOUT HOLD, nil = default behavior of DB
217
+ # @return [OccamsRecord::Cursor]
218
+ #
219
+ def cursor(name: nil, scroll: nil, hold: nil)
220
+ Cursor.new(model.connection, scope.to_sql,
221
+ name: name, scroll: scroll, hold: hold,
222
+ use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders,
223
+ )
224
+ end
157
225
  end
158
226
  end
@@ -14,16 +14,16 @@ module OccamsRecord
14
14
  # If you want to do eager loading, you must first the define a model to pull the associations from (unless
15
15
  # you're using the raw SQL eager loaders `eager_load_one` or `eager_load_many`).
16
16
  #
17
- # results = OccamsRecord.
18
- # sql("
17
+ # results = OccamsRecord
18
+ # .sql("
19
19
  # SELECT * FROM widgets
20
20
  # WHERE category_id IN (%{cat_ids})
21
21
  # ", {
22
22
  # cat_ids: [5, 10]
23
- # }).
24
- # model(Widget).
25
- # eager_load(:category).
26
- # run
23
+ # })
24
+ # .model(Widget)
25
+ # .eager_load(:category)
26
+ # .run
27
27
  #
28
28
  # NOTE To use find_each/find_in_batches, your SQL string must include 'LIMIT %{batch_limit} OFFSET %{batch_offset}',
29
29
  # and an ORDER BY is strongly recomended. OccamsRecord will provide the bind values for you.
@@ -61,7 +61,7 @@ module OccamsRecord
61
61
  # @return [Hash]
62
62
  attr_reader :binds
63
63
 
64
- include Batches
64
+ include OccamsRecord::Batches::CursorHelpers
65
65
  include EagerLoaders::Builder
66
66
  include Enumerable
67
67
  include Measureable
@@ -75,13 +75,15 @@ module OccamsRecord
75
75
  # @param eager_loaders [OccamsRecord::EagerLoaders::Context]
76
76
  # @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
77
77
  # @param measurements [Array]
78
+ # @param connection
78
79
  #
79
- def initialize(sql, binds, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil)
80
+ def initialize(sql, binds, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil, connection: nil)
80
81
  @sql = sql
81
82
  @binds = binds
82
83
  @use = use
83
84
  @eager_loaders = eager_loaders || EagerLoaders::Context.new
84
85
  @query_logger, @measurements = query_logger, measurements
86
+ @conn = connection
85
87
  end
86
88
 
87
89
  #
@@ -139,6 +141,74 @@ module OccamsRecord
139
141
  end
140
142
  end
141
143
 
144
+ #
145
+ # Load records in batches of N and yield each record to a block if given. If no block is given,
146
+ # returns an Enumerator.
147
+ #
148
+ # NOTE Unlike ActiveRecord's find_each, ORDER BY is respected. The primary key will be appended
149
+ # to the ORDER BY clause to help ensure consistent batches. Additionally, it will be run inside
150
+ # of a transaction.
151
+ #
152
+ # @param batch_size [Integer]
153
+ # @param use_transaction [Boolean] Ensure it runs inside of a database transaction
154
+ # @yield [OccamsRecord::Results::Row]
155
+ # @return [Enumerator] will yield each record
156
+ #
157
+ def find_each(batch_size: 1000, use_transaction: true)
158
+ enum = Enumerator.new { |y|
159
+ find_in_batches(batch_size: batch_size, use_transaction: use_transaction).each { |batch|
160
+ batch.each { |record| y.yield record }
161
+ }
162
+ }
163
+ if block_given?
164
+ enum.each { |record| yield record }
165
+ else
166
+ enum
167
+ end
168
+ end
169
+
170
+ #
171
+ # Load records in batches of N and yield each batch to a block if given.
172
+ # If no block is given, returns an Enumerator.
173
+ #
174
+ # NOTE Unlike ActiveRecord's find_each, ORDER BY is respected. The primary key will be appended
175
+ # to the ORDER BY clause to help ensure consistent batches. Additionally, it will be run inside
176
+ # of a transaction.
177
+ #
178
+ # @param batch_size [Integer]
179
+ # @param use_transaction [Boolean] Ensure it runs inside of a database transaction
180
+ # @yield [OccamsRecord::Results::Row]
181
+ # @return [Enumerator] will yield each batch
182
+ #
183
+ def find_in_batches(batch_size: 1000, use_transaction: true)
184
+ enum = Batches::OffsetLimit::RawQuery
185
+ .new(conn, @sql, @binds, use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders)
186
+ .enum(batch_size: batch_size, use_transaction: use_transaction)
187
+ if block_given?
188
+ enum.each { |batch| yield batch }
189
+ else
190
+ enum
191
+ end
192
+ end
193
+
194
+ #
195
+ # Returns a cursor you can open and perform operations on. A lower-level alternative to
196
+ # find_each_with_cursor and find_in_batches_with_cursor.
197
+ #
198
+ # NOTE Postgres only. See the docs for OccamsRecord::Cursor for more details.
199
+ #
200
+ # @param name [String] Specify a name for the cursor (defaults to a random name)
201
+ # @param scroll [Boolean] true = SCROLL, false = NO SCROLL, nil = default behavior of DB
202
+ # @param hold [Boolean] true = WITH HOLD, false = WITHOUT HOLD, nil = default behavior of DB
203
+ # @return [OccamsRecord::Cursor]
204
+ #
205
+ def cursor(name: nil, scroll: nil, hold: nil)
206
+ Cursor.new(conn, @sql,
207
+ name: name, scroll: scroll, hold: hold,
208
+ use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders,
209
+ )
210
+ end
211
+
142
212
  private
143
213
 
144
214
  # Returns the SQL as a String with all variables escaped
@@ -158,45 +228,6 @@ module OccamsRecord
158
228
  @sql.match(/\s+FROM\s+"?(\w+)"?/i)&.captures&.first
159
229
  end
160
230
 
161
- #
162
- # Returns an Enumerator that yields batches of records, of size "of".
163
- # The SQL string must include 'LIMIT %{batch_limit} OFFSET %{batch_offset}'.
164
- # The bind values will be provided by OccamsRecord.
165
- #
166
- # @param of [Integer] batch size
167
- # @param use_transaction [Boolean] Ensure it runs inside of a database transaction
168
- # @return [Enumerator] yields batches
169
- #
170
- def batches(of:, use_transaction: true, append_order_by: nil)
171
- unless @sql =~ /LIMIT\s+%\{batch_limit\}/i and @sql =~ /OFFSET\s+%\{batch_offset\}/i
172
- raise ArgumentError, "When using find_each/find_in_batches you must specify 'LIMIT %{batch_limit} OFFSET %{batch_offset}'. SQL statement: #{@sql}"
173
- end
174
-
175
- Enumerator.new do |y|
176
- if use_transaction and conn.open_transactions == 0
177
- conn.transaction {
178
- run_batches y, of
179
- }
180
- else
181
- run_batches y, of
182
- end
183
- end
184
- end
185
-
186
- def run_batches(y, of)
187
- offset = 0
188
- loop do
189
- results = RawQuery.new(@sql, @binds.merge({
190
- batch_limit: of,
191
- batch_offset: offset,
192
- }), use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders).run
193
-
194
- y.yield results if results.any?
195
- break if results.size < of
196
- offset += results.size
197
- end
198
- end
199
-
200
231
  def conn
201
232
  @conn ||= @eager_loaders.model&.connection || ActiveRecord::Base.connection
202
233
  end
@@ -4,7 +4,7 @@ module OccamsRecord
4
4
  # ActiveRecord's internal type casting API changes from version to version.
5
5
  CASTER = case ActiveRecord::VERSION::MAJOR
6
6
  when 4 then :type_cast_from_database
7
- when 5, 6 then :deserialize
7
+ when 5, 6, 7 then :deserialize
8
8
  else raise "OccamsRecord::Results::CASTER does yet support this version of ActiveRecord"
9
9
  end
10
10
 
@@ -8,10 +8,10 @@ module OccamsRecord
8
8
  # The ONLY reason to use this is if you absolutely need ActiveRecord objects but still want to use Occams's
9
9
  # more advanced eager loading or find_each/find_in_batches features.
10
10
  #
11
- # OccamsRecord.
12
- # query(Order.order("created_at DESC")).
13
- # eager_load(:line_items, ->(q) { q.order("price") }).
14
- # find_each do |o|
11
+ # OccamsRecord
12
+ # .query(Order.order("created_at DESC"))
13
+ # .eager_load(:line_items, ->(q) { q.order("price") })
14
+ # .find_each do |o|
15
15
  # order = OccamsRecord::Ugly.active_record(o)
16
16
  # ...
17
17
  # end
@@ -2,6 +2,6 @@
2
2
  # Main entry point for using OccamsRecord.
3
3
  #
4
4
  module OccamsRecord
5
- # Library version
6
- VERSION = "1.2.1".freeze
5
+ # @private
6
+ VERSION = "1.4.0-beta1".freeze
7
7
  end
data/lib/occams-record.rb CHANGED
@@ -5,9 +5,15 @@ require 'occams-record/measureable'
5
5
  require 'occams-record/eager_loaders/eager_loaders'
6
6
  require 'occams-record/results/results'
7
7
  require 'occams-record/results/row'
8
+ require 'occams-record/cursor'
9
+ require 'occams-record/errors'
10
+
11
+ require 'occams-record/batches/offset_limit/scoped'
12
+ require 'occams-record/batches/offset_limit/raw_query'
13
+ require 'occams-record/batches/cursor_helpers'
14
+
8
15
  require 'occams-record/query'
9
16
  require 'occams-record/raw_query'
10
- require 'occams-record/errors'
11
17
 
12
18
  module OccamsRecord
13
19
  autoload :Ugly, 'occams-record/ugly'
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.2.1
4
+ version: 1.4.0.pre.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Hollinger
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-04 00:00:00.000000000 Z
11
+ date: 2022-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: '4.2'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6.2'
22
+ version: '7.1'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: '4.2'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6.2'
32
+ version: '7.1'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: appraisal
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -44,8 +44,8 @@ dependencies:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
46
  version: '0'
47
- description: A faster, lower-memory querying API for ActiveRecord that returns results
48
- as unadorned, read-only objects.
47
+ description: A faster, lower-memory, fuller-featured querying API for ActiveRecord
48
+ that returns results as unadorned, read-only objects.
49
49
  email: jordan.hollinger@gmail.com
50
50
  executables: []
51
51
  extensions: []
@@ -53,8 +53,10 @@ extra_rdoc_files: []
53
53
  files:
54
54
  - README.md
55
55
  - lib/occams-record.rb
56
- - lib/occams-record/batches.rb
57
- - lib/occams-record/connection.rb
56
+ - lib/occams-record/batches/cursor_helpers.rb
57
+ - lib/occams-record/batches/offset_limit/raw_query.rb
58
+ - lib/occams-record/batches/offset_limit/scoped.rb
59
+ - lib/occams-record/cursor.rb
58
60
  - lib/occams-record/eager_loaders/ad_hoc_base.rb
59
61
  - lib/occams-record/eager_loaders/ad_hoc_many.rb
60
62
  - lib/occams-record/eager_loaders/ad_hoc_one.rb
@@ -81,7 +83,7 @@ homepage: https://jhollinger.github.io/occams-record/
81
83
  licenses:
82
84
  - MIT
83
85
  metadata: {}
84
- post_install_message:
86
+ post_install_message:
85
87
  rdoc_options: []
86
88
  require_paths:
87
89
  - lib
@@ -92,12 +94,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
92
94
  version: 2.3.0
93
95
  required_rubygems_version: !ruby/object:Gem::Requirement
94
96
  requirements:
95
- - - ">="
97
+ - - ">"
96
98
  - !ruby/object:Gem::Version
97
- version: '0'
99
+ version: 1.3.1
98
100
  requirements: []
99
- rubygems_version: 3.0.3
100
- signing_key:
101
+ rubygems_version: 3.1.6
102
+ signing_key:
101
103
  specification_version: 4
102
104
  summary: The missing high-efficiency query API for ActiveRecord
103
105
  test_files: []
@@ -1,113 +0,0 @@
1
- module OccamsRecord
2
- #
3
- # Methods for building batch finding methods. It expects "model" and "scope" methods to be present.
4
- #
5
- module Batches
6
- #
7
- # Load records in batches of N and yield each record to a block if given. If no block is given,
8
- # returns an Enumerator.
9
- #
10
- # NOTE Unlike ActiveRecord's find_each, ORDER BY is respected. The primary key will be appended
11
- # to the ORDER BY clause to help ensure consistent batches. Additionally, it will be run inside
12
- # of a transaction.
13
- #
14
- # @param batch_size [Integer]
15
- # @param use_transaction [Boolean] Ensure it runs inside of a database transaction
16
- # @param append_order_by [String] Append this column to ORDER BY to ensure consistent results. Defaults to the primary key. Pass false to disable.
17
- # @yield [OccamsRecord::Results::Row]
18
- # @return [Enumerator] will yield each record
19
- #
20
- def find_each(batch_size: 1000, use_transaction: true, append_order_by: nil)
21
- enum = Enumerator.new { |y|
22
- batches(of: batch_size, use_transaction: use_transaction, append_order_by: append_order_by).each { |batch|
23
- batch.each { |record| y.yield record }
24
- }
25
- }
26
- if block_given?
27
- enum.each { |record| yield record }
28
- else
29
- enum
30
- end
31
- end
32
-
33
- #
34
- # Load records in batches of N and yield each batch to a block if given.
35
- # If no block is given, returns an Enumerator.
36
- #
37
- # NOTE Unlike ActiveRecord's find_each, ORDER BY is respected. The primary key will be appended
38
- # to the ORDER BY clause to help ensure consistent batches. Additionally, it will be run inside
39
- # of a transaction.
40
- #
41
- # @param batch_size [Integer]
42
- # @param use_transaction [Boolean] Ensure it runs inside of a database transaction
43
- # @param append_order_by [String] Append this column to ORDER BY to ensure consistent results. Defaults to the primary key. Pass false to disable.
44
- # @yield [OccamsRecord::Results::Row]
45
- # @return [Enumerator] will yield each batch
46
- #
47
- def find_in_batches(batch_size: 1000, use_transaction: true, append_order_by: nil)
48
- enum = batches(of: batch_size, use_transaction: use_transaction, append_order_by: append_order_by)
49
- if block_given?
50
- enum.each { |batch| yield batch }
51
- else
52
- enum
53
- end
54
- end
55
-
56
- private
57
-
58
- #
59
- # Returns an Enumerator that yields batches of records, of size "of".
60
- # NOTE ActiveRecord 5+ provides the 'in_batches' method to do something
61
- # similiar, although 4.2 does not. Also it does not respect ORDER BY,
62
- # whereas this does.
63
- #
64
- # @param of [Integer] batch size
65
- # @param use_transaction [Boolean] Ensure it runs inside of a database transaction
66
- # @param append_order_by [String] Append this column to ORDER BY to ensure consistent results. Defaults to the primary key. Pass false to disable.
67
- # @return [Enumerator] yields batches
68
- #
69
- def batches(of:, use_transaction: true, append_order_by: nil)
70
- append_order =
71
- case append_order_by
72
- when false then nil
73
- when nil then model.primary_key
74
- else append_order_by
75
- end
76
-
77
- Enumerator.new do |y|
78
- if use_transaction and model.connection.open_transactions == 0
79
- model.connection.transaction {
80
- run_batches y, of, append_order
81
- }
82
- else
83
- run_batches y, of, append_order
84
- end
85
- end
86
- end
87
-
88
- def run_batches(y, of, append_order_by = nil)
89
- limit = scope.limit_value
90
- batch_size = limit && limit < of ? limit : of
91
-
92
- offset = scope.offset_value || 0
93
- out_of_records, count = false, 0
94
- order_by =
95
- if append_order_by
96
- append_order_by.to_s == model.primary_key.to_s ? append_order_by.to_sym : append_order_by
97
- end
98
-
99
- until out_of_records
100
- l = limit && batch_size > limit - count ? limit - count : batch_size
101
- q = scope
102
- q = q.order(order_by) if order_by
103
- q = q.offset(offset).limit(l)
104
- results = Query.new(q, use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders).run
105
-
106
- y.yield results if results.any?
107
- count += results.size
108
- offset += results.size
109
- out_of_records = results.size < batch_size || (limit && count >= limit)
110
- end
111
- end
112
- end
113
- end
@@ -1,7 +0,0 @@
1
- module OccamsRecord
2
- class Connection
3
- def initialize(model, role = nil)
4
- @model, @role = model, role
5
- end
6
- end
7
- end