occams-record 0.14.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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