class CassandraStore::Base

Public Class Methods

cast_value(value, type) click to toggle source
# File lib/cassandra_store/base.rb, line 228
def self.cast_value(value, type)
  return nil if value.nil?

  case type
  when :text
    value.to_s
  when :int, :bigint
    Integer(value)
  when :boolean
    return true if [1, "1", "true", true].include?(value)
    return false if [0, "0", "false", false].include?(value)

    raise ArgumentError, "Can't cast '#{value}' to #{type}"
  when :date
    if value.is_a?(String) then Date.parse(value)
    elsif value.respond_to?(:to_date) then value.to_date
    else raise(ArgumentError, "Can't cast '#{value}' to #{type}")
    end
  when :timestamp
    if value.is_a?(String) then Time.parse(value)
    elsif value.respond_to?(:to_time) then value.to_time
    elsif value.is_a?(Numeric) then Time.at(value)
    else raise(ArgumentError, "Can't cast '#{value}' to #{type}")
    end.utc.round(3)
  when :timeuuid
    return value if value.is_a?(Cassandra::TimeUuid)
    return Cassandra::TimeUuid.new(value) if value.is_a?(String) || value.is_a?(Integer)

    raise ArgumentError, "Can't cast '#{value}' to #{type}"
  when :uuid
    return value if value.is_a?(Cassandra::Uuid)
    return Cassandra::Uuid.new(value) if value.is_a?(String) || value.is_a?(Integer)

    raise ArgumentError, "Can't cast '#{value}' to #{type}"
  else
    raise CassandraStore::UnknownType, "Unknown type #{type}"
  end
end
cluster_execute(statement, options = {}) click to toggle source
# File lib/cassandra_store/base.rb, line 304
def self.cluster_execute(statement, options = {})
  logger.debug(statement)

  cluster_pool.with do |connection|
    connection.execute(statement, options)
  end
end
clustering_key_columns() click to toggle source
# File lib/cassandra_store/base.rb, line 198
def self.clustering_key_columns
  columns.select { |_, options| options[:clustering_key] }
end
column(name, type, partition_key: false, clustering_key: false) click to toggle source
# File lib/cassandra_store/base.rb, line 202
def self.column(name, type, partition_key: false, clustering_key: false)
  self.columns = columns.merge(name => { type: type, partition_key: partition_key, clustering_key: clustering_key })

  define_attribute_methods name

  define_method name do
    read_raw_attribute(name)
  end

  define_method :"#{name}=" do |value|
    raise(ArgumentError, "Can't update key '#{name}' for persisted records") if persisted? && (self.class.columns[name][:partition_key] || self.class.columns[name][:clustering_key])

    send :"#{name}_will_change!" unless read_raw_attribute(name) == value

    write_raw_attribute(name, self.class.cast_value(value, type))
  end
end
configure(hosts: ["127.0.0.1"], keyspace:, cluster_settings: {}, replication: {}, durable_writes: true, pool: { size: 5, timeout: 5 }) click to toggle source
# File lib/cassandra_store/base.rb, line 23
def self.configure(hosts: ["127.0.0.1"], keyspace:, cluster_settings: {}, replication: {}, durable_writes: true, pool: { size: 5, timeout: 5 })
  self.keyspace_settings = { name: keyspace, replication: replication, durable_writes: durable_writes }

  self.cluster_pool = ConnectionPool.new(pool) do
    Cassandra.cluster(cluster_settings.merge(hosts: hosts)).connect
  end

  self.connection_pool = ConnectionPool.new(pool) do
    Cassandra.cluster(cluster_settings.merge(hosts: hosts)).connect(keyspace)
  end
end
create(attributes = {}) click to toggle source
# File lib/cassandra_store/base.rb, line 100
def self.create(attributes = {})
  new(attributes).tap(&:save)
end
create!(attributes = {}) click to toggle source
# File lib/cassandra_store/base.rb, line 96
def self.create!(attributes = {})
  new(attributes).tap(&:save!)
end
create_keyspace(name: keyspace_settings[:name], replication: keyspace_settings[:replication], durable_writes: keyspace_settings[:durable_writes], if_not_exists: false) click to toggle source
# File lib/cassandra_store/base.rb, line 39
  def self.create_keyspace(name: keyspace_settings[:name], replication: keyspace_settings[:replication], durable_writes: keyspace_settings[:durable_writes], if_not_exists: false)
    cql = <<~CQL
      CREATE KEYSPACE #{if_not_exists ? "IF NOT EXISTS" : ""} #{quote_keyspace_name(name)}
        WITH REPLICATION = {
          #{replication.map { |key, value| "#{quote_value(key)}: #{quote_value(value)}" }.join(", ")}
        }
        AND DURABLE_WRITES = #{quote_value(durable_writes)}
    CQL

    cluster_execute(cql)
  end
