module Cassie::Model::ClassMethods

Public Instance Methods

batch(options = nil) { || ... } click to toggle source

All insert, update, and delete calls within the block will be sent as a single batch to Cassandra. The consistency level will default to the write consistency level if it's been set.

# File lib/cassie/model.rb, line 337
def batch(options = nil)
  options = consistency_options(write_consistency, options)
  connection.batch(options) do
    yield
  end
end
column(name, type, as: nil) click to toggle source

Define a column name and type from the table. Columns must be defined in order to be used. This method will handle defining the getter and setter methods as well.

The type specified must be a valid CQL data type.

Because Cassandra stores column names with each row it is beneficial to use very short column names. You can specify the :as option to define a more human readable version. This will add the appropriate getter and setter methods as well as allow you to use the alias name in the methods that take an attributes hash.

Defining a column will also define getter and setter methods for both the column name and the alias name (if specified). So `column :i, :int, as: :id` will define the methods `i`, `i=`, `id`, and `id=`.

If you define a counter column then it will define methods for `increment_i!` and `decrement_i!` which take an optional amount argument. Note that if you have a counter column you cannot have any other non-primary key columns and you cannot call create, update, or save and must use the increment and decrement commands.

# File lib/cassie/model.rb, line 102
def column(name, type, as: nil)
  name = name.to_sym
  type_class = nil
  type_name = type.to_s.downcase.classify
  # Backward compatibility with older driver versions.
  type_name = "Text" if type_name == "Varchar"
  begin
    type_class = "Cassandra::Types::#{type_name}".constantize
  rescue NameError
    raise ArgumentError.new("#{type.inspect} is not an allowed Cassandra type")
  end

  self._columns = _columns.merge(name => type_class)
  self._column_aliases = _column_aliases.merge(name => name)

  aliased = (as && as.to_s != name.to_s)
  if aliased
    self._column_aliases = _column_aliases.merge(as => name)
  end

  if type.to_s == "counter"
    self._counter_table = true

    define_method(name) { instance_variable_get(:"@#{name}") || 0 }
    define_method("#{name}=") { |value| instance_variable_set(:"@#{name}", value.to_i) }

    define_method("increment_#{name}!") { |amount = 1, ttl: nil| send(:adjust_counter!, name, amount, ttl: ttl) }
    define_method("decrement_#{name}!") { |amount = 1, ttl: nil| send(:adjust_counter!, name, -amount, ttl: ttl) }
    if aliased
      define_method(as) { send(name) }
      define_method("increment_#{as}!") { |amount = 1, ttl: nil| send("increment_#{name}!", amount, ttl: ttl) }
      define_method("decrement_#{as}!") { |amount = 1, ttl: nil| send("increment_#{name}!", amount, ttl: ttl) }
    end
  else
    attr_reader name
    define_method("#{name}=") { |value| instance_variable_set(:"@#{name}", self.class.send(:coerce, value, type_class)) }
    attr_reader name
    if aliased
      define_method(as) { send(name) }
      define_method("#{as}=") { |value| send("#{name}=", value) }
    end
  end
end
column_name(name_or_alias) click to toggle source

Returns the internal column name after resolving any aliases.

# File lib/cassie/model.rb, line 152
def column_name(name_or_alias)
  _column_aliases[name_or_alias] || name_or_alias
end
column_names() click to toggle source

Returns an array of the defined column names as symbols.

# File lib/cassie/model.rb, line 147
def column_names
  _columns.keys
end
connection() click to toggle source

Returns the Cassie instance used to communicate with Cassandra.

# File lib/cassie/model.rb, line 345
def connection
  Cassie.instance
end
count(where = nil) click to toggle source

Return the count of rows in the table. If the where argument is specified then it will be added as the WHERE clause.

# File lib/cassie/model.rb, line 287
def count(where = nil)
  options = nil
  if where.is_a?(Hash) && where.include?(:options)
    where = where.dup
    options = where.delete(:options)
  end

  cql = "SELECT COUNT(*) FROM #{full_table_name}"
  values = nil

  if where
    where_clause, values = cql_where_clause(where)
    cql += " WHERE #{where_clause}"
  else
    connection.prepare(cql)
  end

  results = connection.find(cql, values, consistency_options(read_consistency, options))
  results.rows.first["count"]
end
create(attributes) click to toggle source

Returns a newly created record. If the record is not valid then it won't be persisted.

# File lib/cassie/model.rb, line 310
def create(attributes)
  record = new(attributes)
  record.save
  record
