Class: Cequel::Record::RecordSet

Inherits:
SimpleDelegator
  • Object
show all
Extended by:
Util::HashAccessors, Forwardable
Includes:
BulkWrites, Enumerable
Defined in:
lib/cequel/record/record_set.rb

Overview

This class represents a subset of records from a particular table. Record sets encapsulate a CQL query, and are constructed using a chained builder interface.

The primary mechanism for specifying which rows should be returned by a CQL query is by specifying values for one or more primary key columns. A record set acts like a deeply-nested hash, where each primary key column is a level of nesting. The #[] method is used to narrow the result set by successive primary key values.

If #[] is used successively to specify all of the columns of a primary key, the result will be a single Cequel::Record or a LazyRecordCollection, depending on whether multiple values were specified for one of the key columns. In either case, the record instances will be unloaded.

Certain methods have behavior that is dependent on which primary keys have been specified using #[]. In many methods, such as #[], #values_at, #before, #after, #from, #upto, and #in, the *first unscoped primary key column* serves as implicit context for the method: the value passed to those methods is an exact or bounding value for that column.

CQL does not allow ordering by arbitrary columns; the ordering of a table is determined by its clustering column(s). You read records in reverse clustering order using #reverse.

Record sets are enumerable collections; under the hood, results are paginated. This pagination can be made explicit using #find_in_batches. RecordSets do not store their records in memory; each time #each or an ‘Enumerable` method is called, the database is queried.

All ‘RecordSet` methods are also exposed directly on Cequel::Record classes. So, for instance, `Post.limit(10)` or `Post.select(:id, :title)` work as expected.

Conversely, you may call any class method of a record class on a record set that targets that class. The class method will be executed in the context of the record set that the method is called on. See below for examples.

Examples:

Model class used for further examples

class Post
  include Cequel::Record

  belongs_to :blog # defines key :blog_subdomain
  key :id, :timeuuid, auto: true

  column :title, :text
  column :author_id, :integer, index: true

  def self.for_author(author)
    where(:author_id, author.id)
  end
end

A record set scoped to all posts

Post.all # returns a record set with no scope restrictions

The first ten posts

# returns a ten-element array of loaded posts
Post.first(10)

# returns a record set scoped to yield the first 10 posts
Post.limit(10)

The posts in the “cassandra” blog

# returns a record set where blog_subdomain = "cassandra"
Post['cassandra']

The post in the “cassandra” blog with id ‘params`

# returns an unloaded Post instance
Post['cassandra'][params[:id]]

The posts in the “cassandra” blog with ids ‘id1, id2`

# returns a LazyRecordCollection containing two unloaded Post instances
Post['cassandra'].values_at('id1', 'id2')

The posts in the “cassandra” blog in descending order of id

# returns a LazyRecordCollection where blog_subdomain="cassandra" in
# descending order of creation
Post['cassandra'].reverse

The posts in the “cassandra” blog created in the last week

# returns a LazyRecordCollection where blog_subdomain="cassandra" and
the timestamp encoded in the uuid is in the last week. This only works
for timeuuid clustering columns
Post['cassandra'].reverse.after(1.week.ago)

10 posts by a given author

# Scoped to 10 posts where author_id=author.id. Results will not be in
# a defined order because the partition key is not specified
Post.for_author(author).limit(10)

See Also:

Since:

  • 1.0.0

Direct Known Subclasses

AssociationCollection

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Util::HashAccessors

hattr_accessor, hattr_inquirer, hattr_reader, hattr_writer

Methods included from BulkWrites

#destroy_all, #update_all

Constructor Details

#initialize(target_class, attributes = {}) ⇒ RecordSet

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of RecordSet.

Parameters:

  • target_class (Class)

    the Record class that this collection yields instances of

  • attributes (Hash) (defaults to: {})

    initial scoping attributes

Since:

  • 1.0.0



123
124
125
126
127
# File 'lib/cequel/record/record_set.rb', line 123

def initialize(target_class, attributes = {})
  attributes = self.class.default_attributes.merge!(attributes)
  @target_class, @attributes = target_class, attributes
  super(target_class)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object (private)

Since:

  • 1.0.0



780
781
782
# File 'lib/cequel/record/record_set.rb', line 780

def method_missing(method, *args, &block)
  target_class.with_scope(self) { super }
end

Instance Attribute Details

#target_classClass (readonly)

Returns the Record class that this collection yields instances of.

Returns:

  • (Class)

    the Record class that this collection yields instances of

Since:

  • 1.0.0



114
115
116
# File 'lib/cequel/record/record_set.rb', line 114

def target_class
  @target_class
end

Class Method Details

.default_attributesObject

Since:

  • 1.0.0



108
109
110
# File 'lib/cequel/record/record_set.rb', line 108

def self.default_attributes
  {scoped_key_values: [], select_columns: []}
end

Instance Method Details

#==(other) ⇒ Object

Since:

  • 1.0.0



627
628
629
# File 'lib/cequel/record/record_set.rb', line 627

def ==(other)
  entries == other.to_a
end

#[](*primary_key_value) ⇒ RecordSet, Record Also known as: /

Note:

Accepting multiple arguments is deprecated behavior. Use #values_at instead.

Restrict this record set to a given value for the next unscoped primary key column

Record sets can be thought of like deeply-nested hashes, where each primary key column is a level of nesting. For instance, if a table consists of a single record with primary key ‘(blog_subdomain, permalink) = (“cassandra”, “cequel”)`, the record set can be thought of like so:

