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



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

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



886
887
888
# File 'lib/cequel/record/record_set.rb', line 886

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



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

def target_class
  @target_class
end

Class Method Details

.default_attributesObject

Since:

  • 1.0.0



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

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

Instance Method Details

#==(other) ⇒ Object

Since:

  • 1.0.0



638
639
640
# File 'lib/cequel/record/record_set.rb', line 638

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



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

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



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

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

#allRecordSet

Returns self.

Returns:

Since:

  • 1.0.0



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

def all
  self
end

#assert_fully_specified!Object

Raises:

  • (ArgumentError)

Since:

  • 1.0.0



629
630
631
632
633
# File 'lib/cequel/record/record_set.rb', line 629

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



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

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



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

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

#consistency(consistency) ⇒ RecordSet

Set the consistency at which to read records into the record set.

Parameters:

  • consistency (Symbol)

    consistency for reads

Returns:

  • (RecordSet)

    record set tuned to given consistency

Since:

  • 1.0.0



463
464
465
# File 'lib/cequel/record/record_set.rb', line 463

def consistency(consistency)
  scoped(query_consistency: consistency)
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



501
502
503
# File 'lib/cequel/record/record_set.rb', line 501

def count
  data_set.count
end

#data_setCequel::Metal::DataSet

Returns the data set underlying this record set.

Returns:

Since:

  • 1.0.0



607
608
609
# File 'lib/cequel/record/record_set.rb', line 607

def data_set
  @data_set ||= construct_data_set
end

#delete_allvoid

This method returns an undefined value.

Delete all matched records without executing callbacks



620
621
622
623
624
625
626
# File 'lib/cequel/record/record_set.rb', line 620

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



516
517
518
# File 'lib/cequel/record/record_set.rb', line 516

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



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

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



531
532
533
534
# File 'lib/cequel/record/record_set.rb', line 531

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



567
568
569
570
# File 'lib/cequel/record/record_set.rb', line 567

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



547
548
549
550
551
552
# File 'lib/cequel/record/record_set.rb', line 547

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



584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
# File 'lib/cequel/record/record_set.rb', line 584

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



477
478
479
# File 'lib/cequel/record/record_set.rb', line 477

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



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

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



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

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



492
493
494
495
496
# File 'lib/cequel/record/record_set.rb', line 492

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



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

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



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

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



615
616
617
# File 'lib/cequel/record/record_set.rb', line 615

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



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

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

#to_aryObject

Since:

  • 1.0.0



643
644
645
# File 'lib/cequel/record/record_set.rb', line 643

def to_ary
  entries
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



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

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



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

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 #where(column_values) ⇒ RecordSet

Note:

Filtering on a primary key requires also filtering on all prior primary keys

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

Overloads:

  • #where(column_name, value) ⇒ RecordSet
    Deprecated.

    Returns record set with filter applied.

    Parameters:

    • column_name (Symbol)

      column for filter

    • value

      value to match in given column

    Returns:

    • (RecordSet)

      record set with filter applied

  • #where(column_values) ⇒ RecordSet

    Returns record set with filter applied.

    Parameters:

    • column_values (Hash)

      map of key column names to values

    Returns:

    • (RecordSet)

      record set with filter applied

Raises:

  • (IllegalQuery)

    if applying filter would generate an impossible query

  • (ArgumentError)

    if the specified column is not a column that can be filtered on

Since:

  • 1.0.0



204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/cequel/record/record_set.rb', line 204

def where(*args)
  if args.length == 1
    column_filters = args.first.symbolize_keys
  elsif args.length == 2
    warn "where(column_name, value) is deprecated. Use " \
         "where(column_name => value) instead"
    column_filters = {args.first.to_sym => args.second}
  else
    fail ArgumentError,
         "wrong number of arguments (#{args.length} for 1..2)"
  end
  filter_columns(column_filters)
end