occams-record 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c38ef4286e850f0409ef60595a18e28dfce74f9b43b60fa776d782f899e800c9
4
- data.tar.gz: 4960fa6e619bbc4e20dbaf94fb5d557ddd5f8ed50443b4ad5f5716fb4da1a383
3
+ metadata.gz: d8d0dd44c9dd065285ae2a1dfc0c5cd05f27d70b2b611011f128a7692c7afc31
4
+ data.tar.gz: 0b08d794ce884b340d00b72d531cc3e7e1c0ee4e200eed585c65edf5066e1a4d
5
5
  SHA512:
6
- metadata.gz: d7f32e7259a88470318e0aac8e89d62c993d443c4ff379392cc7eb731f7649c5cb958994e25be2f23386e0ee0b73d05fc07998a71be6faf7f224c3231560010f
7
- data.tar.gz: 9ced85fba2d01430725e33d97984775b7a2a59f14a0fa11b03965302a528cd99a138fa913e4d0ec2b58f7fd9356bfffe79359b3a2ed3a25fa9a99bcff040ee7c
6
+ metadata.gz: 7587c4c1d9464ac8a078b417afb4e9f2603419b9ef13ecc05da8496854fcf787bf93330756118e5e9b4696729f47c4c676bc9ad36b6ae4da0222c931c7bace0c
7
+ data.tar.gz: b897b931d76a8e87eb97a07319975cd57c6cfa355842f0097830dd75db09f22c158edcb40d476b7be2d54d8c3b489e87294bf66bf0147e234f1dbeba18c2b590
data/README.md CHANGED
@@ -11,6 +11,7 @@ Occam's Record is a high-efficiency query API for ActiveRecord. It is **not** an
11
11
  * `find_each`/`find_in_batches` respects `order` and `limit`.
12
12
  * Allows eager loading of associations when using raw SQL.
13
13
  * Allows `find_each`/`find_in_batches` when using raw SQL.
14
+ * Eager load data from arbitrary SQL (no association required).
14
15
 