“‘ruby {

"cassandra" => {
  "cequel" => #<Post blog_subdomain: "cassandra",
                     permalink: "cequel", title: "Cequel">
}

} “‘

If ‘[]` is invoked enough times to specify all primary keys, then an unloaded `Record` instance is returned; this is the same behavior you would expect from a `Hash`. If only some subset of the primary keys have been specified, the result is still a `RecordSet`.

Examples:

Partially specified primary key

Post['cequel'] # returns a RecordSet

Fully specified primary key

Post['cequel']['cassandra'] # returns an unloaded Record

Parameters:

  • primary_key_value

    value for the first unscoped primary key

Returns:

  • (RecordSet)

    record set with primary key filter applied, if not all primary keys are specified

  • (Record)

    unloaded record, if all primary keys are specified

Since:

  • 1.0.0



268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/cequel/record/record_set.rb', line 268

def [](*primary_key_value)
  if primary_key_value.many?
    warn "Calling #[] with multiple arguments is deprecated. Use " \
         "#values_at"
    return values_at(*primary_key_value)
  end

  primary_key_value = cast_range_key(primary_key_value.first)

  scope_and_resolve do |attributes|
    attributes[:scoped_key_values] << primary_key_value
  end
end

#after(start_key) ⇒ RecordSet

Restrict records to ones whose value in the first unscoped primary key column are strictly greater than the given start_key.

Parameters:

  • start_key

    the exclusive lower bound for the key column

Returns:

  • (RecordSet)

    record set with lower bound applied

See Also:

Since:

  • 1.0.0



363
364
365
# File 'lib/cequel/record/record_set.rb', line 363

def after(start_key)
  scoped(lower_bound: bound(true, false, start_key))
end

#allRecordSet

Returns self.

Returns:

Since:

  • 1.0.0



132
133
134
# File 'lib/cequel/record/record_set.rb', line 132

def all
  self
end

#assert_fully_specified!Object

Raises:

  • (ArgumentError)

Since:

  • 1.0.0



618
619
620
621
622
# File 'lib/cequel/record/record_set.rb', line 618

def assert_fully_specified!
  raise ArgumentError,
        "Missing key component(s) " \
        "#{unscoped_key_names.join(', ')}"
end

#at(*scoped_key_values) ⇒ RecordSet, Record

Deprecated.

Use #[] instead

Scope to values for one or more primary key columns

Parameters:

  • scoped_key_values

    values for primary key columns

Returns:

  • (RecordSet)

    record set with primary key filter applied, if not all primary keys are specified

  • (Record)

    unloaded record, if all primary keys are specified

Since:

  • 1.0.0



225
226
227
228
# File 'lib/cequel/record/record_set.rb', line 225

def at(*scoped_key_values)
  warn "`at` is deprecated. Use `[]` instead"
  traverse(*scoped_key_values)
end

#before(end_key) ⇒ RecordSet

Restrict records to ones whose value in the first unscoped primary key column are strictly less than the given end_key.

Parameters:

  • end_key

    the exclusive upper bound for the key column

Returns:

  • (RecordSet)

    record set with upper bound applied

See Also:

Since:

  • 1.0.0



376
377
378
# File 'lib/cequel/record/record_set.rb', line 376

def before(end_key)
  scoped(upper_bound: bound(false, false, end_key))
end

#countInteger Also known as: length, size

Returns the total number of records in this record set.

Returns:

  • (Integer)

    the total number of records in this record set

Since:

  • 1.0.0



490
491
492
# File 'lib/cequel/record/record_set.rb', line 490

def count
  data_set.count
end

#data_setCequel::Metal::DataSet

Returns the data set underlying this record set.

Returns:

Since:

  • 1.0.0



596
597
598
# File 'lib/cequel/record/record_set.rb', line 596

def data_set
  @data_set ||= construct_data_set
end

#delete_allvoid

This method returns an undefined value.

Delete all matched records without executing callbacks



609
610
611
612
613
614
615
# File 'lib/cequel/record/record_set.rb', line 609

def delete_all
  if partition_specified?
    data_set.delete
  else
    super
  end
end

#each {|record| ... } ⇒ Enumerator, void

Enumerate over the records in this record set

Yield Parameters:

  • record (Record)

    each successive record in the record set

Returns:

  • (Enumerator)

    if no block given

  • (void)

See Also:

Since:

  • 1.0.0



505
506
507
# File 'lib/cequel/record/record_set.rb', line 505

def each(&block)
  find_each(&block)
end

#find(*keys) ⇒ Record, LazyRecordCollection

Return a loaded Record or collection of loaded Records with the specified primary key values

Multiple arguments are mapped onto unscoped key columns. To specify multiple values for a given key column, use an array.

Examples:

One record with one-column primary key

# find the blog with subdomain 'cassandra'
Blog.find('cassandra')

Multiple records with one-column primary key

# find the blogs with subdomain 'cassandra' and 'postgres'
Blog.find(['cassandra', 'postgres'])

One record with two-column primary key

# find the post instance with blog subdomain 'cassandra' and
# permalink 'my-post'
Post.find('cassandra', 'my-post')

Multiple records with two-column primary key

# find the post instances with blog subdomain cassandra and
# permalinks 'my-post' and 'my-new-post'
Post.find('cassandra', ['my-post', 'my-new-post']

Parameters:

  • scoped_key_values

    one or more values for the final primary key column

Returns:

  • (Record)

    if a single key is specified, return the loaded record at that key

  • (LazyRecordCollection)

    if multiple keys are specified, return a collection of loaded records at those keys

Raises:

  • (RecordNotFound)

    if not all the keys correspond to records in the table

Since:

  • 1.0.0



346
347
348
349
350
351
352
# File 'lib/cequel/record/record_set.rb', line 346

def find(*keys)
  return super if block_given?
  keys = [keys] if almost_fully_specified? && keys.many?
  records = traverse(*keys).assert_fully_specified!.load!
  force_array = keys.any? { |value| value.is_a?(Array) }
  force_array ? Array.wrap(records) : records
end

#find_each(options = {}) {|record| ... } ⇒ Enumerator, void

Enumerate over the records in this record set, with control over how the database is queried

Parameters:

  • options (Options) (defaults to: {})

    options for querying the database

Options Hash (options):

  • :batch_size (Integer) — default: 1000

    the maximum number of rows to return per batch query

Yield Parameters:

  • record (Record)

    each successive record in the record set

Returns:

  • (Enumerator)

    if no block given

  • (void)

See Also:

Since:

  • 1.0.0



520
521
522
523
# File 'lib/cequel/record/record_set.rb', line 520

def find_each(options = {})
  return enum_for(:find_each, options) unless block_given?
  find_each_row(options) { |row| yield target_class.hydrate(row) }
end

#find_each_row(options = {}) {|row| ... } ⇒ Enumerator, void

Enumerate over the row data for each record in this record set, without hydrating an actual Cequel::Record instance. Useful for operations where speed is at a premium.

Parameters:

  • options (Options) (defaults to: {})

    options for querying the database

Options Hash (options):

  • :batch_size (Integer) — default: 1000

    the maximum number of rows to return per batch query

Yield Parameters:

  • row (Hash<Symbol,Object>)

    a hash of column names to values for each row

Returns:

  • (Enumerator)

    if no block given

  • (void)

See Also:

Since:

  • 1.0.0



556
557
558
559
# File 'lib/cequel/record/record_set.rb', line 556

def find_each_row(options = {}, &block)
  return enum_for(:find_each_row, options) unless block
  find_rows_in_batches(options) { |rows| rows.each(&block) }
end

#find_in_batches(options = {}) {|batch| ... } ⇒ Enumerator, void

Enumerate over the records in this record set in batches. Note that the given batch_size controls the maximum number of records that can be returned per query, but no batch is guaranteed to be exactly the given ‘batch_size`

