occams-record 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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