end
create!(attributes) click to toggle source

Returns a newly created record or raises an ActiveRecord::RecordInvalid error if the record is not valid.

# File lib/cassie/model.rb, line 318
def create!(attributes)
  record = new(attributes)
  record.save!
  record
end
delete_all(key_hash) click to toggle source

Delete all rows from the table that match the key hash. This method bypasses any destroy callbacks defined on the model.

# File lib/cassie/model.rb, line 326
def delete_all(key_hash)
  cleanup_up_hash = {}
  key_hash.each do |name, value|
    cleanup_up_hash[column_name(name)] = value
  end
  connection.delete(full_table_name, cleanup_up_hash, consistency: write_consistency)
end
find(where) click to toggle source

Find a single record that matches the where argument.

# File lib/cassie/model.rb, line 268
def find(where)
  options = nil
  if where.is_a?(Hash) && where.include?(:options)
    where = where.dup
    options = where.delete(:options)
  end
  find_all(where: where, limit: 1, options: options).first
end
find!(where) click to toggle source

Find a single record that matches the where argument or raise an ActiveRecord::RecordNotFound error if none is found.

# File lib/cassie/model.rb, line 279
def find!(where)
  record = find(where)
  raise Cassie::RecordNotFound unless record
  record
end
find_all(where:, select: nil, order: nil, limit: nil, options: nil) { |record| ... } click to toggle source

Find all records.

The where argument can be a Hash, Array, or String WHERE clause to filter the rows returned. It is required so that you don't accidentally release code that returns all rows. If you really want to select all rows from a table you can specify the value :all.

The select argument can be used to limit which columns are returned and should be passed as an array of column names which can include aliases.

The order argument is a CQL fragment indicating the order. Note that Cassandra will only allow ordering by rows in the primary key.

The limit argument specifies how many rows to return.

You can provide a block to this method in which case it will yield each record as it is foundto the block instead of returning them.

# File lib/cassie/model.rb, line 218
def find_all(where:, select: nil, order: nil, limit: nil, options: nil)
  start_time = Time.now
  columns = (select ? Array(select).collect { |c| column_name(c) } : column_names)
  cql = "SELECT #{columns.join(", ")} FROM #{full_table_name}"
  values = nil

  raise ArgumentError.new("Where clause cannot be blank. Pass :all to find all records.") if where.blank?
  if where && where != :all
    where_clause, values = cql_where_clause(where)
  else
    values = []
  end
  cql += " WHERE #{where_clause}" if where_clause

  if order
    cql += " ORDER BY #{order}"
  end

  if limit
    cql += " LIMIT ?"
    values << Integer(limit)
  end

  results = connection.find(cql, values, consistency_options(read_consistency, options))
  records = [] unless block_given?
  row_count = 0
  loop do
    row_count += results.size
    results.each do |row|
      record = new(row)
      record.instance_variable_set(:@persisted, true)
      if block_given?
        yield record
      else
        records << record
      end
    end
    break if results.last_page?
    results = results.next_page
  end

  if find_subscribers && !find_subscribers.empty?
    payload = FindMessage.new(cql, values, options, Time.now - start_time, row_count)
    find_subscribers.each { |subscriber| subscriber.call(payload) }
  end

  records
end
full_table_name() click to toggle source

Return the full table name including the keyspace.

# File lib/cassie/model.rb, line 193
def full_table_name
  if _keyspace
    "#{keyspace}.#{table_name}"
  else
    table_name
  end
end
keyspace() click to toggle source

Return the keyspace name where the table is located.

# File lib/cassie/model.rb, line 188
def keyspace
  connection.config.keyspace(_keyspace)
end
keyspace=(name) click to toggle source

Set the keyspace for the table. The name should be an abstract keyspace name that is mapped to an actual keyspace name in the configuration. If the name provided is not mapped in the configuration, then the raw value will be used.

# File lib/cassie/model.rb, line 183
def keyspace=(name)
  self._keyspace = name.to_s
end
offset_to_id(key, offset, order: nil, batch_size: 1000, min: nil, max: nil) click to toggle source

Since Cassandra doesn't support offset we need to find the order key of record at the specified the offset.

The key is a Hash describing the primary keys to search minus the last column defined for the primary key. This column is assumed to be an ordering key. If it isn't, this method will fail.

The order argument can be used to specify an order for the ordering key (:asc or :desc). It will default to the natural order of the last ordering key as defined by the ordering_key method.

