occams-record 1.8.1 → 1.9.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: 2778b16aee6b725c967c4877b9905eed75b37b64bf8c5fca7077918ea2a4a53a
4
- data.tar.gz: b8948ddd57573efcbeb90e96235626e482cc557e9b92d4e5fdf0237c182e39df
3
+ metadata.gz: e26a6e33886dfdb5cb2d3000e32c9de7e04bb7b26cc4434c5df7f1deb094ac58
4
+ data.tar.gz: 9cc37f3ab8beeef6e55d5adae2e1f76268acad3381b789f70c896a515abe8cfa
5
5
  SHA512:
6
- metadata.gz: e436f30e97f0a256d146539902b3f6fe9b54c241d7b49a51da63242f59505793c14f6ceef601343660750d0030ddf21c196e40a4aec41b1b5f1490b71b255ca6
7
- data.tar.gz: 14095b912856efe2a097471046e518c0d3040320e72198ae28cf8b76c84040ca3319f16916ef2c70c357d8661e6465a36d7b80c49109896c828a2cc36dd777ee
6
+ metadata.gz: a8236d746e52a2890dfb83e838db153150c249ff0b5fb53a1bc949d386258b5047dc93e63a40d747e0f584ffbcaf3d069122962c23a2fc3691a4e162044927af
7
+ data.tar.gz: 1883cb2f7d7817561837b911828b56b540b5dff7845a943340c489786b73be9b3c24270ec2d32a5e7b9ecdb324a21f346948f183e13439e9339424ee9b593ad9
data/README.md CHANGED
@@ -130,6 +130,18 @@ orders = OccamsRecord
130
130
 
131
131
  ActiveRecord has raw SQL escape hatches like `find_by_sql` and `exec_query`, but they give up critical features like eager loading and `find_each`/`find_in_batches`. Occams Record's escape hatches don't make you give up anything.
132
132
 
133
+ **Query params**
134
+
135
+ ```ruby
136
+ # Supported in all versions of OccamsRecord
137
+ OccamsRecord.sql("SELECT * FROM orders WHERE user_id = %{user_id}", {user_id: user.id}).run
138
+
139
+ # Supported in OccamsRecord 1.9+
140
+ OccamsRecord.sql("SELECT * FROM orders WHERE user_id = :user_id", {user_id: user.id}).run
141
+ OccamsRecord.sql("SELECT * FROM orders WHERE user_id = ?", [user.id]).run
142
+ OccamsRecord.sql("SELECT * FROM orders WHERE user_id = %s", [user.id]).run
143
+ ```
144
+
133
145
  **Batched loading with cursors**
134
146
 
135
147
  `find_each_with_cursor`, `find_in_batches_with_cursor`, and `cursor.open` are all available.
@@ -334,6 +346,16 @@ o o o o +
334
346
  + + o o +
