occams-record 0.7.0 → 0.8.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
  SHA1:
3
- metadata.gz: 22118121c55c3eeb9cc211b6871f34d6ad9d94a5
4
- data.tar.gz: bad217602878815157dba40a38cfd52ac58fdd6d
3
+ metadata.gz: 704aeee89a29c6faaf5044e3eba09efdffa64bbd
4
+ data.tar.gz: 70415bb9a9a32a11dca5d1a25d4eff04ffc19571
5
5
  SHA512:
6
- metadata.gz: 1da29e4dad1adabf559e808b691c9281d299abde694999ae4de00a8502ebfe61b9dcde370d84ddea754744fcc32580b644759a48a37b9de3ca521bca4a9d6836
7
- data.tar.gz: 8daa621bd1b62c34750afb1075f9f868bd030b313b8deb947011527464e97941a50b83b64a1d0e1ac46225b34dc8a6eabae2ed719f976db249697efb32f14986
6
+ metadata.gz: 487a7e553edb7f30e84ec4071d01596d36cf1232aa1016265bdf4b589f5a2118cfbcffaff0b2508927811638e6b6f4af5a51bd52726ab46f1b8297eb0db8dcda
7
+ data.tar.gz: 8f3f6d2850eb49dabefc1dd95221f19ec56ec70c00c067b5ddf79d0dd96b87bcab854ed99fc041502c598f6df267dd3e657e908f685f06d5a660aed44657869b
data/README.md CHANGED
@@ -138,6 +138,36 @@ widgets[0].orders[0].description
138
138
  => "O839SJZ98B 1/8/2017"