drop_keyspace(name: keyspace_settings[:name], if_exists: false) click to toggle source
# File lib/cassandra_store/base.rb, line 35
def self.drop_keyspace(name: keyspace_settings[:name], if_exists: false)
  cluster_execute("DROP KEYSPACE #{if_exists ? "IF EXISTS" : ""} #{quote_keyspace_name(name)}")
end
execute(statement, options = {}) click to toggle source
# File lib/cassandra_store/base.rb, line 312
def self.execute(statement, options = {})
  logger.debug(statement)

  connection_pool.with do |connection|
    connection.execute(statement, options)
  end
end
execute_batch(statements, options = {}) click to toggle source
# File lib/cassandra_store/base.rb, line 320
def self.execute_batch(statements, options = {})
  statements.each do |statement|
    logger.debug(statement)
  end

  connection_pool.with do |connection|
    batch = connection.send(:"#{options[:batch_type] || "logged"}_batch")

    statements.each do |statement|
      batch.add(statement)
    end

    connection.execute(batch, options.except(:batch_type))
  end
end
key_columns() click to toggle source
# File lib/cassandra_store/base.rb, line 190
def self.key_columns
  partition_key_columns.merge(clustering_key_columns)
end
new(attributes = {}) click to toggle source
# File lib/cassandra_store/base.rb, line 51
def initialize(attributes = {})
  @persisted = false
  @destroyed = false

  assign(attributes)
end
partition_key_columns() click to toggle source
# File lib/cassandra_store/base.rb, line 194
def self.partition_key_columns
  columns.select { |_, options| options[:partition_key] }
end
quote_column_name(column_name) click to toggle source
# File lib/cassandra_store/base.rb, line 275
def self.quote_column_name(column_name)
  raise(ArgumentError, "Invalid column name #{column_name}") if column_name.to_s.include?("\"")

  "\"#{column_name}\""
end
quote_keyspace_name(keyspace_name) click to toggle source
# File lib/cassandra_store/base.rb, line 267
def self.quote_keyspace_name(keyspace_name)
  quote_column_name(keyspace_name)
end
quote_string(string) click to toggle source
# File lib/cassandra_store/base.rb, line 296
def self.quote_string(string)
  "'#{string.gsub("'", "''")}'"
end
quote_table_name(table_name) click to toggle source
# File lib/cassandra_store/base.rb, line 271
def self.quote_table_name(table_name)
  quote_column_name(table_name)
end
quote_value(value) click to toggle source
# File lib/cassandra_store/base.rb, line 281
def self.quote_value(value)
  case value
  when Time, ActiveSupport::TimeWithZone
    (value.to_r * 1000).round.to_s
  when DateTime
    quote_value(value.utc.to_time)
  when Date
    quote_value(value.strftime("%Y-%m-%d"))
  when Numeric, true, false, Cassandra::Uuid
    value.to_s
  else
    quote_string(value.to_s)
  end
end
relation() click to toggle source
# File lib/cassandra_store/base.rb, line 220
def self.relation
  CassandraStore::Relation.new(target: self)
end
statement(template, args = {}) click to toggle source
# File lib/cassandra_store/base.rb, line 336
def self.statement(template, args = {})
  res = template.dup

  args.each do |key, value|
    res.gsub!(":#{key}", quote_value(value))
  end

  res
end
table_name() click to toggle source
# File lib/cassandra_store/base.rb, line 186
def self.table_name
  name.tableize
end
truncate_table() click to toggle source
# File lib/cassandra_store/base.rb, line 300
def self.truncate_table
  execute "TRUNCATE TABLE #{quote_table_name table_name}"
end

Public Instance Methods

==(other) click to toggle source
# File lib/cassandra_store/base.rb, line 58
def ==(other)
  other.instance_of?(self.class) && key_values == other.key_values
end
assign(attributes = {}) click to toggle source
# File lib/cassandra_store/base.rb, line 74
def assign(attributes = {})
  attributes.each do |column, value|
    send(:"#{column}=", value)
  end
end
attributes() click to toggle source
# File lib/cassandra_store/base.rb, line 80
def attributes
  columns.each_with_object({}) do |(name, _), hash|
    hash[name] = read_raw_attribute(name)
  end
end
delete() click to toggle source
# File lib/cassandra_store/base.rb, line 178
def delete
  raise CassandraStore::RecordNotPersisted unless persisted?

  self.class.execute(delete_record_statement)

  true
end
destroy() click to toggle source
# File lib/cassandra_store/base.rb, line 164
def destroy
  raise CassandraStore::RecordNotPersisted unless persisted?

  run_hook :before_destroy

  delete

  destroyed!

  run_hook :after_destroy

  true
end
destroyed!() click to toggle source
# File lib/cassandra_store/base.rb, line 148
def destroyed!
  @destroyed = true
end
destroyed?() click to toggle source
# File lib/cassandra_store/base.rb, line 144
def destroyed?
  !!@destroyed
end
eql?(other) click to toggle source
# File lib/cassandra_store/base.rb, line 62
def eql?(other)
  self == other
end
hash() click to toggle source
# File lib/cassandra_store/base.rb, line 66
def hash
  key_values.hash