335
347
  ```
336
348
 
349
+ ## Testing without Docker
350
+
351
+ It's possible to run tests without Docker Compose, but you'll be limited by the Ruby version(s) and database(s) you have on your system.
352
+
353
+ ```bash
354
+ bundle install
355
+ bundle exec appraisal ar-7.0 bundle install
356
+ bundle exec appraisal ar-7.0 rake test
357
+ ```
358
+
337
359
  # License
338
360
 
339
361
  MIT License. See LICENSE for details.
@@ -9,8 +9,12 @@ module OccamsRecord
9
9
  @conn, @sql, @binds = conn, sql, binds
10
10
  @use, @query_logger, @eager_loaders = use, query_logger, eager_loaders
11
11
 
12
+ unless binds.is_a? Hash
13
+ raise ArgumentError, "When using find_each/find_in_batches with raw SQL, binds MUST be a Hash. SQL statement: #{@sql}"
14
+ end
15
+
12
16
  unless @sql =~ /LIMIT\s+%\{batch_limit\}/i and @sql =~ /OFFSET\s+%\{batch_offset\}/i
13
- raise ArgumentError, "When using find_each/find_in_batches you must specify 'LIMIT %{batch_limit} OFFSET %{batch_offset}'. SQL statement: #{@sql}"
17
+ raise ArgumentError, "When using find_each/find_in_batches with raw SQL, you must specify 'LIMIT %{batch_limit} OFFSET %{batch_offset}'. SQL statement: #{@sql}"
14
18
  end
15
19
  end
16
20
 
@@ -0,0 +1,71 @@
1
+ module OccamsRecord
2
+ module BindsConverter
3
+ #
4
+ # A base class for converting a SQL string with Rails-style query params (?, :foo) to native Ruby format (%s, %{foo}).
5
+ #
6
+ # It works kind of like a tokenizer. Subclasses must 1) implement get_bind to return the converted bind
7
+ # from the current position and 2) pass the bind sigil (e.g. ?, :) to the parent constructor.
8
+ #
9
+ class Abstract
10
+ # @private
11
+ ESCAPE = "\\".freeze
12
+
13
+ def initialize(sql, bind_sigil)
14
+ @sql = sql
15
+ @end = sql.size - 1
16
+ @start_i, @i = 0, 0
17
+ @bind_sigil = bind_sigil
18
+ end
19
+
20
+ # @return [String] The converted SQL string
21
+ def to_s
22
+ sql = ""
23
+ each { |frag| sql << frag }
24
+ sql
25
+ end
26
+
27
+ protected
28
+
29
+ # Yields each SQL fragment and converted bind to the given block
30
+ def each
31
+ escape = false
32
+ until @i > @end
33
+ char = @sql[@i]
34
+ unescape = escape
35
+ case char
36
+ when @bind_sigil
37
+ if escape
38
+ @i += 1
39
+ elsif @i > @start_i
40
+ yield flush_sql
41
+ else
42
+ yield get_bind
43
+ end
44
+ when ESCAPE
45
+ if escape
46
+ @i += 1
47
+ elsif @i > @start_i
48
+ yield flush_sql
49
+ escape = true
50
+ @i += 1
51
+ @start_i = @i
52
+ else
53
+ escape = true
54
+ @i += 1
55
+ end
56
+ else
57
+ @i += 1
58
+ end
59
+ escape = false if unescape
60
+ end
61
+ yield flush_sql if @i > @start_i
62
+ end
63
+
64
+ def flush_sql
65
+ t = @sql[@start_i..@i - 1]
66
+ @start_i = @i
67
+ t
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,35 @@
1
+ module OccamsRecord
2
+ module BindsConverter
3
+ # @private
4
+ WORD = /\w/
5
+
6
+ #
7
+ # Converts Rails-style named binds (:foo) into native Ruby format (%{foo}).
8
+ #
9
+ class Named < Abstract
10
+ def initialize(sql)
11
+ super(sql, ":".freeze)
12
+ end
13
+
14
+ protected
15
+
16
+ def get_bind
17
+ old_i = @i
18
+ @i += 1
19
+ @start_i = @i
20
+
21
+ until @i > @end or @sql[@i] !~ WORD
22
+ @i += 1
23
+ end
24
+
25
+ if @i > @start_i
26
+ name = @sql[@start_i..@i - 1]
27
+ @start_i = @i
28
+ "%{#{name}}"
29
+ else
30
+ @sql[old_i]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ module OccamsRecord
2
+ module BindsConverter
3
+ #
4
+ # Converts Rails-style positional binds (?) into native Ruby format (%s).
5
+ #
6
+ class Positional < Abstract
7
+ def initialize(sql)
8
+ super(sql, "?".freeze)
9
+ end
10
+
11
+ protected
12
+
13
+ def get_bind
14
+ @i += 1
15
+ @start_i = @i
16
+ "%s".freeze
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,23 @@
1
+ module OccamsRecord
2
+ #
3
+ # Classes and methods for converting from Rails-style binds (?, :foo) to native Ruby format (%s, %{foo}).
4
+ #
5
+ module BindsConverter
6
+ #
7
+ # Convert any Rails-style binds (?, :foo) to native Ruby format (%s, %{foo}).
8
+ #
9
+ # @param sql [String]
10
+ # @param binds [Hash|Array]
11
+ # @return [String] the converted SQL string
12
+ #
13
+ def self.convert(sql, binds)
14
+ converter =
15
+ case binds
16
+ when Hash then Named.new(sql)
17
+ when Array then Positional.new(sql)
18
+ else raise ArgumentError, "OccamsRecord: Unsupported SQL bind params '#{binds.inspect}'. Only Hash and Array are supported"
19
+ end
20
+ converter.to_s
21
+ end
22
+ end
23
+ end
@@ -203,9 +203,8 @@ module OccamsRecord
203
203
  # end
204
204
  #
205
205
  def execute(sql, binds = {})
206
- conn.execute(sql % binds.reduce({}) { |acc, (key, val)|
206
+ conn.execute(sql % binds.each_with_object({}) { |(key, val), acc|
207
207
  acc[key] = conn.quote(val)
208
- acc
209
208
  })
210
209
  end
211
210
 
@@ -51,12 +51,13 @@ module OccamsRecord
51
51
  #
52
52
  def run(rows, query_logger: nil, measurements: nil)
53
53
  fkey_binds = calc_fkey_binds rows
54
- assoc = if fkey_binds.all? { |_, vals| vals.any? }
55
- binds = @binds.merge(fkey_binds)
56
- RawQuery.new(@sql, binds, use: @use, eager_loaders: @eager_loaders, query_logger: query_logger, measurements: measurements).run
57
- else
58
- []
59
- end
54
+ assoc =
55
+ if fkey_binds.all? { |_, vals| vals.any? }
56
+ binds = @binds.merge(fkey_binds)
57
+ RawQuery.new(@sql, binds, use: @use, eager_loaders: @eager_loaders, query_logger: query_logger, measurements: measurements).run
58
+ else
59
+ []
60
+ end
60
61
  merge! assoc, rows
61
62
  nil
62
63
  end
@@ -69,17 +70,15 @@ module OccamsRecord
69
70
  # @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
70
71
  #
71
72
  def calc_fkey_binds(rows)
72
- @mapping.keys.reduce({}) { |a, fkey|
73
- a[fkey.to_s.pluralize.to_sym] = rows.reduce(Set.new) { |aa, row|
73
+ @mapping.keys.each_with_object({}) { |fkey, acc|
74
+ acc[fkey.to_s.pluralize.to_sym] = rows.each_with_object(Set.new) { |row, acc2|
74
75
  begin
75
76
  val = row.send fkey
76
- aa << val if val
77
+ acc2 << val if val
77
78
  rescue NoMethodError => e
78
79
  raise MissingColumnError.new(row, e.name)
79
80
  end
80
- aa
81
81
  }.to_a
82
- a
83
82
  }
84
83
  end
85
84
 
@@ -20,7 +20,6 @@ module OccamsRecord
20
20
  raise MissingColumnError.new(row, e.name)
21
21
  end
22
22
  }.compact.uniq
23
- ids.sort! if $occams_record_test
24
23
 
25
24
  q = base_scope.where(@ref.association_primary_key => ids)
26
25
  yield q if ids.any?
@@ -24,23 +24,21 @@ module OccamsRecord
24
24
  # @param join_rows [Array<Array<String>>] raw join'd ids from the db
25
25
  #
26
26
  def merge!(assoc_rows, rows, join_rows)
27
- joins_by_id = join_rows.reduce({}) { |a, join|
27
+ joins_by_id = join_rows.each_with_object({}) { |join, acc|
28
28
  id = join[0].to_s
29
- a[id] ||= []
30
- a[id] << join[1].to_s
31
- a
29
+ acc[id] ||= []
30
+ acc[id] << join[1].to_s
32
31
  }
33
32
 
34
33
  assoc_order_cache = {} # maintains the original order of assoc_rows
35
- assoc_rows_by_id = assoc_rows.each_with_index.reduce({}) { |a, (row, idx)|
34
+ assoc_rows_by_id = assoc_rows.each_with_index.each_with_object({}) { |(row, idx), acc|
36
35
  begin
37
36
  id = row.send(@ref.association_primary_key).to_s
38
37
  rescue NoMethodError => e
39
38
  raise MissingColumnError.new(row, e.name)
40
39
  end
41
40
  assoc_order_cache[id] = idx
42
- a[id] = row
43
- a
41
+ acc[id] = row
44
42
  }
45
43
 
46
44
  assign = "#{name}="
@@ -20,7 +20,6 @@ module OccamsRecord
20
20
  raise MissingColumnError.new(row, e.name)
21
21
  end
22
22
  }.compact.uniq
23
- ids.sort! if $occams_record_test
24
23
 
25
24
  q = base_scope.where(@ref.foreign_key => ids)
26
25
  q.where!(@ref.type => rows[0].class&.model_name) if @ref.options[:as]
@@ -85,7 +85,6 @@ module OccamsRecord
85
85
  next if type.nil? or type == ""
86
86
  model = type.constantize
87
87
  ids = rows_of_type.map(&@foreign_key).uniq
88
- ids.sort! if $occams_record_test
89
88
  q = base_scope(model).where(@ref.active_record_primary_key => ids)
90
89
  yield q if ids.any?
91
90
  end
@@ -20,7 +20,7 @@ module OccamsRecord
20
20
  raise ArgumentError, "#{@ref.active_record.name}##{@ref.name} cannot be eager loaded because these `through` associations are polymorphic: #{names.join ', '}"
21
21
  end
22
22
  unless @optimizer == :none or @optimizer == :select
23
- raise ArgumentError, "Unrecognized optimizer '#{@optimizer}'"
23
+ raise ArgumentError, "Unrecognized optimizer '#{@optimizer}' (valid options are :none, :select)"
24
24
  end
25
25
 
26
26
  chain = @ref.chain.reverse
@@ -31,7 +31,6 @@ module OccamsRecord
31
31
  @loader = build_loader
32
32
  end
33
33
 
34
- # TODO make not hacky
35
34
  def through_name
36
35
  @loader.name
37
36
  end
@@ -47,6 +46,7 @@ module OccamsRecord
47
46
 
48
47
  private
49
48
 
49
+ # starting at the top of the chain, recurse and return the leaf node(s)
50
50
  def reduce(node, depth = 0)
51
51
  link = @chain[depth]
52
52
  case link&.macro
@@ -69,38 +69,48 @@ module OccamsRecord
69
69
  end
70
70
  end
71
71
 
72
+ # build all the nested eager loaders
72
73
  def build_loader
73
74
  head = @chain[0]
74
75
  links = @chain[1..-2]
75
76
  tail = @chain[-1]
76
77
 
77
- outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref, optimized_select(head), parent: tracer.parent)
78
+ outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref, optimized_scope(head), parent: tracer.parent)
78
79
  outer_loader.tracer.through = true
79
80
 
80
81
  inner_loader = links.
81
82
  reduce(outer_loader) { |loader, link|
82
- nested_loader = loader.nest(link.ref.source_reflection.name, optimized_select(link))
83
+ nested_loader = loader.nest(link.ref.source_reflection.name, optimized_scope(link))
83
84
  nested_loader.tracer.through = true
84
85
  nested_loader
85
86
  }.
86
- nest(tail.ref.source_reflection.name, @scope, use: @use, as: @as, active_record_fallback: @active_record_fallback)
87
+ nest(tail.ref.source_reflection.name, @scopes, use: @use, as: @as, active_record_fallback: @active_record_fallback)
87
88
 
88
89
  @eager_loaders.each { |loader| inner_loader.eager_loaders << loader }
89
90
  inner_loader.tracer.name = tracer.name
90
91
  outer_loader
91
92
  end
92
93
 
93
- def optimized_select(link)
94
- return nil unless @optimizer == :select
94
+ def optimized_scope(link)
95
+ case @optimizer
96
+ when :select
97
+ optimized_select(link)
98
+ when :none
99
+ nil
100
+ end
101
+ end
95
102
 
96
- cols = case link.macro
97
- when :belongs_to
98
- [link.ref.association_primary_key]
99
- when :has_one, :has_many
100
- [link.ref.association_primary_key, link.ref.foreign_key]
101
- else
102
- raise "Unsupported through chain link type '#{link.macro}'"
103
- end
103
+ # only select the ids/foreign keys required to link the parent/child records
104
+ def optimized_select(link)
105
+ cols =
106
+ case link.macro
107
+ when :belongs_to
108
+ [link.ref.association_primary_key]
109
+ when :has_one, :has_many
110
+ [link.ref.association_primary_key, link.ref.foreign_key]
111
+ else
112
+ raise "Unsupported through chain link type '#{link.macro}'"
113
+ end
104
114
 
105
115
  case link.next_ref.source_reflection.macro
106
116
  when :belongs_to
@@ -35,14 +35,13 @@ module OccamsRecord
35
35
  # Optimized for merges where there's a single mapping key pair (which is the vast majority)
36
36
  if mapping.size == 1
37
37
  target_attr, assoc_attr = target_attrs[0], assoc_attrs[0]
38
- assoc_rows_by_ids = assoc_rows.reduce({}) { |a, assoc_row|
38
+ assoc_rows_by_ids = assoc_rows.each_with_object({}) { |assoc_row, acc|
39
39
  begin
40
40
  id = assoc_row.send assoc_attr
41
41
  rescue NoMethodError => e
42
42
  raise MissingColumnError.new(assoc_row, e.name)
43
43
  end
44
- a[id] ||= assoc_row
45
- a
44
+ acc[id] ||= assoc_row
46
45
  }
47
46
 
48
47
  target_rows.each do |row|
@@ -56,14 +55,13 @@ module OccamsRecord
56
55
 
57
56
  # Slower but works with any number of mapping key pairs
58
57
  else
59
- assoc_rows_by_ids = assoc_rows.reduce({}) { |a, assoc_row|
58
+ assoc_rows_by_ids = assoc_rows.each_with_object({}) { |assoc_row, acc|
60
59
  begin
61
60
  ids = assoc_attrs.map { |attr| assoc_row.send attr }
62
61
  rescue NoMethodError => e
63
62
  raise MissingColumnError.new(assoc_row, e.name)
64
63
  end
65
- a[ids] ||= assoc_row
66
- a
64
+ acc[ids] ||= assoc_row
67
65
  }
68
66
 
69
67
  target_rows.each do |row|
@@ -2,28 +2,31 @@ module OccamsRecord
2
2
  module Pluck
3
3
  private
4
4
 
5
- def pluck_results(results, cols, model: nil)
6
- if cols.size == 1
7
- pluck_results_single(results, cols[0].to_s, model: model)
5
+ def pluck_results(results, model: nil)
6
+ casters = TypeCaster.generate(results.columns, results.column_types, model: model)
7
+ if results[0]&.size == 1
8
+ pluck_results_single(results, casters)
8
9
  else
9
- pluck_results_multi(results, cols.map(&:to_s), model: model)
10
+ pluck_results_multi(results, casters)
10
11
  end
11
12
  end
12
13
 
13
14
  # returns an array of values
14
- def pluck_results_single(results, col, model: nil)
15
- casters = TypeCaster.generate(results.columns, results.column_types, model: model)
15
+ def pluck_results_single(results, casters)
16
16
  col = results.columns[0]
17
17
  caster = casters[col]
18
- results.map { |row|
19
- val = row[col]
20
- caster ? caster.(val) : val
21
- }
18
+ if caster
19
+ results.map { |row|
20
+ val = row[col]
21
+ caster.(val)
22
+ }
23
+ else
24
+ results.map { |row| row[col] }
25
+ end
22
26
  end
23
27
 
24
28
  # returns an array of arrays
25
- def pluck_results_multi(results, cols, model: nil)
26
- casters = TypeCaster.generate(results.columns, results.column_types, model: model)
29
+ def pluck_results_multi(results, casters)
27
30
  results.map { |row|
28
31
  row.map { |col, val|
29
32
  caster = casters[col]
@@ -99,14 +99,15 @@ module OccamsRecord
99
99
  return [] if sql.blank? # return early in case ActiveRecord::QueryMethods#none was used
100
100
 
101
101
  @query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
102
- result = if measure?
103
- record_start_time!
104
- measure!(model.table_name, sql) {
105
- model.connection.exec_query sql
106
- }
107
- else
108
- model.connection.exec_query sql
109
- end
102
+ result =
103
+ if measure?
104
+ record_start_time!
105
+ measure!(model.table_name, sql) {
106
+ model.connection.exec_query sql
107
+ }
108
+ else
109
+ model.connection.exec_query sql
110
+ end
110
111
  row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: model, modules: @use, tracer: @eager_loaders.tracer, active_record_fallback: @active_record_fallback)
111
112
  rows = result.rows.map { |row| row_class.new row }
112
113
  @eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
@@ -238,19 +239,20 @@ module OccamsRecord
238
239
  # @return [Array]
239
240
  #
240
241
  def pluck(*cols)
241
- sql = (block_given? ? yield(scope).to_sql : scope).select(*cols).to_sql
242
+ sql = (block_given? ? yield(scope).to_sql : scope).unscope(:select).select(*cols).to_sql
242
243
  return [] if sql.blank? # return early in case ActiveRecord::QueryMethods#none was used
243
244
 
244
245
  @query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
245
- result = if measure?
246
- record_start_time!
247
- measure!(model.table_name, sql) {
248
- model.connection.exec_query sql
249
- }
250
- else
251
- model.connection.exec_query sql
252
- end
253
- pluck_results result, cols, model: @model
246
+ result =
247
+ if measure?
248
+ record_start_time!
249
+ measure!(model.table_name, sql) {
250
+ model.connection.exec_query sql
251
+ }
252
+ else
253
+ model.connection.exec_query sql
254
+ end
255
+ pluck_results(result, model: @model)
254
256
  end
255
257
  end
256
258
  end
@@ -1,7 +1,7 @@
1
1
  module OccamsRecord
2
2
  #
3
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
4
+ # a Hash or Array 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
7
  # results = OccamsRecord.sql("
@@ -39,10 +39,10 @@ module OccamsRecord
39
39
  # It is possible to coerce the SQLite adapter into returning native types for everything IF they're columns of a table
40
40
  # that you have an AR model for. e.g. if you're selecting from the widgets, table: `OccamsRecord.sql("...").model(Widget)...`.
41
41
  #
42
- # MySQL ?
42
+ # MySQL Mostly native Ruby types, but more testing is needed.
43
43
  #
44
- # @param sql [String] The SELECT statement to run. Binds should use Ruby's named string substitution.
45
- # @param binds [Hash] Bind values (Symbol keys)
44
+ # @param sql [String] The SELECT statement to run. Binds may be Rails-style (?, :foo) or Ruby-style (%s, %{foo}).
45
+ # @param binds [Hash] Bind values as Hash (with Symbol keys) or an Array
46
46
  # @param use [Array<Module>] optional Module to include in the result class (single or array)
47
47
  # @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
48
48
  # @return [OccamsRecord::RawQuery]
@@ -58,7 +58,7 @@ module OccamsRecord
58
58
  class RawQuery
59
59
  # @return [String]
60
60
  attr_reader :sql
61
- # @return [Hash]
61
+ # @return [Hash|Array]
62
62
  attr_reader :binds
63
63
 
64
64
  include OccamsRecord::Batches::CursorHelpers
@@ -70,8 +70,8 @@ module OccamsRecord
70
70
  #
71
71
  # Initialize a new query.
72
72
  #
73
- # @param sql [String] The SELECT statement to run. Binds should use Ruby's named string substitution.
74
- # @param binds [Hash] Bind values (Symbol keys)
73
+ # @param sql [String] The SELECT statement to run. Binds may be Rails-style (?, :foo) or Ruby-style (%s, %{foo}).
74
+ # @param binds [Hash] Bind values as Hash (with Symbol keys) or an Array
75
75
  # @param use [Array<Module>] optional Module to include in the result class (single or array)
76
76
  # @param eager_loaders [OccamsRecord::EagerLoaders::Context]
77
77
  # @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
@@ -79,7 +79,7 @@ module OccamsRecord
79
79
  # @param connection
80
80
  #
81
81
  def initialize(sql, binds, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil, connection: nil)
82
- @sql = sql
82
+ @sql = BindsConverter.convert(sql, binds)
83
83
  @binds = binds
84
84
  @use = use
85
85
  @eager_loaders = eager_loaders || EagerLoaders::Context.new
@@ -110,14 +110,15 @@ module OccamsRecord
110
110
  def run
111
111
  _escaped_sql = escaped_sql
112
112
  @query_logger << _escaped_sql if @query_logger
113
- result = if measure?
114
- record_start_time!
115
- measure!(table_name, _escaped_sql) {
116
- conn.exec_query _escaped_sql
117
- }
118
- else
119
- conn.exec_query _escaped_sql
120
- end
113
+ result =
114
+ if measure?
115
+ record_start_time!
116
+ measure!(table_name, _escaped_sql) {
117
+ conn.exec_query _escaped_sql
118
+ }
119
+ else
120
+ conn.exec_query _escaped_sql
121
+ end
121
122
  row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: @eager_loaders.model, modules: @use, tracer: @eager_loaders.tracer)
122
123
  rows = result.rows.map { |row| row_class.new row }
123
124
  @eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
@@ -211,25 +212,29 @@ module OccamsRecord
211
212
  end
212
213
 
213
214
  #
214
- # Returns the specified column(s) as an array of values.
215
+ # Returns the column(s) you've SELECT as an array of values.
215
216
  #
216
- # If more than one column is given, the result will be an array of arrays.
217
+ # If you're selecting multiple columns, you'll get back an array of arrays.
218
+ # Otherwise you'll get an array of the single column's values.
217
219
  #
218
- # @param cols [Array] one or more column names as Symbols or Strings. Also accepts SQL functions, e.g. "LENGTH(name)".
220
+ # @param *args DEPRECATED
219
221
  # @return [Array]
220
222
  #
221
- def pluck(*cols)
223
+ def pluck(*args)
224
+ $stderr.puts "OccamsRecord: passing arguments to OccamsRecord.sql(\"...\").pluck is deprecated and will be removed in a future version. Called from #{caller[0]}" if args.any?
225
+
222
226
  _escaped_sql = escaped_sql
223
227
  @query_logger << _escaped_sql if @query_logger
224
- result = if measure?
225
- record_start_time!
226
- measure!(table_name, _escaped_sql) {
227
- conn.exec_query _escaped_sql
228
- }
229
- else
230
- conn.exec_query _escaped_sql
231
- end
232
- pluck_results result, cols, model: @eager_loaders.model
228
+ result =
229
+ if measure?
230
+ record_start_time!
231
+ measure!(table_name, _escaped_sql) {
232
+ conn.exec_query _escaped_sql
233
+ }
234
+ else
235
+ conn.exec_query _escaped_sql
236
+ end
237
+ pluck_results(result, model: @eager_loaders.model)
233
238
  end
234
239
 
235
240
  private
@@ -237,14 +242,23 @@ module OccamsRecord
237
242
  # Returns the SQL as a String with all variables escaped
238
243
  def escaped_sql
239
244
  return sql if binds.empty?
240
- sql % binds.reduce({}) { |a, (col, val)|
241
- a[col.to_sym] = if val.is_a? Array
242
- val.map { |x| conn.quote x }.join(', ')
243
- else
244
- conn.quote val
245
- end
246
- a
247
- }
245
+ escaped_binds =
246
+ if binds.is_a? Array
247
+ binds.map { |val| quote val }
248
+ else
249
+ binds.each_with_object({}) { |(col, val), acc|
250
+ acc[col.to_sym] = quote val
251
+ }
252
+ end
253
+ sql % escaped_binds
254
+ end
255
+
256
+ def quote(val)
257
+ if val.is_a? Array
258
+ val.map { |x| conn.quote x }.join(', ')
259
+ else
260
+ conn.quote val
261
+ end
248
262
  end
249
263
 
250
264
  def table_name
@@ -28,9 +28,10 @@ module OccamsRecord
28
28
  self.table_name = model ? model.table_name : nil
29
29
  self.eager_loader_trace = tracer
30
30
  self.active_record_fallback = active_record_fallback
31
- self.primary_key = if model&.primary_key and (pkey = model.primary_key.to_s) and columns.include?(pkey)
32
- pkey
33
- end
31
+ self.primary_key =
32
+ if model&.primary_key and (pkey = model.primary_key.to_s) and columns.include?(pkey)
33
+ pkey
34
+ end
34
35
 
35
36
  # Build getters & setters for associations. (We need setters b/c they're set AFTER the row is initialized
36
37
  attr_accessor(*association_names)
@@ -42,7 +43,7 @@ module OccamsRecord
42
43
 
43
44
  if caster
44
45
  define_method(col) {
45
- @cast_values[idx] = caster.(@raw_values[idx]) if @cast_values[idx].nil?
46
+ @cast_values[idx] = caster.(@raw_values[idx]) unless @cast_values.has_key?(idx)
46
47
  @cast_values[idx]
47
48
  }
48
49
  else
@@ -51,7 +52,7 @@ module OccamsRecord
51
52
  }
52
53
  end
53
54
 
54
- define_method("#{col}?") { !!send(col) }
55
+ define_method("#{col}?") { send(col).present? }
55
56
  end
56
57
  end
57
58
  end
@@ -69,21 +69,20 @@ module OccamsRecord
69
69
  # @return [Hash] a Hash with String or Symbol keys
70
70
  #
71
71
  def to_h(symbolize_names: false, recursive: false)
72
- hash = self.class.columns.reduce({}) { |a, col_name|
72
+ hash = self.class.columns.each_with_object({}) { |col_name, acc|
73
73
  key = symbolize_names ? col_name.to_sym : col_name
74
- a[key] = send col_name
75
- a
74
+ acc[key] = send col_name
76
75
  }
77
76
 
78
- recursive ? self.class.associations.reduce(hash) { |a, assoc_name|
77
+ recursive ? self.class.associations.each_with_object(hash) { |assoc_name, acc|
79
78
  key = symbolize_names ? assoc_name.to_sym : assoc_name
80
79
  assoc = send assoc_name
81
- a[key] = if assoc.is_a? Array
82
- assoc.map { |x| x.to_h(symbolize_names: symbolize_names, recursive: true) }
83
- elsif assoc
84
- assoc.to_h(symbolize_names: symbolize_names, recursive: true)
85
- end
86
- a
80
+ acc[key] =
81
+ if assoc.is_a? Array
82
+ assoc.map { |x| x.to_h(symbolize_names: symbolize_names, recursive: true) }
83
+ elsif assoc
84
+ assoc.to_h(symbolize_names: symbolize_names, recursive: true)
85
+ end
87
86
  } : hash
88
87
  end
89
88
 
@@ -1,6 +1,6 @@
1
1
  module OccamsRecord
2
- # @private
3
2
  module TypeCaster
3
+ # @private
4
4
  CASTER =
5
5
  case ActiveRecord::VERSION::MAJOR
6
6
  when 4 then :type_cast_from_database
@@ -9,9 +9,13 @@ module OccamsRecord
9
9
  end
10
10
 
11
11
  #
12
- # @param column_names [Array<String>] the column names in the result set. The order MUST match the order returned by the query.
13
- # @param column_types [Hash] Column name => type from an ActiveRecord::Result
14
- # @param model [ActiveRecord::Base] the AR model representing the table (it holds column & type info).
12
+ # Returns a Hash containing type converters (a Proc) for each column. The Proc's accept a value and return a converted value, mapping enum values from the model if necessary.
13
+ #
14
+ # NOTE Some columns may have no Proc (particularly if you're using SQLite and running a raw SQL query).
15
+ #
16
+ # @param column_names [Array<String>] the column names in the result set (ActiveRecord::Result#columns). The order MUST match the order returned by the query.
17
+ # @param column_types [Hash] Column name => type (ActiveRecord::Result#column_types)
18
+ # @param model [ActiveRecord::Base] the AR model representing the table (it holds column & type info as well as enums).
15
19
  # @return [Hash<Proc>] a Hash of casting Proc's keyed by column
16
20
  #
17
21
  def self.generate(column_names, column_types, model: nil)
@@ -42,8 +46,6 @@ module OccamsRecord
42
46
  end
43
47
  when :datetime
44
48
  ->(val) { type.send(CASTER, val)&.in_time_zone }
45
- when :boolean
46
- ->(val) { type.send(CASTER, val) }
47
49
  else
48
50
  if enum
49
51
  ->(val) {
@@ -3,5 +3,5 @@
3
3
  #
4
4
  module OccamsRecord
5
5
  # @private
6
- VERSION = "1.8.1".freeze
6
+ VERSION = "1.9.0".freeze
7
7
  end
data/lib/occams-record.rb CHANGED
@@ -14,6 +14,11 @@ require 'occams-record/batches/offset_limit/scoped'
14
14
  require 'occams-record/batches/offset_limit/raw_query'
15
15
  require 'occams-record/batches/cursor_helpers'
16
16
 
17
+ require 'occams-record/binds_converter'
18
+ require 'occams-record/binds_converter/abstract'
19
+ require 'occams-record/binds_converter/named'
20
+ require 'occams-record/binds_converter/positional'
21
+
17
22
  require 'occams-record/query'
18
23
  require 'occams-record/raw_query'
19
24
 
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.8.1
4
+ version: 1.9.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: 2023-07-02 00:00:00.000000000 Z
11
+ date: 2023-07-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -42,6 +42,10 @@ files:
42
42
  - lib/occams-record/batches/cursor_helpers.rb
43
43
  - lib/occams-record/batches/offset_limit/raw_query.rb
44
44
  - lib/occams-record/batches/offset_limit/scoped.rb
45
+ - lib/occams-record/binds_converter.rb
46
+ - lib/occams-record/binds_converter/abstract.rb
47
+ - lib/occams-record/binds_converter/named.rb
48
+ - lib/occams-record/binds_converter/positional.rb
45
49
  - lib/occams-record/cursor.rb
46
50
  - lib/occams-record/eager_loaders/ad_hoc_base.rb
47
51
  - lib/occams-record/eager_loaders/ad_hoc_many.rb