Class: OccamsRecord::Query

Inherits:
Object
  • Object
show all
Includes:
Enumerable, Batches::CursorHelpers, EagerLoaders::Builder, Measureable, Pluck
Defined in:
lib/occams-record/query.rb

Overview

Represents a query to be run and eager associations to be loaded. Use OccamsRecord.query to create your queries instead of instantiating objects directly.

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Measureable

#measure

Methods included from EagerLoaders::Builder

#eager_load, #eager_load_many, #eager_load_one, #nest

Methods included from Batches::CursorHelpers

#find_each_with_cursor, #find_in_batches_with_cursor

Constructor Details

#initialize(scope, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil, active_record_fallback: nil) ⇒ Query

Initialize a new query.

Parameters:

  • scope (ActiveRecord::Relation)
  • use (Array<Module>) (defaults to: nil)

    optional Module to include in the result class (single or array)

  • query_logger (Array) (defaults to: nil)

    (optional) an array into which all queries will be inserted for logging/debug purposes

  • eager_loaders (OccamsRecord::EagerLoaders::Context) (defaults to: nil)
  • measurements (Array) (defaults to: nil)
  • active_record_fallback (Symbol) (defaults to: nil)

    If passed, missing methods will be forwarded to an ActiveRecord instance. Options are :lazy (allow lazy loading in the AR record) or :strict (require strict loading)



54
55
56
57
58
59
60
61
# File 'lib/occams-record/query.rb', line 54

def initialize(scope, use: nil, eager_loaders: nil, query_logger: nil, measurements: nil, active_record_fallback: nil)
  @model = scope.klass
  @scope = scope
  @eager_loaders = eager_loaders || EagerLoaders::Context.new(@model)
  @use = use
  @query_logger, @measurements = query_logger, measurements
  @active_record_fallback = active_record_fallback
end

Instance Attribute Details

#modelActiveRecord::Base (readonly)

Returns:

  • (ActiveRecord::Base)


34
35
36
# File 'lib/occams-record/query.rb', line 34

def model
  @model
end

#scopeActiveRecord::Relation (readonly)

Returns scope for building the main SQL query.

Returns:

  • (ActiveRecord::Relation)

    scope for building the main SQL query



36
37
38
# File 'lib/occams-record/query.rb', line 36

def scope
  @scope
end

Instance Method Details

#countInteger

Returns the number of rows that will be returned if the query is run.

Returns:

  • (Integer)


125
126
127
# File 'lib/occams-record/query.rb', line 125

def count
  scope.count
end

#cursor(name: nil, scroll: nil, hold: nil) ⇒ OccamsRecord::Cursor

Returns a cursor you can open and perform operations on. A lower-level alternative to find_each_with_cursor and find_in_batches_with_cursor.

NOTE Postgres only. See the docs for OccamsRecord::Cursor for more details.

Parameters:

  • name (String) (defaults to: nil)

    Specify a name for the cursor (defaults to a random name)

  • scroll (Boolean) (defaults to: nil)

    true = SCROLL, false = NO SCROLL, nil = default behavior of DB

  • hold (Boolean) (defaults to: nil)

    true = WITH HOLD, false = WITHOUT HOLD, nil = default behavior of DB

Returns:



226
227
228
229
230
231
# File 'lib/occams-record/query.rb', line 226

def cursor(name: nil, scroll: nil, hold: nil)
  Cursor.new(model.connection, scope.to_sql,
    name: name, scroll: scroll, hold: hold,
    use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders,
  )
end

#each {|OccamsRecord::Results::Row| ... } ⇒ Object

If you pass a block, each result row will be yielded to it. If you don’t, an Enumerable will be returned.

Yields:

Returns:

  • Enumerable



155
156
157
158
159
160
161
# File 'lib/occams-record/query.rb', line 155

def each
  if block_given?
    to_a.each { |row| yield row }
  else
    to_a.each
  end
end

#find_each(batch_size: 1000, use_transaction: true, append_order_by: nil) {|OccamsRecord::Results::Row| ... } ⇒ Enumerator

Load records in batches of N and yield each record to a block if given. If no block is given, returns an Enumerator.

NOTE Unlike ActiveRecord’s find_each, ORDER BY is respected. The primary key will be appended to the ORDER BY clause to help ensure consistent batches. Additionally, it will be run inside of a transaction.

Parameters:

  • batch_size (Integer) (defaults to: 1000)
  • use_transaction (Boolean) (defaults to: true)

    Ensure it runs inside of a database transaction

  • append_order_by (String) (defaults to: nil)

    Append this column to ORDER BY to ensure consistent results. Defaults to the primary key. Pass false to disable.

Yields:

Returns:

  • (Enumerator)

    will yield each record



177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/occams-record/query.rb', line 177