The min and max can be used to limit the offset calculation to a range of values (exclusive).

# File lib/cassie/model.rb, line 360
def offset_to_id(key, offset, order: nil, batch_size: 1000, min: nil, max: nil)
  ordering_key = primary_key.last
  cluster_order = _ordering_keys[ordering_key] || :asc
  order ||= cluster_order
  order_cql = "#{ordering_key} #{order}" unless order == cluster_order

  from = (order == :desc ? max : min)
  to = (order == :desc ? min : max)
  loop do
    limit = (offset > batch_size ? batch_size : offset + 1)
    conditions_cql = []
    conditions = []
    if from
      conditions_cql << "#{ordering_key} #{order == :desc ? "<" : ">"} ?"
      conditions << from
    end
    if to
      conditions_cql << "#{ordering_key} #{order == :desc ? ">" : "<"} ?"
      conditions << to
    end
    key.each do |name, value|
      conditions_cql << "#{column_name(name)} = ?"
      conditions << value
    end
    conditions.unshift(conditions_cql.join(" AND "))

    results = find_all(select: [ordering_key], where: conditions, limit: limit, order: order_cql)
    last_row = results.last if results.size == limit
    last_id = last_row.send(ordering_key) if last_row

    if last_id.nil?
      return nil
    elsif limit >= offset
      return last_id
    else
      offset -= results.size
      from = last_id
    end
  end
end
ordering_key(name, order) click to toggle source

Define and ordering key for the table. The order attribute should be either :asc or :desc

# File lib/cassie/model.rb, line 174
def ordering_key(name, order)
  order = order.to_sym
  raise ArgumentError.new("order must be either :asc or :desc") unless order == :asc || order == :desc
  _ordering_keys[name.to_sym] = order
end
primary_key() click to toggle source

Return an array of column names for the table primary key.

# File lib/cassie/model.rb, line 169
def primary_key
  _primary_key
end
primary_key=(value) click to toggle source

Set the primary key for the table. The value should be set as an array with the clustering key first.

# File lib/cassie/model.rb, line 158
def primary_key=(value)
  self._primary_key = Array(value).map { |column|
    if column.is_a?(Array)
      column.map(&:to_sym)
    else
      column.to_sym
    end
  }.flatten
end

Private Instance Methods

coerce(value, type_class) click to toggle source

Force a value to be the correct Cassandra data type.

# File lib/cassie/model.rb, line 433
def coerce(value, type_class)
  if value.nil?
    nil
  elsif type_class == Cassandra::Types::Timeuuid && value.is_a?(Cassandra::TimeUuid)
    value
  elsif type_class == Cassandra::Types::Uuid
    # Work around for bug in cassandra-driver 2.1.3
    if value.is_a?(Cassandra::Uuid)
      value
    else
      Cassandra::Uuid.new(value)
    end
  elsif type_class == Cassandra::Types::Timestamp && value.is_a?(String)
    Time.parse(value)
  elsif type_class == Cassandra::Types::Inet && value.is_a?(::IPAddr)
    value
  elsif type_class == Cassandra::Types::List
    Array.new(value)
  elsif type_class == Cassandra::Types::Set
    Set.new(value)
  elsif type_class == Cassandra::Types::Map
    Hash[value]
  else
    type_class.new(value)
  end
end
consistency_options(consistency, options) click to toggle source
# File lib/cassie/model.rb, line 460
def consistency_options(consistency, options)
  if consistency
    if options
      options = options.merge(consistency: consistency) if options[:consistency].nil?
      options
    else
      {consistency: consistency}
    end
  else
    options
  end
end
cql_where_clause(where) click to toggle source

Turn a hash of column value, array of [cql, value] or a CQL string into a CQL where clause. Returns the values pulled out in an array for making a prepared statement.

# File lib/cassie/model.rb, line 406
def cql_where_clause(where)
  case where
  when Hash
    cql = []
    values = []
    where.each do |column, value|
      col_name = column_name(column)
      if value.is_a?(Array)
        q = "?#{",?" * (value.size - 1)}"
        cql << "#{col_name} IN (#{q})"
        values.concat(value)
      else
        cql << "#{col_name} = ?"
        values << coerce(value, _columns[col_name])
      end
    end
    [cql.join(" AND "), values]
  when Array
    [where.first, where[1, where.size]]
  when String
    [where, []]
  else
    raise ArgumentError.new("invalid CQL where clause #{where}")
  end
end