139
139
  ```
140
140
 
141
+ ## Raw SQL queries
142
+
143
+ If you have a complicated query to run, you may drop down to hand-written SQL while still taking advantage of eager loading and variable escaping. (Note the slightly different syntax for binding variables.)
144
+
145
+ NOTE this feature is quite new and might have some bugs. Issues and Pull Requests welcome.
146
+
147
+ ```ruby
148
+ widgets = OccamsRecord.sql(%(
149
+ SELECT * FROM widgets
150
+ WHERE category_id = %{cat_id}
151
+ ), {
152
+ cat_id: 5
153
+ }).run
154
+ ```
155
+
156
+ To perform eager loading, you must specify the base model. NOTE some database adapters, notably SQLite, require you to always specify the model.
157
+
158
+ ```ruby
159
+ widgets = OccamsRecord.
160
+ sql(%(
161
+ SELECT * FROM widgets
162
+ WHERE category_id IN (%{cat_ids})
163
+ ), {
164
+ cat_ids: [5, 10]
165
+ }).
166
+ model(Widget).
167
+ eager_load(:category).
168
+ run
169
+ ```
170
+
141
171
  ## Unsupported features
142
172
 
143
173
  The following `ActiveRecord` are not supported, and I have no plans to do so. However, I'd be glad to accept pull requests.
data/lib/occams-record.rb CHANGED
@@ -2,5 +2,6 @@ require 'active_record'
2
2
  require 'occams-record/version'
3
3
  require 'occams-record/merge'
4
4
  require 'occams-record/eager_loaders'
5
- require 'occams-record/result_row'
5
+ require 'occams-record/results'
6
6
  require 'occams-record/query'
7
+ require 'occams-record/raw_query'
@@ -50,7 +50,8 @@ module OccamsRecord
50
50
  #
51
51
  # Returns an Enumerator that yields batches of records, of size "of".
52
52
  # NOTE ActiveRecord 5+ provides the 'in_batches' method to do something
53
- # similiar, although 4.2 does not. Also it does not respect ORDER BY.
53
+ # similiar, although 4.2 does not. Also it does not respect ORDER BY,
54
+ # whereas this does.
54
55
  #
55
56
  # @param of [Integer] batch size
56
57
  # @return [Enumerator] yields batches
@@ -10,19 +10,54 @@ module OccamsRecord
10
10
  autoload :HasMany, 'occams-record/eager_loaders/has_many'
11
11
  autoload :Habtm, 'occams-record/eager_loaders/habtm'
12
12
 
13
- # Fetch the appropriate eager loader for the given association type.
14
- def self.fetch!(ref)
15
- case ref.macro
16
- when :belongs_to
17
- ref.options[:polymorphic] ? PolymorphicBelongsTo : BelongsTo
18
- when :has_one
19
- HasOne
20
- when :has_many
21
- HasMany
22
- when :has_and_belongs_to_many
23
- EagerLoaders::Habtm
24
- else
25
- raise "Unsupported association type `#{macro}`"
13
+ # Methods for adding eager loading to a query.
14
+ module Builder
15
+ #
16
+ # Specify an association to be eager-loaded. You may optionally pass a block that accepts a scope
17
+ # which you may modify to customize the query. For maximum memory savings, always `select` only
18
+ # the colums you actually need.
19
+ #
20
+ # @param assoc [Symbol] name of association
21
+ # @param scope [Proc] a scope to apply to the query (optional)
22
+ # @param select [String] a custom SELECT statement, minus the SELECT (optional)
23
+ # @param use [Array<Module>] optional Module to include in the result class (single or array)
24
+ # @param eval_block [Proc] a block where you may perform eager loading on *this* association (optional)
25
+ # @return [OccamsRecord::Query] returns self
26
+ #
27
+ def eager_load(assoc, scope = nil, select: nil, use: nil, &eval_block)
28
+ ref = @model ? @model.reflections[assoc.to_s] : nil
29
+ raise "OccamsRecord: No assocation `:#{assoc}` on `#{@model&.name || '<model missing>'}`" if ref.nil?
30
+ scope ||= -> { self.select select } if select
31
+ @eager_loaders << eager_loader_for_association(ref).new(ref, scope, use, &eval_block)
32
+ self
33
+ end
34
+
35
+ private
36
+
37
+ # Run all defined eager loaders into the given result rows
38
+ def eager_load!(rows)
39
+ @eager_loaders.each { |loader|
40
+ loader.query(rows) { |scope|
41
+ assoc_rows = Query.new(scope, use: loader.use, query_logger: @query_logger, &loader.eval_block).run
42
+ loader.merge! assoc_rows, rows
43
+ }
44
+ }
45
+ end
46
+
47
+ # Fetch the appropriate eager loader for the given association type.
48
+ def eager_loader_for_association(ref)
49
+ case ref.macro
50
+ when :belongs_to
51
+ ref.options[:polymorphic] ? PolymorphicBelongsTo : BelongsTo
52
+ when :has_one
53
+ HasOne
54
+ when :has_many
55
+ HasMany
56
+ when :has_and_belongs_to_many
57
+ EagerLoaders::Habtm
58
+ else
59
+ raise "Unsupported association type `#{macro}`"
60
+ end
26
61
  end
27
62
  end
28
63
  end
@@ -25,7 +25,7 @@ module OccamsRecord
25
25
  #
26
26
  # Yield one or more ActiveRecord::Relation objects to a given block.
27
27
  #
28
- # @param rows [Array<OccamsRecord::ResultRow>] Array of rows used to calculate the query.
28
+ # @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
29
29
  #
30
30
  def query(rows)
31
31
  raise 'Not Implemented'
@@ -34,8 +34,8 @@ module OccamsRecord
34
34
  #
35
35
  # Merges the associated rows into the parent rows.
36
36
  #
37
- # @param assoc_rows [Array<OccamsRecord::ResultRow>]
38
- # @param rows [Array<OccamsRecord::ResultRow>]
37
+ # @param assoc_rows [Array<OccamsRecord::Results::Row>]
38
+ # @param rows [Array<OccamsRecord::Results::Row>]
39
39
  #