def find_each(batch_size: 1000, use_transaction: true, append_order_by: nil)
  enum = Enumerator.new { |y|
    find_in_batches(batch_size: 1000, use_transaction: use_transaction, append_order_by: append_order_by).each { |batch|
      batch.each { |record| y.yield record }
    }
  }
  if block_given?
    enum.each { |record| yield record }
  else
    enum
  end
end

#find_in_batches(batch_size: 1000, use_transaction: true, append_order_by: nil) {|OccamsRecord::Results::Row| ... } ⇒ Enumerator

Load records in batches of N and yield each batch to a block if given. If no block is given, returns an Enumerator.

NOTE Unlike ActiveRecord’s find_each, ORDER BY is respected. The primary key will be appended to the ORDER BY clause to help ensure consistent batches. Additionally, it will be run inside of a transaction.

Parameters:

  • batch_size (Integer) (defaults to: 1000)
  • use_transaction (Boolean) (defaults to: true)

    Ensure it runs inside of a database transaction

  • append_order_by (String) (defaults to: nil)

    Append this column to ORDER BY to ensure consistent results. Defaults to the primary key. Pass false to disable.

Yields:

Returns:

  • (Enumerator)

    will yield each batch



204
205
206
207
208
209
210
211
212
213
# File 'lib/occams-record/query.rb', line 204

def find_in_batches(batch_size: 1000, use_transaction: true, append_order_by: nil)
  enum = Batches::OffsetLimit::Scoped
    .new(model, scope, use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders)
    .enum(batch_size: batch_size, use_transaction: use_transaction, append_order_by: append_order_by)
  if block_given?
    enum.each { |batch| yield batch }
  else
    enum
  end
end

#firstOccamsRecord::Results::Row

Run the query with LIMIT 1 and return the first result (which could be nil).



134
135
136
# File 'lib/occams-record/query.rb', line 134

def first
  run { |q| q.limit 1 }.first
end

#first!OccamsRecord::Results::Row

Run the query with LIMIT 1 and return the first result. If nothing is found an OccamsRecord::NotFound exception will be raised.



144
145
146
# File 'lib/occams-record/query.rb', line 144

def first!
  first || raise(OccamsRecord::NotFound.new(model.name, scope.where_values_hash))
end

#pluck(*cols) ⇒ Array

Returns the specified column(s) as an array of values.

If more than one column is given, the result will be an array of arrays.

Parameters:

  • cols (Array)

    one or more column names as Symbols or Strings. Also accepts SQL functions, e.g. “LENGTH(name)”.

Returns:

  • (Array)


241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/occams-record/query.rb', line 241

def pluck(*cols)
  sql = (block_given? ? yield(scope).to_sql : scope).unscope(:select).select(*cols).to_sql
  return [] if sql.blank? # return early in case ActiveRecord::QueryMethods#none was used

  @query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
  result =
    if measure?
      record_start_time!
      measure!(model.table_name, sql) {
        model.connection.exec_query sql
      }
    else
      model.connection.exec_query sql
    end
  pluck_results(result, model: @model)
end

#query {|ActiveRecord::Relation| ... } ⇒ OccamsRecord::Query

Returns a new Query object with a modified scope.

Yields:

  • (ActiveRecord::Relation)

    the current scope which you may modify and return

Returns:



69
70
71
72
# File 'lib/occams-record/query.rb', line 69

def query
  scope = block_given? ? yield(@scope) : @scope
  Query.new(scope, use: @use, eager_loaders: @eager_loaders, query_logger: @query_logger)
end

#run {|ActiveRecord::Relation| ... } ⇒ Array<OccamsRecord::Results::Row> Also known as: to_a

Run the query and return the results.

You may optionally pass a block to modify the query just before it’s run (the change will NOT persist). This is very useful for running paginated queries.

occams = OccamsRecord.query(Widget.all)

# returns first 100 rows
occams.run { |q| q.offset(0).limit(100) }

# returns second 100 rows
occams.run { |q| q.offset(100).limit(100) }

# returns ALL rows
occams.run

Any Enumerable method (e.g. each, to_a, map, reduce, etc.) may be used instead. Additionally, ‘find_each` and `find_in_batches` are available.

Yields:

  • (ActiveRecord::Relation)

    You may use this to return and run a modified relation

Returns:



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/occams-record/query.rb', line 97

def run
  sql = block_given? ? yield(scope).to_sql : scope.to_sql
  return [] if sql.blank? # return early in case ActiveRecord::QueryMethods#none was used

  @query_logger << "#{@eager_loaders.tracer}: #{sql}" if @query_logger
  result =
    if measure?
      record_start_time!
      measure!(model.table_name, sql) {
        model.connection.exec_query sql
      }
    else
      model.connection.exec_query sql
    end
  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)
  rows = result.rows.map { |row| row_class.new row }
  @eager_loaders.run!(rows, query_logger: @query_logger, measurements: @measurements)
  yield_measurements!
  rows
end