Class: OccamsRecord::Cursor

Inherits:
Object
  • Object
show all
Defined in:
lib/occams-record/cursor.rb

Overview

An interface to database cursors. Supported databases:

* PostgreSQL

Constant Summary collapse

SCROLL =
{
  true => "SCROLL",
  false => "NO SCROLL",
  nil => "",
}.freeze
HOLD =
{
  true => "WITH HOLD",
  false => "WITHOUT HOLD",
  nil => "",
}.freeze
DIRECTIONS =
{
  next: "NEXT",
  prior: "PRIOR",
  first: "FIRST",
  last: "LAST",
  absolute: "ABSOLUTE",
  relative: "RELATIVE",
  forward: "FORWARD",
  backward: "BACKWARD",
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(conn, sql, name: nil, scroll: nil, hold: nil, use: nil, query_logger: nil, eager_loaders: nil) ⇒ Cursor

Initializes a new Cursor. NOTE all operations must be performed within a block passed to #open.

While you CAN manually initialize a cursor, it’s more common to get one via OccamsRecord::Query#cursor or OccamsRecord::RawQuery#cursor.

Parameters:

  • conn (ActiveRecord::Connection)
  • sql (String)

    The query to run

  • 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

  • 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)


61
62
63
64
65
66
67
68
# File 'lib/occams-record/cursor.rb', line 61

def initialize(conn, sql, name: nil, scroll: nil, hold: nil, use: nil, query_logger: nil, eager_loaders: nil)
  @conn, @sql = conn, sql
  @scroll = SCROLL.fetch(scroll)
  @hold = HOLD.fetch(hold)
  @use, @query_logger, @eager_loaders = use, query_logger, eager_loaders
  @name = name || "occams_cursor_#{SecureRandom.hex 4}"
  @quoted_name = conn.quote_table_name(@name)
end

Instance Attribute Details

#connActiveRecord::Connection (readonly)

Returns:

  • (ActiveRecord::Connection)


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

def conn
  @conn
end

#nameString (readonly)

Name of the cursor

Returns:

  • (String)


40
41
42
# File 'lib/occams-record/cursor.rb', line 40

def name
  @name
end

#quoted_nameString (readonly)

Name of the cursor (safely SQL-escaped)

Returns:

  • (String)


44
45
46
# File 'lib/occams-record/cursor.rb', line 44

def quoted_name
  @quoted_name
end

Instance Method Details

#each(batch_size: 1000) ⇒ Object

Loads records in batches of N and yields each record to a block (if given). If no block is given, returns an Enumerator.

cursor.open do |c|
  c.each do |record|
    ...
  end
end

Parameters:

  • batch_size (Integer) (defaults to: 1000)

    fetch this many rows at once



104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/occams-record/cursor.rb', line 104

def each(batch_size: 1000)
  enum = Enumerator.new { |y|
    each_batch(batch_size: batch_size).each { |batch|
      batch.each { |record| y.yield record }
    }
  }
  if block_given?
    enum.each { |record| yield record }
  else
    enum
  end
end

#each_batch(batch_size: 1000) ⇒ Object

Loads records in batches of N and yields each batch to a block (if given). If no block is given, returns an Enumerator.

cursor.open do |c|
  c.each_batch do |batch|
    ...
  end
end

Parameters:

  • batch_size (Integer) (defaults to: 1000)

    fetch this many rows at once



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/occams-record/cursor.rb', line 129

def each_batch(batch_size: 1000)
  enum = Enumerator.new { |y|
    out_of_records = false
    until out_of_records
      results = fetch :forward, batch_size
      y.yield results if results.any?
      out_of_records = results.size < batch_size
    end
  }
  if block_given?
    enum.each { |batch| yield batch }
  else
    enum
  end
end

#execute(sql, binds = {}) ⇒ Object

Run an arbitrary command on the cursor. Use ‘binds’ to escape inputs.

cursor.open do |c|
  c.execute("MOVE FORWARD %{num} FOR #{c.quoted_name}", {num: 100})
  ...
end


205
206
207
208
209
# File 'lib/occams-record/cursor.rb', line 205

def execute(sql, binds = {})
  conn.execute(sql % binds.each_with_object({}) { |(key, val), acc|
    acc[key] = conn.quote(val)
  })
end

#fetch(direction, num = nil) ⇒ OccamsRecord::Results::Row

Fetch records in the given direction.

cursor.open do |c|
  c.fetch :forward, 100
  ...
end

Parameters:

  • direction (Symbol)

    :next, :prior, :first, :last, :absolute, :relative, :forward or :backward

  • num (Integer) (defaults to: nil)

    number of rows to fetch (optional for some directions)

Returns:



157
158
159
160
161
162
163
# File 'lib/occams-record/cursor.rb', line 157

def fetch(direction, num = nil)
  query "FETCH %{dir} %{num} FROM %{name}".freeze % {
    dir: DIRECTIONS.fetch(direction),
    num: num&.to_i,
    name: @quoted_name,
  }
end

#move(direction, num = nil) ⇒ Object

Move the cursor the given direction.

cursor.open do |c|
  ...
  c.move :backward, 100
  ...
end

Parameters:

  • direction (Symbol)

    :next, :prior, :first, :last, :absolute, :relative, :forward or :backward

  • num (Integer) (defaults to: nil)

    number of rows to move (optional for some directions)



177
178
179
180
181
182
183
# File 'lib/occams-record/cursor.rb', line 177

def move(direction, num = nil)
  query "MOVE %{dir} %{num} FROM %{name}".freeze % {
    dir: DIRECTIONS.fetch(direction),
    num: num&.to_i,
    name: @quoted_name,
  }
end

#open(use_transaction: true) {|self| ... } ⇒ Object

Declares and opens the cursor, runs the given block (yielding self), and closes the cursor.

cursor.open do |c|
  c.fetch :forward, 100
end

Parameters:

  • use_transaction (Boolean) (defaults to: true)

    When true, ensures it’s wrapped in a transaction

Yields:

  • (self)

Returns:

  • the value returned by the block

Raises:

  • (ArgumentError)


81
82
83
84
85
86
87
88
89
90
# File 'lib/occams-record/cursor.rb', line 81

def open(use_transaction: true)
  raise ArgumentError, "A block is required" unless block_given?
  if use_transaction and conn.open_transactions == 0
    conn.transaction {
      perform { yield self }
    }
  else
    perform { yield self }
  end
end

#query(sql, binds = {}) ⇒ Object

Run an arbitrary query on the cursor. Use ‘binds’ to escape inputs.

cursor.open do |c|
  c.query("FETCH FORWARD %{num} FOR #{c.quoted_name}", {num: 100})
  ...
end


193
194
195
# File 'lib/occams-record/cursor.rb', line 193

def query(sql, binds = {})
  ::OccamsRecord::RawQuery.new(sql, binds, use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders, connection: conn).run
end