40
40
  def merge!(assoc_rows, rows)
41
41
  raise 'Not Implemented'
@@ -5,7 +5,7 @@ module OccamsRecord
5
5
  #
6
6
  # Yield one or more ActiveRecord::Relation objects to a given block.
7
7
  #
8
- # @param rows [Array<OccamsRecord::ResultRow>] Array of rows used to calculate the query.
8
+ # @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
9
9
  #
10
10
  def query(rows)
11
11
  ids = rows.map { |r| r.send @ref.foreign_key }.compact.uniq
@@ -15,8 +15,8 @@ module OccamsRecord
15
15
  #
16
16
  # Merge the association rows into the given rows.
17
17
  #
18
- # @param assoc_rows [Array<OccamsRecord::ResultRow>] rows loaded from the association
19
- # @param rows [Array<OccamsRecord::ResultRow>] rows loaded from the main model
18
+ # @param assoc_rows [Array<OccamsRecord::Results::Row>] rows loaded from the association
19
+ # @param rows [Array<OccamsRecord::Results::Row>] rows loaded from the main model
20
20
  #
21
21
  def merge!(assoc_rows, rows)
22
22
  Merge.new(rows, name).
@@ -5,7 +5,7 @@ module OccamsRecord
5
5
  #
6
6
  # Yield one or more ActiveRecord::Relation objects to a given block.
7
7
  #
8
- # @param rows [Array<OccamsRecord::ResultRow>] Array of rows used to calculate the query.
8
+ # @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
9
9
  #
10
10
  def query(rows)
11
11
  assoc_ids = join_rows(rows).map { |row| row[1] }.compact.uniq
@@ -15,8 +15,8 @@ module OccamsRecord
15
15
  #
16
16
  # Merge the association rows into the given rows.
17
17
  #
18
- # @param assoc_rows [Array<OccamsRecord::ResultRow>] rows loaded from the association
19
- # @param rows [Array<OccamsRecord::ResultRow>] rows loaded from the main model
18
+ # @param assoc_rows [Array<OccamsRecord::Results::Row>] rows loaded from the association
19
+ # @param rows [Array<OccamsRecord::Results::Row>] rows loaded from the main model
20
20
  #
21
21
  def merge!(assoc_rows, rows)