Parameters:

  • options (Options) (defaults to: {})

    options for querying the database

Options Hash (options):

  • :batch_size (Integer) — default: 1000

    the maximum number of rows to return per batch query

Yield Parameters:

  • batch (Array<Record>)

    batch of records

Returns:

  • (Enumerator)

    if no block given

  • (void)

Since:

  • 1.0.0



536
537
538
539
540
541
# File 'lib/cequel/record/record_set.rb', line 536

def find_in_batches(options = {})
  return enum_for(:find_in_batches, options) unless block_given?
  find_rows_in_batches(options) do |rows|
    yield rows.map { |row| target_class.hydrate(row) }
  end
end

#find_rows_in_batches(options = {}) {|batch| ... } ⇒ Enumerator, void

Enumerate over batches of row data for the records in this record set.

Parameters:

  • options (Options) (defaults to: {})

    options for querying the database

Options Hash (options):

  • :batch_size (Integer) — default: 1000

    the maximum number of rows to return per batch query

Yield Parameters:

  • batch (Array<Hash<Symbol,Object>>)

    a batch of rows

Returns:

  • (Enumerator)

    if no block given

  • (void)

See Also:

Since:

  • 1.0.0



573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# File 'lib/cequel/record/record_set.rb', line 573

def find_rows_in_batches(options = {}, &block)
  return find_rows_in_single_batch(options, &block) if row_limit
  options.assert_valid_keys(:batch_size)
  batch_size = options.fetch(:batch_size, 1000)
  batch_record_set = base_record_set = limit(batch_size)
  more_results = true

  while more_results
    rows = batch_record_set.find_rows_in_single_batch
    yield rows if rows.any?
    more_results = rows.length == batch_size
    last_row = rows.last
    if more_results
      find_nested_batches_from(last_row, options, &block)
      batch_record_set = base_record_set.next_batch_from(last_row)
    end
  end
