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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c5eba9d662af4ceea78d6d50d9649dc88faebb18ab8f2d9e0704f4fe5367985
4
- data.tar.gz: cd5aa76eeb6a5f89ed65bd10549e468173dc2f8aa896346925b4b3752cf7858e
3
+ metadata.gz: a1e01657287159a2aa1a27ff66c1932ae83235ad0140dbaafd58103862ddaa8f
4
+ data.tar.gz: ec9530c3844cd79739d1a2387031654e85f4a2a127d948df0c537431aa235ed5
5
5
  SHA512:
6
- metadata.gz: d48d4389624f627302877ca6bcda1e7b1dc19facd4b54b4d255bb11b75d1f79149c1e79aff271b715eca0f43e03819e5aaaebc2897a7b0b29696eb20e0bcc4fc
7
- data.tar.gz: ab8fdfddb10f751aa74274efc6229e5c24d32ec968fe8c88f30fd9af760652d9a75a2cc2bc17c29e3afcdd41b2d6f78e7cda60d485b6844458267beda2839aa2
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 won't limit you. (More complete examples are shown later, but these should whet your appetite.)
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
- By default, bundler will install the latest (supported) version of ActiveRecord. To specify a version to test against, run:
305
+ **Test against all supported ActiveRecord versions**
301
306
 
302
307
  ```bash
303
- AR=5.2 bundle update activerecord
304
- bundle exec rake test
305
- ```
308
+ bundle exec appraisal install
306
309
 
307
- Look inside `Gemfile` to see all testable versions.
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. It will be run inside
11
- # of a transaction to ensure batch integrity.
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.offset(offset).limit(l)
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?
@@ -0,0 +1,7 @@
1
+ module OccamsRecord
2
+ class Connection
3
+ def initialize(model, role = nil)
4
+ @model, @role = model, role
5
+ end
6
+ end
7
+ end
@@ -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.active_record_primary_key.to_s})
37
+ single!(assoc_rows, {@ref.foreign_key.to_s => @ref.association_primary_key.to_s})
37
38
  end
38
39
  end
39
40
  end
@@ -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.foreign_key => ids)
25
26
  q.where!(@ref.type => rows[0].class&.model_name) if @ref.options[:as]
@@ -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
- # OccamsRecord will provide the bind values for you.
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
- type =
42
- column_types[col] ||
43
- model_column_types[col] ||
44
- raise("OccamsRecord: Column `#{col}` does not exist on model `#{self.model_name}`")
45
-
46
- case type.type
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.association_primary_key.to_sym
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
@@ -3,5 +3,5 @@
3
3
  #
4
4
  module OccamsRecord
5
5
  # Library version
6
- VERSION = "1.1.4".freeze
6
+ VERSION = "1.2.1".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.1.4
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: 2019-12-17 00:00:00.000000000 Z
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.1'
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.1'
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
- rubyforge_project:
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