22
22
  joins_by_id = join_rows(rows).reduce({}) { |a, join|
@@ -46,7 +46,7 @@ module OccamsRecord
46
46
  #
47
47
  # Fetches (and caches) an array of rows from the join table. The rows are [fkey, assoc_fkey].
48
48
  #
49
- # @param rows [Array<OccamsRecord::ResultRow>]
49
+ # @param rows [Array<OccamsRecord::Results::Row>]
50
50
  # @return [Array<Array<String>>]
51
51
  #
52
52
  def join_rows(rows)
@@ -5,8 +5,8 @@ module OccamsRecord
5
5
  #
6
6
  # Merge the association rows into the given rows.
7
7
  #
8
- # @param assoc_rows [Array<OccamsRecord::ResultRow>] rows loaded from the association
9
- # @param rows [Array<OccamsRecord::ResultRow>] rows loaded from the main model
8
+ # @param assoc_rows [Array<OccamsRecord::Results::Row>] rows loaded from the association
9
+ # @param rows [Array<OccamsRecord::Results::Row>] rows loaded from the main model
10
10
  #
11
11
  def merge!(assoc_rows, rows)
12
12
  Merge.new(rows, name).
@@ -5,7 +5,7 @@ module OccamsRecord
5
5
  #
6
6
  # Yield one or more ActiveRecord::Relation objects to a given block.
7
7
  #
8
- # @param rows [Array<OccamsRecord::ResultRow>] Array of rows used to calculate the query.
8
+ # @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
9
9
  #
10
10
  def query(rows)
11
11
  return if rows.empty?
@@ -18,8 +18,8 @@ module OccamsRecord
18
18
  #
19
19
  # Merge the association rows into the given rows.
20
20
  #
21
- # @param assoc_rows [Array<OccamsRecord::ResultRow>] rows loaded from the association
22
- # @param rows [Array<OccamsRecord::ResultRow>] rows loaded from the main model
21
+ # @param assoc_rows [Array<OccamsRecord::Results::Row>] rows loaded from the association
22
+ # @param rows [Array<OccamsRecord::Results::Row>] rows loaded from the main model
23
23
  #
24
24
  def merge!(assoc_rows, rows)
25
25
  Merge.new(rows, name).
@@ -25,7 +25,7 @@ module OccamsRecord
25
25
  #
26
26
  # Yield ActiveRecord::Relations to the given block, one for every "type" represented in the given rows.
27
27
  #
28
- # @param rows [Array<OccamsRecord::ResultRow>] Array of rows used to calculate the query.
28
+ # @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
29
29
  #
30
30
  def query(rows)
31
31
  rows_by_type = rows.group_by(&@foreign_type)
@@ -6,13 +6,13 @@ module OccamsRecord
6
6
  # After initializing, perform a specific type of merge by calling the appropriate *! method.
7
7
  #
8
8
  class Merge
9
- # @return [Array<OccamsRecord::ResultRow>] the rows into which associated rows will be merged
9
+ # @return [Array<OccamsRecord::Results::Row>] the rows into which associated rows will be merged
10
10
  attr_reader :target_rows
11
11
 
12
12
  #
13
13
  # Initialize a new Merge operation.
14
14
  #
15
- # @param target_rows [Array<OccamsRecord::ResultRow] the rows into which associated rows should be merged
15
+ # @param target_rows [Array<OccamsRecord::Results::Row] the rows into which associated rows should be merged
16
16
  # @param assoc_name [String|Symbol] name of the attribute where associated rows will be put
17
17
  #
18
18
  def initialize(target_rows, assoc_name)
@@ -24,7 +24,7 @@ module OccamsRecord
24
24
  # Merge a single assoc_row into each target_rows (or nil if one can't be found).
25
25
  # target_attr and assoc_attr are the matching keys on target_rows and assoc_rows, respectively.
26
26
  #
27
- # @param assoc_rows [Array<OccamsRecord::ResultRow>] rows to merge into target_rows
27
+ # @param assoc_rows [Array<OccamsRecord::Results::Row>] rows to merge into target_rows
28
28
  # @param target_attr [String|Symbol] name of the matching key on the target records
29
29
  # @param assoc_attr [String] name of the matching key on the associated records
30
30
  #
@@ -35,10 +35,9 @@ module OccamsRecord
35
35
  attr_reader :model
36
36
  # @return [ActiveRecord::Relation] scope for building the main SQL query
37
37
  attr_reader :scope
38
- # @return [ActiveRecord::Connection]
39
- attr_reader :conn
40
38
 
41
39
  include Batches
40
+ include EagerLoaders::Builder
42
41
 
43
42
  #
44
43
  # Initialize a new query.
@@ -53,51 +52,23 @@ module OccamsRecord
53
52
  @model = scope.klass
54
53
  @scope = scope
55
54
  @eager_loaders = eager_loaders
56
- @conn = model.connection
57
55
  @use = use
58
56
  @query_logger = query_logger
59
57
  instance_eval(&eval_block) if eval_block
60
58
  end
61
59
 
62
- #
63
- # Specify an association to be eager-loaded. You may optionally pass a block that accepts a scope
64
- # which you may modify to customize the query. For maximum memory savings, always `select` only
65
- # the colums you actually need.
66
- #
67
- # @param assoc [Symbol] name of association
68
- # @param scope [Proc] a scope to apply to the query (optional)
69
- # @param select [String] a custom SELECT statement, minus the SELECT (optional)
70
- # @param use [Array<Module>] optional Module to include in the result class (single or array)
71
- # @param eval_block [Proc] a block where you may perform eager loading on *this* association (optional)
72
- # @return [OccamsRecord::Query] returns self
73
- #
74
- def eager_load(assoc, scope = nil, select: nil, use: nil, &eval_block)
75
- ref = model.reflections[assoc.to_s]
76
- raise "OccamsRecord: No assocation `:#{assoc}` on `#{model.name}`" if ref.nil?
77
- scope ||= -> { self.select select } if select
78
- @eager_loaders << EagerLoaders.fetch!(ref).new(ref, scope, use, &eval_block)
79
- self
80
- end
81
-
82
60
  #
83
61
  # Run the query and return the results.
84
62
  #
85
- # @return [Array<OccamsRecord::ResultRow>]
63
+ # @return [Array<OccamsRecord::Results::Row>]
86
64
  #
87
65
  def run
88
66
  sql = scope.to_sql
89
67
  @query_logger << sql if @query_logger
90
- result = conn.exec_query sql
91
- row_class = OccamsRecord.build_result_row_class(model, result.columns, result.column_types, @eager_loaders.map(&:name), modules: @use)
68
+ result = model.connection.exec_query sql
69
+ row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.map(&:name), model: model, modules: @use)
92
70
  rows = result.rows.map { |row| row_class.new row }