15
16
  [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:
16
17
 
@@ -86,6 +87,28 @@ widgets = OccamsRecord.
86
87
  run
87
88
  ```
88
89
 
90
+ **Eager load using raw SQL without a predefined association**
91
+
92
+ Let's say we want to load each widget and eager load all the customers who've ever ordered it. We could do that using the above example, but we end up loading a bunch of stuff we don't care about. What if we could define an ad hoc association using raw SQL? Enter `eager_load_one` and `eager_load_many`! See the full documentation for a full description of all options.
93
+
94
+ ```ruby
95
+ widgets = OccamsRecord.
96
+ query(Widget.order("name")).
97
+
98
+ # load the results of the query into "customers", matching "widget_id"
99
+ # in the results to the "id" field of the widgets
100
+ eager_load_many(:customers, {:widget_id => :id}, %(
101
+ SELECT DISTINCT customers.id, customers.name, order_items.widget_id
102
+ FROM customers
103
+ INNER JOIN orders ON orders.customer_id = customers.id
104
+ INNER JOIN order_items ON order_items.order_id = orders.id
105
+ WHERE order_items.widget_id IN (%{ids})
106
+ ), binds: {
107
+ # additional bind values (ids will be passed in for you)
108
+ }).
109
+ run
110
+ ```
111
+
89
112
  ## Injecting instance methods
90
113
 
91
114
  By default your results will only have getters for selected columns and eager-loaded associations. If you must, you *can* inject extra methods into your results by putting those methods into a Module. NOTE this is discouraged, as you should try to maintain a clear separation between your persistence layer and your domain.
@@ -10,6 +10,10 @@ module OccamsRecord
10
10
  autoload :HasMany, 'occams-record/eager_loaders/has_many'
11
11
  autoload :Habtm, 'occams-record/eager_loaders/habtm'
12
12
 
13
+ autoload :AdHocBase, 'occams-record/eager_loaders/ad_hoc_base'
14
+ autoload :AdHocOne, 'occams-record/eager_loaders/ad_hoc_one'
15
+ autoload :AdHocMany, 'occams-record/eager_loaders/ad_hoc_many'
16
+
13
17
  # Methods for adding eager loading to a query.
14
18
  module Builder
15
19
  #
@@ -34,15 +38,72 @@ module OccamsRecord
34
38
  self
35
39
  end
36
40
 
41
+ #
42
+ # Specify some arbitrary SQL to be loaded into some arbitrary attribute ("name"). The attribute will
43
+ # hold either one record or none.
44
+ #
45
+ # In the example below, :category is NOT an association on Widget. Though if it where it would be a belongs_to. The
46
+ # mapping argument says "The id column in this table (categories) maps to the category_id column in the other table (widgets)".
47
+ # The %{ids} bind param will be provided for you, and in this case will be all the category_id values from the main
48
+ # query.
49
+ #
50
+ # res = OccamsRecord.
51
+ # query(Widget.order("name")).
52
+ # eager_load_one(:category, {:id => :category_id}, %(
53
+ # SELECT * FROM categories WHERE id IN (%{ids}) AND name != %{bad_name}
54
+ # ), binds: {
55
+ # bad_name: "Bad Category"
56
+ # }).
57
+ # run
58
+ #
59
+ # @param name [Symbol] name of attribute to load records into
60
+ # @param mapping [Hash] a one element Hash with the key being the local/child id and the value being the foreign/parent id
61
+ # @param sql [String] the SQL to query the associated records. Include a bind params called '%{ids}' for the foreign/parent ids.
62
+ # @param binds [Hash] any additional binds for your query.
63
+ # @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
64
+ # @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
65
+ #
66
+ def eager_load_one(*args, &eval_block)
67
+ @eager_loaders << EagerLoaders::AdHocOne.new(*args, &eval_block)
68
+ self
69
+ end
70
+
71
+ #
72
+ # Specify some arbitrary SQL to be loaded into some arbitrary attribute ("name"). The attribute will
73
+ # hold an array of 0 or more associated records.
74
+ #
75
+ # In the example below, :parts is NOT an association on Widget. Though if it where it would be a has_many. The
76
+ # mapping argument says "The widget_id column in this table (parts) maps to the id column in the other table (widgets)".
77
+ # The %{ids} bind param will be provided for you, and in this case will be all the id values from the main
78
+ # query.
79
+ #
80
+ # res = OccamsRecord.
81
+ # query(Widget.order("name")).
82
+ # eager_load_many(:parts, {:widget_id => :id}, %(
83
+ # SELECT * FROM parts WHERE widget_id IN (%{ids}) AND sku NOT IN (%{bad_skus})
84
+ # ), binds: {
85
+ # bad_skus: ["G90023ASDf0"]
86
+ # }).
87
+ # run
88
+ #
89
+ # @param name [Symbol] name of attribute to load records into
90
+ # @param mapping [Hash] a one element Hash with the key being the local/child id and the value being the foreign/parent id
91
+ # @param sql [String] the SQL to query the associated records. Include a bind params called '%{ids}' for the foreign/parent ids.
92
+ # @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
93
+ # @param binds [Hash] any additional binds for your query.
94
+ # @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
95
+ #
96
+ def eager_load_many(*args, &eval_block)
97
+ @eager_loaders << EagerLoaders::AdHocMany.new(*args, &eval_block)
98
+ self
99
+ end
100
+
37
101
  private
38
102
 
39
103
  # Run all defined eager loaders into the given result rows
40
104
  def eager_load!(rows)
41
105
  @eager_loaders.each { |loader|
42
- loader.query(rows) { |scope|
43
- assoc_rows = Query.new(scope, use: loader.use, query_logger: @query_logger, &loader.eval_block).run
44
- loader.merge! assoc_rows, rows
45
- }
106
+ loader.run(rows, query_logger: @query_logger)
46
107
  }
47
108
  end
48
109
 
@@ -0,0 +1,61 @@
1
+ module OccamsRecord
2
+ module EagerLoaders
3
+ #
4
+ # Base class for eager loading ad hoc associations.
5
+ #
6
+ class AdHocBase
7
+ # @return [String] association name
8
+ attr_reader :name
9
+
10
+ #
11
+ # Initialize a new add hoc association.
12
+ #
13
+ # @param name [Symbol] name of attribute to load records into
14
+ # @param mapping [Hash] a one element Hash with the key being the local/child id and the value being the foreign/parent id
15
+ # @param sql [String] the SQL to query the associated records. Include a bind params called '%{ids}' for the foreign/parent ids.
16
+ # @param binds [Hash] any additional binds for your query.
17
+ # @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
18
+ # @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
19
+ # @yield
20
+ #
21
+ def initialize(name, mapping, sql, binds: {}, model: nil, use: nil, &eval_block)
22
+ @name = name.to_s
23
+ @sql, @binds, @use, @model, @eval_block = sql, binds, use, model, eval_block
24
+ raise ArgumentError, "Add-hoc eager loading mapping must contain exactly one key-value pair" unless mapping.size == 1
25
+ @local_key = mapping.keys.first
26
+ @foreign_key = mapping.fetch(@local_key)
27
+ end
28
+
29
+ #
30
+ # Run the query and merge the results into the given rows.
31
+ #
32
+ # @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
33
+ # @param query_logger [Array<String>]
34
+ #
35
+ def run(rows, query_logger: nil)
36
+ calc_ids(rows) { |ids|
37
+ binds = @binds.merge({:ids => ids})
38
+ assoc_rows = RawQuery.new(@sql, binds, use: @use, query_logger: query_logger, &@eval_block).model(@model).run
39
+ merge! assoc_rows, rows
40
+ }
41
+ end
42
+
43
+ private
44
+
45
+ #
46
+ # Yield ids from the parent association to a block.
47
+ #
48
+ # @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
49
+ # @yield
50
+ #
51
+ def calc_ids(rows)
52
+ ids = rows.map { |r| r.send @foreign_key }.compact.uniq
53
+ yield ids
54
+ end
55
+
56
+ def merge!(assoc_rows, rows)
57
+ raise 'Not Implemented'
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,21 @@
1
+ module OccamsRecord
2
+ module EagerLoaders
3
+ #
4
+ # Eager loader for an ad hoc association of 0 or many records (like has_many).
5
+ #
6
+ class AdHocMany < AdHocBase
7
+ private
8
+
9
+ #
10
+ # Merge the association rows into the given rows.
11
+ #
12
+ # @param assoc_rows [Array<OccamsRecord::Results::Row>] rows loaded from the associated table
13
+ # @param rows [Array<OccamsRecord::Results::Row>] rows loaded from the main table
14
+ #
15
+ def merge!(assoc_rows, rows)
16
+ Merge.new(rows, name).
17
+ many!(assoc_rows, @foreign_key.to_s, @local_key.to_s)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module OccamsRecord
2
+ module EagerLoaders
3
+ #
4
+ # Eager loader for an ad hoc association of 0 or 1 records (like belongs_to or has_one).
5
+ #
6
+ class AdHocOne < AdHocBase
7
+ private
8
+
9
+ #
10
+ # Merge the association rows into the given rows.
11
+ #
12
+ # @param assoc_rows [Array<OccamsRecord::Results::Row>] rows loaded from the associated table
13
+ # @param rows [Array<OccamsRecord::Results::Row>] rows loaded from the main table
14
+ #
15
+ def merge!(assoc_rows, rows)
16
+ Merge.new(rows, name).
17
+ single!(assoc_rows, @foreign_key.to_s, @local_key.to_s)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -6,10 +6,6 @@ module OccamsRecord
6
6
  class Base
7
7
  # @return [String] association name
8
8
  attr_reader :name
9
- # @return [Array<Module>] optional Module to include in the result class (single or array)
10
- attr_reader :use
11
- # @return [Proc] optional Proc for eager loading things on this association
12
- attr_reader :eval_block
13
9
 
14
10
  #
15
11
  # @param ref [ActiveRecord::Association] the ActiveRecord association
@@ -25,6 +21,21 @@ module OccamsRecord
25
21
  @name = (as || ref.name).to_s
26
22
  end
27
23
 
24
+ #
25
+ # Run the query and merge the results into the given rows.
26
+ #
27
+ # @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
28
+ # @param query_logger [Array<String>]
29
+ #
30
+ def run(rows, query_logger: nil)
31
+ query(rows) { |scope|
32
+ assoc_rows = Query.new(scope, use: @use, query_logger: query_logger, &@eval_block).run
33
+ merge! assoc_rows, rows
34
+ }
35
+ end
36
+
37
+ private
38
+
28
39
  #
29
40
  # Yield one or more ActiveRecord::Relation objects to a given block.
30
41
  #
@@ -2,6 +2,8 @@ module OccamsRecord
2
2
  module EagerLoaders
3
3
  # Eager loader for belongs_to associations.
4
4
  class BelongsTo < Base
5
+ private
6
+
5
7
  #
6
8
  # Yield one or more ActiveRecord::Relation objects to a given block.
7
9
  #
@@ -2,6 +2,8 @@ module OccamsRecord
2
2
  module EagerLoaders
3
3
  # Eager loader for has_and_belongs_to_many associations.
4
4
  class Habtm < Base
5
+ private
6
+
5
7
  #
6
8
  # Yield one or more ActiveRecord::Relation objects to a given block.
7
9
  #
@@ -2,6 +2,8 @@ module OccamsRecord
2
2
  module EagerLoaders
3
3
  # Eager loader for has_many associations.
4
4
  class HasMany < HasOne
5
+ private
6
+
5
7
  #
6
8
  # Merge the association rows into the given rows.
7
9
  #
@@ -2,6 +2,8 @@ module OccamsRecord
2
2
  module EagerLoaders
3
3
  # Eager loader for has_one associations.
4
4
  class HasOne < Base
5
+ private
6
+
5
7
  #
6
8
  # Yield one or more ActiveRecord::Relation objects to a given block.
7
9
  #
@@ -4,10 +4,6 @@ module OccamsRecord
4
4
  class PolymorphicBelongsTo
5
5
  # @return [String] association name
6
6
  attr_reader :name
7
- # @return [Array<Module>] optional Module to include in the result class (single or array)
8
- attr_reader :use
9
- # @return [Proc] optional Proc for eager loading things on this association
10
- attr_reader :eval_block
11
7
 
12
8
  #
13
9
  # @param ref [ActiveRecord::Association] the ActiveRecord association
@@ -24,6 +20,21 @@ module OccamsRecord
24
20
  @foreign_key = @ref.foreign_key.to_sym
25
21
  end
26
22
 
23
+ #
24
+ # Run the query and merge the results into the given rows.
25
+ #
26
+ # @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
27
+ # @param query_logger [Array<String>]
28
+ #
29
+ def run(rows, query_logger: nil)
30
+ query(rows) { |scope|
31
+ assoc_rows = Query.new(scope, use: @use, query_logger: query_logger, &@eval_block).run
32
+ merge! assoc_rows, rows
33
+ }
34
+ end
35
+
36
+ private
37
+
27
38
  #
28
39
  # Yield ActiveRecord::Relations to the given block, one for every "type" represented in the given rows.
29
40
  #
@@ -49,8 +49,8 @@ module OccamsRecord
49
49
  # @return [Hash]
50
50
  attr_reader :binds
51
51
 
52
- include EagerLoaders::Builder
53
52
  include Batches
53
+ include EagerLoaders::Builder
54
54
 
55
55
  #
56
56
  # Initialize a new query.
@@ -61,7 +61,7 @@ module OccamsRecord
61
61
  # @param eager_loaders [OccamsRecord::EagerLoaders::Base]
62
62
  # @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
63
63
  #
64
- def initialize(sql, binds, use: nil, eager_loaders: [], query_logger: nil)
64
+ def initialize(sql, binds, use: nil, eager_loaders: [], query_logger: nil, &eval_block)
65
65
  @sql = sql
66
66
  @binds = binds
67
67
  @use = use
@@ -69,6 +69,7 @@ module OccamsRecord
69
69
  @query_logger = query_logger
70
70
  @model = nil
71
71
  @conn = @model&.connection || ActiveRecord::Base.connection
72
+ instance_eval(&eval_block) if eval_block
72
73
  end
73
74
 
74
75
  #
@@ -92,7 +93,9 @@ module OccamsRecord
92
93
  # @return [Array<OccamsRecord::Results::Row>]
93
94
  #
94
95
  def run
95
- result = @conn.exec_query escaped_sql
96
+ _escaped_sql = escaped_sql
97
+ @query_logger << _escaped_sql if @query_logger
98
+ result = @conn.exec_query _escaped_sql
96
99
  row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.map(&:name), model: @model, modules: @use)
97
100
  rows = result.rows.map { |row| row_class.new row }
98
101
  eager_load! rows
@@ -3,5 +3,5 @@
3
3
  #
4
4
  module OccamsRecord
5
5
  # Library version
6
- VERSION = '0.14.0'.freeze
6
+ VERSION = '0.15.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: 0.14.0
4
+ version: 0.15.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: 2018-03-19 00:00:00.000000000 Z
11
+ date: 2018-03-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -41,6 +41,9 @@ files:
41
41
  - lib/occams-record.rb
42
42
  - lib/occams-record/batches.rb
43
43
  - lib/occams-record/eager_loaders.rb
44
+ - lib/occams-record/eager_loaders/ad_hoc_base.rb
45
+ - lib/occams-record/eager_loaders/ad_hoc_many.rb
46
+ - lib/occams-record/eager_loaders/ad_hoc_one.rb
44
47
  - lib/occams-record/eager_loaders/base.rb
45
48
  - lib/occams-record/eager_loaders/belongs_to.rb
46
49
  - lib/occams-record/eager_loaders/habtm.rb