end
key_values() click to toggle source
# File lib/cassandra_store/base.rb, line 70
def key_values
  self.class.key_columns.map { |column, _| read_raw_attribute(column) }
end
new_record?() click to toggle source
# File lib/cassandra_store/base.rb, line 140
def new_record?
  !persisted?
end
persisted!() click to toggle source
# File lib/cassandra_store/base.rb, line 136
def persisted!
  @persisted = true
end
persisted?() click to toggle source
# File lib/cassandra_store/base.rb, line 132
def persisted?
  !!@persisted
end
read_raw_attribute(attribute) click to toggle source
# File lib/cassandra_store/base.rb, line 86
def read_raw_attribute(attribute)
  return nil unless instance_variable_defined?(:"@#{attribute}")

  instance_variable_get(:"@#{attribute}")
end
save() click to toggle source
# File lib/cassandra_store/base.rb, line 110
def save
  return false unless valid?

  _save
end
save!() click to toggle source
# File lib/cassandra_store/base.rb, line 104
def save!
  validate!

  _save
end
update(attributes = {}) click to toggle source
# File lib/cassandra_store/base.rb, line 152
def update(attributes = {})
  assign(attributes)

  save
end
update!(attributes = {}) click to toggle source
# File lib/cassandra_store/base.rb, line 158
def update!(attributes = {})
  assign(attributes)

  save!
end
valid?(context = nil) click to toggle source
Calls superclass method
# File lib/cassandra_store/base.rb, line 116
def valid?(context = nil)
  context ||= new_record? ? :create : :update

  run_hook :before_validation

  retval = super(context)

  run_hook :after_validation

  retval
end
validate!(context = nil) click to toggle source
# File lib/cassandra_store/base.rb, line 128
def validate!(context = nil)
  valid?(context) || raise(CassandraStore::RecordInvalid, errors.to_a.join(", "))
end
write_raw_attribute(attribute, value) click to toggle source
# File lib/cassandra_store/base.rb, line 92
def write_raw_attribute(attribute, value)
  instance_variable_set(:"@#{attribute}", value)
end

Protected Instance Methods

generate_timeuuid(time = Time.now) click to toggle source
# File lib/cassandra_store/base.rb, line 422
def generate_timeuuid(time = Time.now)
  @timeuuid_generator ||= Cassandra::TimeUuid::Generator.new
  @timeuuid_generator.at(time)
end
generate_uuid() click to toggle source
# File lib/cassandra_store/base.rb, line 417
def generate_uuid
  @uuid_generator ||= Cassandra::Uuid::Generator.new
  @uuid_generator.uuid
end

Private Instance Methods

_save() click to toggle source
# File lib/cassandra_store/base.rb, line 348
def _save
  run_hook :before_save

  if persisted?
    update_record
  else
    create_record
    persisted!
  end

  run_hook :after_save

  changes_applied

  true
end
create_record() click to toggle source
# File lib/cassandra_store/base.rb, line 365
def create_record
  run_hook :before_create

  self.class.execute(create_record_statement)

  run_hook :after_create
end
create_record_statement() click to toggle source
# File lib/cassandra_store/base.rb, line 373
def create_record_statement
  columns_clause = changes.keys.map { |column_name| self.class.quote_column_name column_name }.join(", ")
  values_clause = changes.values.map(&:last).map { |value| self.class.quote_value value }.join(", ")

  "INSERT INTO #{self.class.quote_table_name self.class.table_name}(#{columns_clause}) VALUES(#{values_clause})"
end
delete_record_statement() click to toggle source
# File lib/cassandra_store/base.rb, line 407
def delete_record_statement
  "DELETE FROM #{self.class.quote_table_name self.class.table_name} #{where_key_clause}"
end
update_record() click to toggle source
# File lib/cassandra_store/base.rb, line 380
def update_record
  run_hook :before_update

  self.class.execute_batch(update_record_statements) unless changes.empty?

  run_hook :after_update
end
update_record_statements() click to toggle source
# File lib/cassandra_store/base.rb, line 388
def update_record_statements
  nils = changes.select { |_, (__, new_value)| new_value.nil? }
  objs = changes.reject { |_, (__, new_value)| new_value.nil? }

  statements = []

  if nils.present?
    statements << "DELETE #{nils.keys.join(", ")} FROM #{self.class.quote_table_name self.class.table_name} #{where_key_clause}"
  end

  if objs.present?
    update_clause = objs.map { |column, (_, new_value)| "#{self.class.quote_column_name column} = #{self.class.quote_value new_value}" }.join(", ")

    statements << "UPDATE #{self.class.quote_table_name self.class.table_name} SET #{update_clause} #{where_key_clause}"
  end

  statements
end
where_key_clause() click to toggle source
# File lib/cassandra_store/base.rb, line 411
def where_key_clause
  "WHERE #{self.class.key_columns.map { |column, _| "#{self.class.quote_column_name column} = #{self.class.quote_value read_raw_attribute(column)}" }.join(" AND ")}"
end