93
-
94
- @eager_loaders.each { |loader|
95
- loader.query(rows) { |scope|
96
- assoc_rows = Query.new(scope, use: loader.use, query_logger: @query_logger, &loader.eval_block).run
97
- loader.merge! assoc_rows, rows
98
- }
99
- }
100
-
71
+ eager_load! rows
101
72
  rows
102
73
  end
103
74
 
@@ -0,0 +1,128 @@
1
+ module OccamsRecord
2
+ #
3
+ # Starts building a OccamsRecord::RawQuery. Pass it a raw SQL statement, optionally followed by
4
+ # a Hash of binds. While this doesn't offer an additional performance boost, it's a nice way to
5
+ # write safe, complicated SQL by hand while also supporting eager loading.
6
+ #
7
+ # results = OccamsRecord.sql(%(
8
+ # SELECT * FROM widgets
9
+ # WHERE category_id = %{cat_id}
10
+ # ), {
11
+ # cat_id: 5
12
+ # }).run
13
+ #
14
+ # If you want to do eager loading, you must first the define a model to pull the associations from.
15
+ # NOTE If you're using SQLite, you must *always* specify the model.
16
+ #
17
+ # results = OccamsRecord.
18
+ # sql(%(
19
+ # SELECT * FROM widgets
20
+ # WHERE category_id IN (%{cat_ids})
21
+ # ), {
22
+ # cat_ids: [5, 10]
23
+ # }).
24
+ # model(Widget).
25
+ # eager_load(:category).
26
+ # run
27
+ #
28
+ # @param sql [String] The SELECT statement to run. Binds should use the built-in Ruby "%{bind_name}" syntax.
29
+ # @param binds [Hash] Bind values (Symbol keys)
30
+ # @param use [Array<Module>] optional Module to include in the result class (single or array)
31
+ # @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
32
+ # @return [OccamsRecord::RawQuery]
33
+ #
34
+ def self.sql(sql, binds, use: nil, query_logger: nil)
35
+ RawQuery.new(sql, binds, use: use, query_logger: nil)
36
+ end
37
+
38
+ #
39
+ # Represents a raw SQL query to be run and eager associations to be loaded. Use OccamsRecord.sql to create your queries
40
+ # instead of instantiating objects directly.
41
+ #
42
+ class RawQuery
43
+ # @return [String]
44
+ attr_reader :sql
45
+ # @return [Hash]
46
+ attr_reader :binds
47
+
48
+ include EagerLoaders::Builder
49
+
50
+ #
51
+ # Initialize a new query.
52
+ #
53
+ # @param sql [String] The SELECT statement to run. Binds should use the built-in Ruby "%{bind_name}" syntax.
54
+ # @param binds [Hash] Bind values (Symbol keys)
55
+ # @param use [Array<Module>] optional Module to include in the result class (single or array)
56
+ # @param eager_loaders [OccamsRecord::EagerLoaders::Base]
57
+ # @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
58
+ # @param eval_block [Proc] block that will be eval'd on this instance. Can be used for eager loading. (optional)
59
+ #
60
+ def initialize(sql, binds, use: nil, eager_loaders: [], query_logger: nil)
61
+ @sql = sql
62
+ @binds = binds
63
+ @use = use
64
+ @eager_loaders = eager_loaders
65
+ @query_logger = query_logger
66
+ @model = nil
67
+ @conn = @model&.connection || ActiveRecord::Base.connection
68
+ end
69
+
70
+ #
71
+ # Specify the model to be used to load eager associations. Normally this would be the main table you're
72
+ # SELECTing from.
73
+ #
74
+ # NOTE Some database adapters, notably SQLite's, require that the model *always* be specified, even if you
75
+ # aren't doing eager loading.
76
+ #
77
+ # @param klass [ActiveRecord::Base]
78
+ # @return [OccamsRecord::RawQuery] self
79
+ #
80
+ def model(klass)
81
+ @model = klass
82
+ self
83
+ end
84
+
85
+ #
86
+ # Run the query and return the results.
87
+ #
88
+ # @return [Array<OccamsRecord::Results::Row>]
89
+ #
90
+ def run
91
+ result = @conn.exec_query escaped_sql
92
+ row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.map(&:name), model: @model, modules: @use)
93
+ rows = result.rows.map { |row| row_class.new row }
94
+ eager_load! rows
95
+ rows
96
+ end
97
+
98
+ alias_method :to_a, :run
99
+
100
+ #
101
+ # If you pass a block, each result row will be yielded to it. If you don't,
102
+ # an Enumerable will be returned.
103
+ #
104
+ # @return Enumerable
105
+ #
106
+ def each
107
+ if block_given?
108
+ to_a.each { |row| yield row }
109
+ else
110
+ to_a.each
111
+ end
112
+ end
113
+
114
+ private
115
+
116
+ # Returns the SQL as a String with all variables escaped
117
+ def escaped_sql
118
+ sql % binds.reduce({}) { |a, (col, val)|
119
+ a[col.to_sym] = if val.is_a? Array
120
+ val.map { |x| @conn.quote x }.join(', ')
121
+ else
122
+ @conn.quote val
123
+ end
124
+ a
125
+ }
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,106 @@
1
+ module OccamsRecord
2
+ module Results
3
+ # ActiveRecord's internal type casting API changes from version to version.
4
+ CASTER = case ActiveRecord::VERSION::MAJOR
5
+ when 4 then :type_cast_from_database
6
+ when 5 then :deserialize
7
+ end
8
+
9
+ #
10
+ # Dynamically build a class for a specific set of result rows. It inherits from OccamsRecord::Results::Row, and optionall includes
11
+ # a user-defined module.
12
+ #
13
+ # @param column_names [Array<String>] the column names in the result set. The order MUST match the order returned by the query.
14
+ # @param column_types [Hash] Column name => type from an ActiveRecord::Result
15
+ # @param association_names [Array<String>] names of associations that will be eager loaded into the results.
16
+ # @param model [ActiveRecord::Base] the AR model representing the table (it holds column & type info).
17
+ # @param modules [Array<Module>] (optional)
18
+ # @return [OccamsRecord::Results::Row] a class customized for this result set
19
+ #
20
+ def self.klass(column_names, column_types, association_names = [], model: nil, modules: nil)
21
+ Class.new(Results::Row) do
22
+ Array(modules).each { |mod| include mod } if modules
23
+
24
+ self.columns = column_names.map(&:to_s)
25
+ self.associations = association_names.map(&:to_s)
26
+ self.model_name = model ? model.name : nil
27
+
28
+ # Build getters & setters for associations. (We need setters b/c they're set AFTER the row is initialized
29
+ attr_accessor(*association_names)
30
+
31
+ # Build a getter for each attribute returned by the query. The values will be type converted on demand.
32
+ model_column_types = model ? model.attributes_builder.types : {}
33
+ self.columns.each_with_index do |col, idx|
34
+ type =
35
+ column_types[col] ||
36
+ model_column_types[col] ||
37
+ raise("OccamsRecord: Column `#{col}` does not exist on model `#{self.model_name}`")
38
+
39
+ case type.type
40
+ when :datetime
41
+ define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx]).in_time_zone }
42
+ else
43
+ define_method(col) { @cast_values[idx] ||= type.send(CASTER, @raw_values[idx]) }
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ #
50
+ # Abstract class for result rows.
51
+ #
52
+ class Row
53
+ class << self
54
+ # Array of column names
55
+ attr_accessor :columns
56
+ # Array of associations names
57
+ attr_accessor :associations
58
+ # Name of Rails model
59
+ attr_accessor :model_name
60
+ end
61
+ self.columns = []
62
+ self.associations = []
63
+
64
+ #
65
+ # Initialize a new result row.
66
+ #
67
+ # @param raw_values [Array] array of raw values from db
68
+ #
69
+ def initialize(raw_values)
70
+ @raw_values = raw_values
71
+ @cast_values = {}
72
+ end
73
+
74
+ #
75
+ # Return row as a Hash (recursive).
76
+ #
77
+ # @param symbolize_names [Boolean] if true, make Hash keys Symbols instead of Strings
78
+ # @return [Hash] a Hash with String or Symbol keys
79
+ #
80
+ def to_h(symbolize_names: false)
81
+ hash = self.class.columns.reduce({}) { |a, col_name|
82
+ key = symbolize_names ? col_name.to_sym : col_name
83
+ a[key] = send col_name
84
+ a
85
+ }
86
+
87
+ self.class.associations.reduce(hash) { |a, assoc_name|
88
+ key = symbolize_names ? assoc_name.to_sym : assoc_name
89
+ assoc = send assoc_name
90
+ a[key] = if assoc.is_a? Array
91
+ assoc.map { |x| x.to_h(symbolize_names: symbolize_names) }
92
+ elsif assoc
93
+ assoc.to_h(symbolize_names: symbolize_names)
94
+ end
95
+ a
96
+ }
97
+ end
98
+
99
+ alias_method :to_hash, :to_h
100
+
101
+ def inspect
102
+ "#<OccamsRecord::Results::Row @model_name=#{self.class.model_name} @raw_values=#{@raw_values}>"
103
+ end
104
+ end
105
+ end
106
+ end
@@ -3,5 +3,5 @@
3
3
  #
