occams-record 1.1.4 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +17 -9
- data/lib/occams-record/batches.rb +28 -11
- data/lib/occams-record/connection.rb +7 -0
- data/lib/occams-record/eager_loaders/belongs_to.rb +2 -1
- data/lib/occams-record/eager_loaders/has_one.rb +1 -0
- data/lib/occams-record/eager_loaders/polymorphic_belongs_to.rb +1 -0
- data/lib/occams-record/raw_query.rb +16 -5
- data/lib/occams-record/results/results.rb +13 -7
- data/lib/occams-record/results/row.rb +1 -1
- data/lib/occams-record/version.rb +1 -1
- metadata +20 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a1e01657287159a2aa1a27ff66c1932ae83235ad0140dbaafd58103862ddaa8f
|
4
|
+
data.tar.gz: ec9530c3844cd79739d1a2387031654e85f4a2a127d948df0c537431aa235ed5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a0303147fa894e42d0642020a4a207c67b3a79fa3fc2dfa1b9be819224c8eee6bb261bfa2079a073cd1894f744b4995f307afff594f458c5b7ae7574b26b0d32
|
7
|
+
data.tar.gz: bf81dc3eeb78cbf9ed99740a1c4c8e1c71287f4af278c2a623d9220384460f952ccc50fa835393ef3fd416fa0019fccc6a6e41d0c8dbd77c637f08689ca5c585
|
data/README.md
CHANGED
@@ -12,14 +12,14 @@ OccamsRecord is a high-efficiency, advanced query library for use alongside Acti
|
|
12
12
|
|
13
13
|
### 2) Supercharged querying & eager loading
|
14
14
|
|
15
|
-
Continue using ActiveRecord's query builder, but let Occams take over eager loading and raw SQL calls. None of the examples below are possible with ActiveRecord, but OccamsRecord
|
15
|
+
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.)
|
16
16
|
|
17
17
|
**Customize the SQL used to eager load associations**
|
18
18
|
|
19
19
|
```ruby
|
20
20
|
OccamsRecord.
|
21
21
|
query(User.active).
|
22
|
-
eager_load(:orders, ->(q) { q.where("created_at >= ?", date })
|
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`**
|
@@ -97,6 +97,8 @@ gem 'occams-record'
|
|
97
97
|
|
98
98
|
Full documentation is available at [rubydoc.info/gems/occams-record](http://www.rubydoc.info/gems/occams-record).
|
99
99
|
|
100
|
+
Code lives at at [github.com/jhollinger/occams-record](https://github.com/jhollinger/occams-record). Contributions welcome!
|
101
|
+
|
100
102
|
Build your queries like normal, using ActiveRecord's excellent query builder. Then pass them off to Occams Record.
|
101
103
|
|
102
104
|
```ruby
|
@@ -290,21 +292,27 @@ On the other hand, Active Record makes it *very* easy to forget to eager load as
|
|
290
292
|
|
291
293
|
# Testing
|
292
294
|
|
293
|
-
To run the tests, simply run:
|
294
|
-
|
295
295
|
```bash
|
296
296
|
bundle install
|
297
|
+
|
298
|
+
# test against SQLite
|
297
299
|
bundle exec rake test
|
300
|
+
|
301
|
+
# test against Postgres
|
302
|
+
TEST_DATABASE_URL=postgres://postgres@localhost:5432/occams_record bundle exec rake test
|
298
303
|
```
|
299
304
|
|
300
|
-
|
305
|
+
**Test against all supported ActiveRecord versions**
|
301
306
|
|
302
307
|
```bash
|
303
|
-
|
304
|
-
bundle exec rake test
|
305
|
-
```
|
308
|
+
bundle exec appraisal install
|
306
309
|
|
307
|
-
|
310
|
+
# test against SQLite
|
311
|
+
bundle exec appraisal rake test
|
312
|
+
|
313
|
+
# test against Postgres
|
314
|
+
TEST_DATABASE_URL=postgres://postgres@localhost:5432/occams_record bundle exec appraisal rake test
|
315
|
+
```
|
308
316
|
|
309
317
|
# License
|
310
318
|
|
@@ -7,17 +7,19 @@ module OccamsRecord
|
|
7
7
|
# Load records in batches of N and yield each record to a block if given. If no block is given,
|
8
8
|
# returns an Enumerator.
|
9
9
|
#
|
10
|
-
# NOTE Unlike ActiveRecord's find_each, ORDER BY is respected.
|
11
|
-
#
|
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.
|
12
13
|
#
|
13
14
|
# @param batch_size [Integer]
|
14
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.
|
15
17
|
# @yield [OccamsRecord::Results::Row]
|
16
18
|
# @return [Enumerator] will yield each record
|
17
19
|
#
|
18
|
-
def find_each(batch_size: 1000, use_transaction: true)
|
20
|
+
def find_each(batch_size: 1000, use_transaction: true, append_order_by: nil)
|
19
21
|
enum = Enumerator.new { |y|
|
20
|
-
batches(of: batch_size, use_transaction: use_transaction).each { |batch|
|
22
|
+
batches(of: batch_size, use_transaction: use_transaction, append_order_by: append_order_by).each { |batch|
|
21
23
|
batch.each { |record| y.yield record }
|
22
24
|
}
|
23
25
|
}
|
@@ -38,11 +40,12 @@ module OccamsRecord
|
|
38
40
|
#
|
39
41
|
# @param batch_size [Integer]
|
40
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.
|
41
44
|
# @yield [OccamsRecord::Results::Row]
|
42
45
|
# @return [Enumerator] will yield each batch
|
43
46
|
#
|
44
|
-
def find_in_batches(batch_size: 1000, use_transaction: true)
|
45
|
-
enum = batches(of: batch_size, use_transaction: use_transaction)
|
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)
|
46
49
|
if block_given?
|
47
50
|
enum.each { |batch| yield batch }
|
48
51
|
else
|
@@ -60,30 +63,44 @@ module OccamsRecord
|
|
60
63
|
#
|
61
64
|
# @param of [Integer] batch size
|
62
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.
|
63
67
|
# @return [Enumerator] yields batches
|
64
68
|
#
|
65
|
-
def batches(of:, use_transaction: true)
|
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
|
+
|
66
77
|
Enumerator.new do |y|
|
67
78
|
if use_transaction and model.connection.open_transactions == 0
|
68
79
|
model.connection.transaction {
|
69
|
-
run_batches y, of
|
80
|
+
run_batches y, of, append_order
|
70
81
|
}
|
71
82
|
else
|
72
|
-
run_batches y, of
|
83
|
+
run_batches y, of, append_order
|
73
84
|
end
|
74
85
|
end
|
75
86
|
end
|
76
87
|
|
77
|
-
def run_batches(y, of)
|
88
|
+
def run_batches(y, of, append_order_by = nil)
|
78
89
|
limit = scope.limit_value
|
79
90
|
batch_size = limit && limit < of ? limit : of
|
80
91
|
|
81
92
|
offset = scope.offset_value || 0
|
82
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
|
83
98
|
|
84
99
|
until out_of_records
|
85
100
|
l = limit && batch_size > limit - count ? limit - count : batch_size
|
86
|
-
q = scope
|
101
|
+
q = scope
|
102
|
+
q = q.order(order_by) if order_by
|
103
|
+
q = q.offset(offset).limit(l)
|
87
104
|
results = Query.new(q, use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders).run
|
88
105
|
|
89
106
|
y.yield results if results.any?
|
@@ -20,6 +20,7 @@ module OccamsRecord
|
|
20
20
|
raise MissingColumnError.new(row, e.name)
|
21
21
|
end
|
22
22
|
}.compact.uniq
|
23
|
+
ids.sort! if $occams_record_test
|
23
24
|
|
24
25
|
q = base_scope.where(@ref.association_primary_key => ids)
|
25
26
|
yield q if ids.any?
|
@@ -33,7 +34,7 @@ module OccamsRecord
|
|
33
34
|
#
|
34
35
|
def merge!(assoc_rows, rows)
|
35
36
|
Merge.new(rows, name).
|
36
|
-
single!(assoc_rows, {@ref.foreign_key.to_s => @ref.
|
37
|
+
single!(assoc_rows, {@ref.foreign_key.to_s => @ref.association_primary_key.to_s})
|
37
38
|
end
|
38
39
|
end
|
39
40
|
end
|
@@ -55,6 +55,7 @@ module OccamsRecord
|
|
55
55
|
next if type.nil? or type == ""
|
56
56
|
model = type.constantize
|
57
57
|
ids = rows_of_type.map(&@foreign_key).uniq
|
58
|
+
ids.sort! if $occams_record_test
|
58
59
|
q = base_scope(model).where(@ref.active_record_primary_key => ids)
|
59
60
|
yield q if ids.any?
|
60
61
|
end
|
@@ -4,7 +4,7 @@ module OccamsRecord
|
|
4
4
|
# a Hash of binds. While this doesn't offer an additional performance boost, it's a nice way to
|
5
5
|
# write safe, complicated SQL by hand while also supporting eager loading.
|
6
6
|
#
|
7
|
-
# results = OccamsRecord.sql(
|
7
|
+
# results = OccamsRecord.sql("
|
8
8
|
# SELECT * FROM widgets
|
9
9
|
# WHERE category_id = %{cat_id}
|
10
10
|
# ", {
|
@@ -13,7 +13,6 @@ module OccamsRecord
|
|
13
13
|
#
|
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
|
-
# NOTE If you're using SQLite, you must *always* specify the model.
|
17
16
|
#
|
18
17
|
# results = OccamsRecord.
|
19
18
|
# sql("
|
@@ -27,8 +26,20 @@ module OccamsRecord
|
|
27
26
|
# run
|
28
27
|
#
|
29
28
|
# NOTE To use find_each/find_in_batches, your SQL string must include 'LIMIT %{batch_limit} OFFSET %{batch_offset}',
|
30
|
-
# and an ORDER BY is strongly recomended.
|
31
|
-
#
|
29
|
+
# and an ORDER BY is strongly recomended. OccamsRecord will provide the bind values for you.
|
30
|
+
#
|
31
|
+
# NOTE There is variation of the types of values returned (e.g. a Date object vs a date string) depending on the database
|
32
|
+
# and ActiveRecord version being used:
|
33
|
+
#
|
34
|
+
# Postgres always returns native Ruby types.
|
35
|
+
#
|
36
|
+
# SQLite will return native types for the following: integers, floats, string/text.
|
37
|
+
# For booleans it will return 0|1 for AR 6+, and "t|f" for AR 5-.
|
38
|
+
# Dates and times will be ISO8601 formatted strings.
|
39
|
+
# It is possible to coerce the SQLite adapter into returning native types for everything IF they're columns of a table
|
40
|
+
# that you have an AR model for. e.g. if you're selecting from the widgets, table: `OccamsRecord.sql("...").model(Widget)...`.
|
41
|
+
#
|
42
|
+
# MySQL ?
|
32
43
|
#
|
33
44
|
# @param sql [String] The SELECT statement to run. Binds should use Ruby's named string substitution.
|
34
45
|
# @param binds [Hash] Bind values (Symbol keys)
|
@@ -156,7 +167,7 @@ module OccamsRecord
|
|
156
167
|
# @param use_transaction [Boolean] Ensure it runs inside of a database transaction
|
157
168
|
# @return [Enumerator] yields batches
|
158
169
|
#
|
159
|
-
def batches(of:, use_transaction: true)
|
170
|
+
def batches(of:, use_transaction: true, append_order_by: nil)
|
160
171
|
unless @sql =~ /LIMIT\s+%\{batch_limit\}/i and @sql =~ /OFFSET\s+%\{batch_offset\}/i
|
161
172
|
raise ArgumentError, "When using find_each/find_in_batches you must specify 'LIMIT %{batch_limit} OFFSET %{batch_offset}'. SQL statement: #{@sql}"
|
162
173
|
end
|
@@ -36,14 +36,20 @@ module OccamsRecord
|
|
36
36
|
attr_accessor(*association_names)
|
37
37
|
|
38
38
|
# Build a getter for each attribute returned by the query. The values will be type converted on demand.
|
39
|
-
model_column_types = model ? model.attributes_builder.types :
|
39
|
+
model_column_types = model ? model.attributes_builder.types : nil
|
40
40
|
self.columns.each_with_index do |col, idx|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
41
|
+
#
|
42
|
+
# NOTE there's lots of variation between DB adapters and AR versions here. Some notes:
|
43
|
+
# * Postgres AR < 6.1 `column_types` will contain entries for every column.
|
44
|
+
# * Postgres AR >= 6.1 `column_types` only contains entries for "exotic" types. Columns with "common" types have already been converted by the PG adapter.
|
45
|
+
# * SQLite `column_types` will always be empty. Some types will have already been convered by the SQLite adapter, but others will depend on
|
46
|
+
# `model_column_types` for converstion. See test/raw_query_test.rb#test_common_types for examples.
|
47
|
+
# * MySQL ?
|
48
|
+
#
|
49
|
+
type = column_types[col] || model_column_types&.[](col)
|
50
|
+
case type&.type
|
51
|
+
when nil
|
52
|
+
define_method(col) { @raw_values[idx] }
|
47
53
|
when :datetime
|
48
54
|
define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx])&.in_time_zone }
|
49
55
|
when :boolean
|
@@ -152,7 +152,7 @@ module OccamsRecord
|
|
152
152
|
def define_ids_reader!(assoc)
|
153
153
|
model = self.class._model
|
154
154
|
ref = model.reflections[assoc]
|
155
|
-
pkey = ref.
|
155
|
+
pkey = ref.klass.primary_key.to_sym
|
156
156
|
|
157
157
|
self.class.class_eval do
|
158
158
|
define_method "#{assoc.singularize}_ids" do
|
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.1
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jordan Hollinger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-07-04 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.
|
22
|
+
version: '6.2'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -29,7 +29,21 @@ dependencies:
|
|
29
29
|
version: '4.2'
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '6.
|
32
|
+
version: '6.2'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: appraisal
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
33
47
|
description: A faster, lower-memory querying API for ActiveRecord that returns results
|
34
48
|
as unadorned, read-only objects.
|
35
49
|
email: jordan.hollinger@gmail.com
|
@@ -40,6 +54,7 @@ files:
|
|
40
54
|
- README.md
|
41
55
|
- lib/occams-record.rb
|
42
56
|
- lib/occams-record/batches.rb
|
57
|
+
- lib/occams-record/connection.rb
|
43
58
|
- lib/occams-record/eager_loaders/ad_hoc_base.rb
|
44
59
|
- lib/occams-record/eager_loaders/ad_hoc_many.rb
|
45
60
|
- lib/occams-record/eager_loaders/ad_hoc_one.rb
|
@@ -81,8 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
96
|
- !ruby/object:Gem::Version
|
82
97
|
version: '0'
|
83
98
|
requirements: []
|
84
|
-
|
85
|
-
rubygems_version: 2.7.6.2
|
99
|
+
rubygems_version: 3.0.3
|
86
100
|
signing_key:
|
87
101
|
specification_version: 4
|
88
102
|
summary: The missing high-efficiency query API for ActiveRecord
|