end

#firstRecord #first(count) ⇒ Array

Overloads:

  • #firstRecord

    Returns the first record in this record set.

    Returns:

    • (Record)

      the first record in this record set

  • #first(count) ⇒ Array

    Returns the first ‘count` records of the record set.

    Parameters:

    • count (Integer)

      how many records to return

    Returns:

    • (Array)

      the first ‘count` records of the record set

Returns:

Since:

  • 1.0.0



466
467
468
# File 'lib/cequel/record/record_set.rb', line 466

def first(count = nil)
  count ? limit(count).entries : limit(1).each.first
end

#from(start_key) ⇒ RecordSet

Restrict records to those whose value in the first unscoped primary key column are greater than or equal to the given start key.

Parameters:

  • start_key

    the inclusive lower bound for values in the key column

Returns:

  • (RecordSet)

    record set with the lower bound applied

See Also:

Since:

  • 1.0.0



411
412
413
414
415
416
417
418
# File 'lib/cequel/record/record_set.rb', line 411

def from(start_key)
  unless partition_specified?
    fail IllegalQuery,
         "Can't construct exclusive range on partition key " \
         "#{range_key_name}"
  end
  scoped(lower_bound: bound(true, true, start_key))
end

#in(range) ⇒ RecordSet

Restrict records to those whose value in the first unscoped primary key column are in the given range. Will accept both inclusive ranges (‘1..5`) and end-exclusive ranges (`1…5`). If you need a range with an exclusive start value, use #after, which can be combined with #before or #from to create a range.

Parameters:

  • range (Range)

    range of values for the key column

Returns:

  • (RecordSet)

    record set with range restriction applied

See Also:

Since:

  • 1.0.0



395
396
397
398
399
400
# File 'lib/cequel/record/record_set.rb', line 395

def in(range)
  scoped(
    lower_bound: bound(true, true, range.first),
    upper_bound: bound(false, !range.exclude_end?, range.last)
  )
end

#lastRecord #last(count) ⇒ Array

Overloads:

  • #lastRecord

    Returns the last record in this record set.

    Returns:

    • (Record)

      the last record in this record set

  • #last(count) ⇒ Array

    Returns the last ‘count` records in the record set in ascending order.

    Parameters:

    • count (Integer)

      how many records to return

    Returns:

    • (Array)

      the last ‘count` records in the record set in ascending order

Returns:

Since:

  • 1.0.0



481
482
483
484
485
# File 'lib/cequel/record/record_set.rb', line 481

def last(count = nil)
  reverse.first(count).tap do |results|
    results.reverse! if count
  end
end

#limit(count) ⇒ RecordSet

Restrict the number of records that the RecordSet can contain.

Parameters:

  • count (Integer)

    the maximum number of records to return

Returns:

  • (RecordSet)

    record set with limit applied

See Also:

Since:

  • 1.0.0



175
176
177
# File 'lib/cequel/record/record_set.rb', line 175

def limit(count)
  scoped(row_limit: count)
end

#reverseRecordSet

Note:

This method can only be called on record sets whose partition key columns are fully specified. See #[] for a discussion of partition key scoping.

Reverse the order in which records will be returned from the record set

Returns:

  • (RecordSet)

    record set with order reversed

Since:

  • 1.0.0



447
448
449
450
451
452
453
454
# File 'lib/cequel/record/record_set.rb', line 447

def reverse
  unless partition_specified?
    fail IllegalQuery,
         "Can't reverse without scoping to partition key " \
         "#{range_key_name}"
  end
  scoped(reversed: !reversed?)
end

#scoped_key_attributesHash

Returns map of key column names to the values that have been specified in this record set.

Returns:

  • (Hash)

    map of key column names to the values that have been specified in this record set

Since:

  • 1.0.0



604
605
606
# File 'lib/cequel/record/record_set.rb', line 604

def scoped_key_attributes
  Hash[scoped_key_columns.map { |col| col.name }.zip(scoped_key_values)]
end

#select {|record| ... } ⇒ Array #select(*columns) ⇒ RecordSet

Overloads:

  • #select {|record| ... } ⇒ Array

    Returns records that pass the test given by the block.

    Yield Parameters:

    • record (Record)

      each record in the record set

    Returns:

    • (Array)

      records that pass the test given by the block

    See Also:

  • #select(*columns) ⇒ RecordSet

    Restrict which columns are selected when records are retrieved from the database

    Parameters:

    • columns (Symbol)

      column names

    Returns:

    • (RecordSet)

      record set with the given column selections applied

    See Also:

Returns:

Since:

  • 1.0.0



160
161
162
163
# File 'lib/cequel/record/record_set.rb', line 160

def select(*columns)
  return super if block_given?
  scoped { |attributes| attributes[:select_columns].concat(columns) }
end

#upto(end_key) ⇒ RecordSet

Restrict records to those whose value in the first unscoped primary key column are less than or equal to the given start key.

Parameters:

  • end_key

    the inclusive upper bound for values in the key column

Returns:

  • (RecordSet)

    record set with the upper bound applied

See Also:

Since:

  • 1.0.0



429
430
431
432
433
434
435
436
# File 'lib/cequel/record/record_set.rb', line 429

def upto(end_key)
  unless partition_specified?
    fail IllegalQuery,
         "Can't construct exclusive range on partition key " \
         "#{range_key_name}"
  end
  scoped(upper_bound: bound(false, true, end_key))
end

#values_at(*primary_key_values) ⇒ RecordSet, LazyRecordCollection

Restrict the records in this record set to those containing any of a set of values

Parameters:

  • primary_key_values

    values to match in the next unscoped primary key

Returns:

  • (RecordSet)

    record set with primary key scope applied if not all primary key columns are specified

  • (LazyRecordCollection)

    collection of unloaded records if all primary key columns are specified

Raises:

  • IllegalQuery if the scoped key column is neither the last partition key column nor the last clustering column

See Also:

Since:

  • 1.0.0



298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/cequel/record/record_set.rb', line 298

def values_at(*primary_key_values)
  unless next_unscoped_key_column_valid_for_in_query?
    fail IllegalQuery,
         "Only the last partition key column and the last clustering " \
         "column can match multiple values"
  end

  primary_key_values = primary_key_values.map(&method(:cast_range_key))

  scope_and_resolve do |attributes|
    attributes[:scoped_key_values] << primary_key_values
  end
end

#where(column_name, value) ⇒ RecordSet

Note:

This should only be used with data columns that have secondary indexes. To filter a record set using a primary key, use #[]

Note:

Only one secondary index filter can be used in a given query

Note:

Secondary index filters cannot be mixed with primary key filters

Filter the record set to records containing a given value in an indexed column

Parameters:

  • column_name (Symbol)

    column for filter

  • value

    value to match in given column

Returns:

  • (RecordSet)

    record set with filter applied

Raises:

  • (IllegalQuery)

    if this record set is already filtered by an indexed column

  • (ArgumentError)

    if the specified column is not an data column with a secondary index

Since:

  • 1.0.0



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/cequel/record/record_set.rb', line 196

def where(column_name, value)
  column = target_class.reflect_on_column(column_name)
  if scoped_indexed_column
    fail IllegalQuery,
         "Can't scope by more than one indexed column in the same query"
  end
  unless column
    fail ArgumentError,
         "No column #{column_name} configured for #{target_class.name}"
  end
  unless column.data_column?
    fail ArgumentError,
         "Use the `at` method to restrict scope by primary key"
  end
  unless column.indexed?
    fail ArgumentError,
         "Can't scope by non-indexed column #{column_name}"
  end
  scoped(scoped_indexed_column: {column_name => column.cast(value)})
end