4
4
  module OccamsRecord
5
5
  # Library version
6
- VERSION = '0.7.0'.freeze
6
+ VERSION = '0.8.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.7.0
4
+ version: 0.8.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: 2017-11-05 00:00:00.000000000 Z
11
+ date: 2017-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -49,7 +49,8 @@ files:
49
49
  - lib/occams-record/eager_loaders/polymorphic_belongs_to.rb
50
50
  - lib/occams-record/merge.rb
51
51
  - lib/occams-record/query.rb
52
- - lib/occams-record/result_row.rb
52
+ - lib/occams-record/raw_query.rb
53
+ - lib/occams-record/results.rb
53
54
  - lib/occams-record/version.rb
54
55
  homepage: https://github.com/jhollinger/occams-record
55
56
  licenses:
@@ -63,7 +64,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
63
64
  requirements:
64
65
  - - ">="
65
66
  - !ruby/object:Gem::Version
66
- version: 2.1.0
67
+ version: 2.3.0
67
68
  required_rubygems_version: !ruby/object:Gem::Requirement
68
69
  requirements:
69
70
  - - ">="
@@ -1,95 +0,0 @@
1
- module OccamsRecord
2
- # ActiveRecord's internal type casting API changes from version to version.
3
- TYPE_CAST_METHOD = case ActiveRecord::VERSION::MAJOR
4
- when 4 then :type_cast_from_database
5
- when 5 then :deserialize
6
- end
7
-
8
- #
9
- # Dynamically build a class for a specific set of result rows. It inherits from OccamsRecord::ResultRow, and optionall includes
10
- # a user-defined module.
11
- #
12
- # @param model [ActiveRecord::Base] the AR model representing the table (it holds column & type info).
13
- # @param column_names [Array<String>] the column names in the result set. The order MUST match the order returned by the query.
14
- # @param column_types [Hash] Column name => type from an ActiveRecord::Result
15
- # @param association_names [Array<String>] names of associations that will be eager loaded into the results.
16
- # @param modules [Array<Module>] (optional)
17
- # @return [OccamsRecord::ResultRow] a class customized for this result set
18
- #
19
- def self.build_result_row_class(model, column_names, column_types, association_names, modules: nil)
20
- Class.new(ResultRow) do
21
- Array(modules).each { |mod| include mod } if modules
22
-
23
- self.columns = column_names.map(&:to_s)
24
- self.associations = association_names.map(&:to_s)
25
- self.model_name = model.name
26
-
27
- # Build getters & setters for associations. (We need setters b/c they're set AFTER the row is initialized
28
- attr_accessor(*association_names)
29
-
30
- # Build a getter for each attribute returned by the query. The values will be type converted on demand.
31
- self.columns.each_with_index do |col, idx|
32
- type =
33
- column_types[col] ||
34
- model.attributes_builder.types[col] ||
35
- raise("OccamsRecord: Column `#{col}` does not exist on model `#{self.model_name}`")
36
- define_method col do
37
- @cast_values_cache[idx] ||= type.send(TYPE_CAST_METHOD, @raw_values[idx])
38
- end
39
- end
40
- end
41
- end
42
-
43
- #
44
- # Abstract class for result rows.
45
- #
46
- class ResultRow
47
- class << self
48
- # Array of column names
49
- attr_accessor :columns
50
- # Array of associations names
51
- attr_accessor :associations
52
- # Name of Rails model
53
- attr_accessor :model_name
54
- end
55
- self.columns = []
56
- self.associations = []
57
-
58
- #
59
- # Initialize a new result row.
60
- #
61
- # @param raw_values [Array] array of raw values from db
62
- #
63
- def initialize(raw_values)
64
- @raw_values = raw_values
65
- @cast_values_cache = {}
66
- end
67
-
68
- #
69
- # Return row as a Hash (recursive).
70
- #
71
- # @param symbolize_names [Boolean] if true, make Hash keys Symbols instead of Strings
72
- # @return [Hash] a Hash with String or Symbol keys
73
- #
74
- def to_h(symbolize_names: false)
75
- hash = self.class.columns.reduce({}) { |a, col_name|
76
- key = symbolize_names ? col_name.to_sym : col_name
77
- a[key] = send col_name
78
- a
79
- }
80
-
81
- self.class.associations.reduce(hash) { |a, assoc_name|
82
- key = symbolize_names ? assoc_name.to_sym : assoc_name
83
- assoc = send assoc_name
84
- a[key] = if assoc.is_a? Array
85
- assoc.map { |x| x.to_h(symbolize_names: symbolize_names) }
86
- elsif assoc
87
- assoc.to_h(symbolize_names: symbolize_names)
88
- end
89
- a
90
- }
91
- end
92
-
93
- alias_method :to_hash, :to_h
94
- end
95
- end