occams-record 0.7.0 → 0.8.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
  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