cassandra_model 0.9.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +13 -0
- data/README.md +170 -0
- data/lib/cassandra_model.rb +48 -0
- data/lib/cassandra_model/batch_reactor.rb +32 -0
- data/lib/cassandra_model/batch_reactor/future.rb +49 -0
- data/lib/cassandra_model/composite_record.rb +49 -0
- data/lib/cassandra_model/composite_record_static.rb +169 -0
- data/lib/cassandra_model/connection_cache.rb +24 -0
- data/lib/cassandra_model/counter_record.rb +58 -0
- data/lib/cassandra_model/data_inquirer.rb +105 -0
- data/lib/cassandra_model/data_modelling.rb +45 -0
- data/lib/cassandra_model/data_set.rb +84 -0
- data/lib/cassandra_model/displayable_attributes.rb +44 -0
- data/lib/cassandra_model/global_callbacks.rb +39 -0
- data/lib/cassandra_model/logging.rb +8 -0
- data/lib/cassandra_model/meta_columns.rb +162 -0
- data/lib/cassandra_model/meta_table.rb +66 -0
- data/lib/cassandra_model/query_builder.rb +122 -0
- data/lib/cassandra_model/query_helper.rb +44 -0
- data/lib/cassandra_model/query_result.rb +23 -0
- data/lib/cassandra_model/raw_connection.rb +163 -0
- data/lib/cassandra_model/record.rb +551 -0
- data/lib/cassandra_model/result_paginator.rb +37 -0
- data/lib/cassandra_model/rotating_table.rb +49 -0
- data/lib/cassandra_model/single_token_batch.rb +23 -0
- data/lib/cassandra_model/single_token_counter_batch.rb +5 -0
- data/lib/cassandra_model/single_token_logged_batch.rb +5 -0
- data/lib/cassandra_model/single_token_unlogged_batch.rb +5 -0
- data/lib/cassandra_model/table_definition.rb +72 -0
- data/lib/cassandra_model/table_descriptor.rb +49 -0
- data/lib/cassandra_model/table_redux.rb +58 -0
- data/lib/cassandra_model/type_guessing.rb +40 -0
- metadata +133 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module CassandraModel
|
2
|
+
class ConnectionCache
|
3
|
+
MUTEX = Mutex.new
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def build_cache
|
7
|
+
Hash.new do |hash, key|
|
8
|
+
MUTEX.synchronize { hash[key] = RawConnection.new(key) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](key)
|
13
|
+
@@cache[key]
|
14
|
+
end
|
15
|
+
|
16
|
+
def clear
|
17
|
+
@@cache.values.map(&:shutdown)
|
18
|
+
@@cache.clear
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
@@cache = build_cache
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module CassandraModel
|
2
|
+
class CounterRecord < Record
|
3
|
+
|
4
|
+
def increment_async!(options)
|
5
|
+
counter_clause = counter_clause(options)
|
6
|
+
row_key = internal_primary_key.values
|
7
|
+
statement = increment_statement(counter_clause)
|
8
|
+
|
9
|
+
future = if batch_reactor
|
10
|
+
execute_async_in_batch(statement, options.values + row_key)
|
11
|
+
else
|
12
|
+
session.execute_async(statement, *options.values, *row_key, write_query_options)
|
13
|
+
end
|
14
|
+
future.on_success { execute_callback(:record_saved) }
|
15
|
+
future.on_failure do |error|
|
16
|
+
Logging.logger.error("Error incrementing #{self.class}: #{error}")
|
17
|
+
execute_callback(:save_record_failed, error)
|
18
|
+
end.then { self }
|
19
|
+
end
|
20
|
+
|
21
|
+
def increment!(options)
|
22
|
+
increment_async!(options).get
|
23
|
+
end
|
24
|
+
|
25
|
+
def save_async
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def internal_primary_key
|
32
|
+
internal_attributes.slice(*self.class.internal_primary_key)
|
33
|
+
end
|
34
|
+
|
35
|
+
def increment_statement(counter_clause)
|
36
|
+
query = "UPDATE #{self.class.table_name} SET #{counter_clause} WHERE #{update_restriction}"
|
37
|
+
statement(query)
|
38
|
+
end
|
39
|
+
|
40
|
+
def counter_clause(options)
|
41
|
+
options.keys.map { |column| "#{column} = #{column} + ?" }.join(', ')
|
42
|
+
end
|
43
|
+
|
44
|
+
def update_restriction
|
45
|
+
self.class.internal_primary_key.map { |key| "#{key} = ?" }.join(' AND ')
|
46
|
+
end
|
47
|
+
|
48
|
+
class << self
|
49
|
+
def counter_columns
|
50
|
+
table_data.counter_columns ||= columns - (partition_key + clustering_columns)
|
51
|
+
end
|
52
|
+
|
53
|
+
def save_in_batch
|
54
|
+
table_config.batch_type = :counter
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module CassandraModel
|
2
|
+
class DataInquirer
|
3
|
+
include TypeGuessing
|
4
|
+
|
5
|
+
attr_reader :partition_key, :column_defaults, :is_sharding
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@partition_key = Hash.new { |hash, key| hash[key] = :text }
|
9
|
+
@column_defaults = Hash.new { |hash, key| hash[key] = '' }
|
10
|
+
@known_keys = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def knows_about(*columns)
|
14
|
+
columns.each do |column|
|
15
|
+
partition_key[column]
|
16
|
+
guess_data_type(column) if @guess_data_types
|
17
|
+
column_defaults[column]
|
18
|
+
end
|
19
|
+
@known_keys << columns
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def shards_queries
|
24
|
+
@is_sharding = true
|
25
|
+
end
|
26
|
+
|
27
|
+
def composite_rows
|
28
|
+
@known_keys.map do |row|
|
29
|
+
partition_key.keys - row
|
30
|
+
end.reject(&:empty?)
|
31
|
+
end
|
32
|
+
|
33
|
+
def defaults(column)
|
34
|
+
raise "Cannot default unknown column #{column}" unless partition_key.include?(column)
|
35
|
+
ColumnDefault.new(column, self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def change_type_of(column)
|
39
|
+
raise "Cannot retype unknown column #{column}" unless partition_key.include?(column)
|
40
|
+
ColumnType.new(column, self)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
ColumnDefault = Struct.new(:column, :inquirer) do
|
46
|
+
def to(value)
|
47
|
+
default_to(value)
|
48
|
+
|
49
|
+
case value
|
50
|
+
when Integer then
|
51
|
+
retype_to(:int)
|
52
|
+
when Float then
|
53
|
+
retype_to(:double)
|
54
|
+
when Time then
|
55
|
+
retype_to(:timestamp)
|
56
|
+
when Cassandra::Uuid then
|
57
|
+
retype_to(:uuid)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def default_to(value)
|
62
|
+
inquirer.column_defaults[column] = value
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def retype_to(type)
|
68
|
+
ColumnType.new(column, inquirer).retype_to(type)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
ColumnType = Struct.new(:column, :inquirer) do
|
73
|
+
def to(type)
|
74
|
+
retype_to(type)
|
75
|
+
|
76
|
+
case type
|
77
|
+
when :int then
|
78
|
+
default_to(0)
|
79
|
+
when :double then
|
80
|
+
default_to(0.0)
|
81
|
+
when :timestamp then
|
82
|
+
default_to(Time.at(0))
|
83
|
+
when :uuid then
|
84
|
+
default_to(Cassandra::Uuid.new(0))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def retype_to(type)
|
89
|
+
inquirer.partition_key[column] = type
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def default_to(value)
|
95
|
+
ColumnDefault.new(column, inquirer).default_to(value)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def guess_data_type(column)
|
100
|
+
type = guessed_data_type(column, :int)
|
101
|
+
change_type_of(column).to(type)
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module CassandraModel
|
2
|
+
module DataModelling
|
3
|
+
|
4
|
+
def self.extended(base)
|
5
|
+
base.send(:include, CompositeRecord)
|
6
|
+
end
|
7
|
+
|
8
|
+
def model_data
|
9
|
+
inquirer = DataInquirer.new
|
10
|
+
data_set = DataSet.new
|
11
|
+
yield inquirer, data_set
|
12
|
+
|
13
|
+
self.table = if table_sliced?(data_set)
|
14
|
+
rotating_table(data_set, inquirer)
|
15
|
+
else
|
16
|
+
meta_table(table_prefix, inquirer, data_set)
|
17
|
+
end
|
18
|
+
|
19
|
+
generate_composite_defaults_from_inquirer(inquirer)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def table_sliced?(data_set)
|
25
|
+
data_set.data_rotation[:slices]
|
26
|
+
end
|
27
|
+
|
28
|
+
def rotating_table(data_set, inquirer)
|
29
|
+
table_list = data_set.data_rotation[:slices].times.map do |index|
|
30
|
+
meta_table("#{table_prefix}_#{index}", inquirer, data_set)
|
31
|
+
end
|
32
|
+
CassandraModel::RotatingTable.new(table_list, data_set.data_rotation[:frequency])
|
33
|
+
end
|
34
|
+
|
35
|
+
def table_prefix
|
36
|
+
table_config.table_name || generate_table_name
|
37
|
+
end
|
38
|
+
|
39
|
+
def meta_table(table_name, inquirer, data_set)
|
40
|
+
table_definition = CassandraModel::TableDefinition.from_data_model(table_name, inquirer, data_set)
|
41
|
+
CassandraModel::MetaTable.new(table_config.connection_name, table_definition)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module CassandraModel
|
2
|
+
class DataSet
|
3
|
+
include TypeGuessing
|
4
|
+
|
5
|
+
attr_reader :columns, :clustering_columns, :data_rotation
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@columns = Hash.new { |hash, key| hash[key] = :text }
|
9
|
+
@data_rotation = {}
|
10
|
+
@clustering_columns = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def knows_about(*columns)
|
14
|
+
columns.each do |column|
|
15
|
+
if @guess_data_types
|
16
|
+
guess_data_type(column)
|
17
|
+
else
|
18
|
+
self.columns[column]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def counts(*columns)
|
24
|
+
if columns.empty?
|
25
|
+
count_column(:count)
|
26
|
+
else
|
27
|
+
columns.map { |column| count_column(column) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def rotates_storage_across(slices)
|
32
|
+
TableRotation.new(slices, self)
|
33
|
+
end
|
34
|
+
|
35
|
+
def is_defined_by(*columns)
|
36
|
+
knows_about(*columns)
|
37
|
+
@clustering_columns = columns
|
38
|
+
end
|
39
|
+
|
40
|
+
def change_type_of(column)
|
41
|
+
raise "Cannot retype unknown column #{column}" unless columns.include?(column)
|
42
|
+
ColumnType.new(column, self)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def count_column(column)
|
48
|
+
@columns[column] = :counter
|
49
|
+
end
|
50
|
+
|
51
|
+
ColumnType = Struct.new(:column, :data_set) do
|
52
|
+
def to(type)
|
53
|
+
data_set.columns[column] = type
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
TableRotation = Struct.new(:slices, :data_set) do
|
58
|
+
def tables
|
59
|
+
define_table_slicing
|
60
|
+
define_rotation_frequency(1.week)
|
61
|
+
end
|
62
|
+
|
63
|
+
def tables_every(interval)
|
64
|
+
define_table_slicing
|
65
|
+
define_rotation_frequency(interval)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def define_rotation_frequency(frequency)
|
71
|
+
data_set.data_rotation[:frequency] = frequency
|
72
|
+
end
|
73
|
+
|
74
|
+
def define_table_slicing
|
75
|
+
data_set.data_rotation[:slices] = slices
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def guess_data_type(column)
|
80
|
+
columns[column] = guessed_data_type(column, :counter)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module CassandraModel
|
2
|
+
module DisplayableAttributesStatic
|
3
|
+
def display_attributes(*columns)
|
4
|
+
map = (columns.first if columns.first.is_a?(Hash))
|
5
|
+
table_config.display_attributes = map ? map : columns
|
6
|
+
end
|
7
|
+
|
8
|
+
def displayable_attributes
|
9
|
+
table_config.display_attributes
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module DisplayableAttributes
|
14
|
+
def self.included(base)
|
15
|
+
base.extend(DisplayableAttributesStatic)
|
16
|
+
end
|
17
|
+
|
18
|
+
def as_json(*_)
|
19
|
+
if displayable_attributes
|
20
|
+
displayable_attributes.is_a?(Hash) ? mapped_as_json : sliced_displayable_attributes
|
21
|
+
else
|
22
|
+
attributes
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def mapped_as_json
|
29
|
+
sliced_displayable_attributes.inject({}) { |memo, (key, value)| memo.merge!(displayable_attributes[key] => value) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def sliced_displayable_attributes
|
33
|
+
attributes.slice(*displayable_attributes_slice)
|
34
|
+
end
|
35
|
+
|
36
|
+
def displayable_attributes_slice
|
37
|
+
displayable_attributes.is_a?(Hash) ? displayable_attributes.keys : displayable_attributes
|
38
|
+
end
|
39
|
+
|
40
|
+
def displayable_attributes
|
41
|
+
self.class.displayable_attributes
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module CassandraModel
|
2
|
+
class GlobalCallbacks
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def add_listener(listener)
|
6
|
+
listeners << listener
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(callback, *params)
|
10
|
+
listeners.each do |listener|
|
11
|
+
callback_name = callback_name(callback)
|
12
|
+
listener.public_send(callback_name, *params) if listener.respond_to?(callback_name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(method, *args, &block)
|
17
|
+
if method =~ /^on_/
|
18
|
+
listener = Object.new.tap do |callback|
|
19
|
+
callback.define_singleton_method(method) { |*callback_args| block.call(*callback_args) }
|
20
|
+
end
|
21
|
+
add_listener listener
|
22
|
+
else
|
23
|
+
super(method, *args, &block)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def callback_name(callback)
|
30
|
+
"on_#{callback}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def listeners
|
34
|
+
@listeners ||= []
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module CassandraModel
|
2
|
+
module MetaColumns
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend MetaColumnsStatic
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def deferred_columns
|
11
|
+
self.class.deferred_columns
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_initialize
|
15
|
+
self.class.after_initialize(self)
|
16
|
+
deferred_columns.each do |column|
|
17
|
+
column_value = @attributes.delete(column)
|
18
|
+
send("#{column}=", column_value) if column_value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def deferred_getset(name, on_load)
|
23
|
+
if instance_variable_defined?(defered_column_name(name))
|
24
|
+
deferred_get(name)
|
25
|
+
else
|
26
|
+
deferred_set_with_callback(name, on_load)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def deferred_get(name)
|
31
|
+
instance_variable_get(defered_column_name(name))
|
32
|
+
end
|
33
|
+
|
34
|
+
def async_deferred_set(name)
|
35
|
+
future = @deferred_futures[name]
|
36
|
+
result = (future.get if future)
|
37
|
+
deferred_set(name, result)
|
38
|
+
end
|
39
|
+
|
40
|
+
def deferred_set_with_callback(name, on_load)
|
41
|
+
result = on_load.call(@attributes)
|
42
|
+
deferred_set(name, result)
|
43
|
+
end
|
44
|
+
|
45
|
+
def deferred_set(name, value)
|
46
|
+
instance_variable_set(defered_column_name(name), value)
|
47
|
+
end
|
48
|
+
|
49
|
+
def defered_column_name(name)
|
50
|
+
"@deferred_#{name}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module MetaColumnsStatic
|
55
|
+
extend Forwardable
|
56
|
+
|
57
|
+
#attr_reader :deferred_column_writers, :async_deferred_column_writers
|
58
|
+
def_delegators :table_config, :deferred_column_writers, :async_deferred_column_writers
|
59
|
+
|
60
|
+
def deferred_column(name, options)
|
61
|
+
name = name.to_sym
|
62
|
+
deferred_columns << name
|
63
|
+
|
64
|
+
create_attr_accessor(name, options)
|
65
|
+
create_save_method(name, options)
|
66
|
+
end
|
67
|
+
|
68
|
+
def async_deferred_column(name, options)
|
69
|
+
name = name.to_sym
|
70
|
+
deferred_columns << name
|
71
|
+
|
72
|
+
async_create_attr_accessor(name, options)
|
73
|
+
async_create_save_method(name, options)
|
74
|
+
end
|
75
|
+
|
76
|
+
def after_initialize(record)
|
77
|
+
futures = if table_config.async_deferred_column_readers
|
78
|
+
table_config.async_deferred_column_readers.inject({}) do |memo, (column, callback)|
|
79
|
+
memo.merge!(column => callback.call(record.attributes))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
record.instance_variable_set(:@deferred_futures, futures)
|
83
|
+
end
|
84
|
+
|
85
|
+
def save_deferred_columns(record)
|
86
|
+
do_save_deferred_columns(record) if table_config.deferred_column_writers
|
87
|
+
end
|
88
|
+
|
89
|
+
def save_async_deferred_columns(record)
|
90
|
+
do_save_async_deferred_columns(record) if table_config.async_deferred_column_writers
|
91
|
+
end
|
92
|
+
|
93
|
+
def deferred_columns
|
94
|
+
table_config.deferred_columns ||= []
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def do_save_deferred_columns(record)
|
100
|
+
table_config.deferred_column_writers.each { |column, callback| callback.call(record.attributes, record.send(column)) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def do_save_async_deferred_columns(record)
|
104
|
+
table_config.async_deferred_column_writers.map { |column, callback| callback.call(record.attributes, record.send(column)) }
|
105
|
+
end
|
106
|
+
|
107
|
+
def create_save_method(name, options)
|
108
|
+
on_save = options[:on_save]
|
109
|
+
if on_save
|
110
|
+
table_config.deferred_column_writers ||= {}
|
111
|
+
table_config.deferred_column_writers[name] = on_save
|
112
|
+
|
113
|
+
define_method(:"save_#{name}") { on_save.call(@attributes, send(name)) }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def async_create_save_method(name, options)
|
118
|
+
on_save = options[:on_save]
|
119
|
+
if on_save
|
120
|
+
table_config.async_deferred_column_writers ||= {}
|
121
|
+
table_config.async_deferred_column_writers[name] = on_save
|
122
|
+
|
123
|
+
define_method(:"save_#{name}") { on_save.call(@attributes, send(name)) }
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def create_attr_accessor(name, options)
|
128
|
+
create_attr_reader(name, options)
|
129
|
+
create_attr_writer(name)
|
130
|
+
end
|
131
|
+
|
132
|
+
def async_create_attr_accessor(name, options)
|
133
|
+
async_create_attr_reader(name, options)
|
134
|
+
create_attr_writer(name)
|
135
|
+
end
|
136
|
+
|
137
|
+
def create_attr_writer(name)
|
138
|
+
define_method(:"#{name}=") do |value|
|
139
|
+
deferred_set(name, value)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def create_attr_reader(name, options)
|
144
|
+
on_load = options[:on_load]
|
145
|
+
raise 'No on_load method provided' unless on_load
|
146
|
+
|
147
|
+
define_method(name) { deferred_getset(name, on_load) }
|
148
|
+
end
|
149
|
+
|
150
|
+
def async_create_attr_reader(name, options)
|
151
|
+
on_load = options[:on_load]
|
152
|
+
raise 'No on_load method provided' unless on_load
|
153
|
+
|
154
|
+
table_config.async_deferred_column_readers ||= {}
|
155
|
+
table_config.async_deferred_column_readers[name] = on_load
|
156
|
+
|
157
|
+
define_method(name) do
|
158
|
+
deferred_get(name) || async_deferred_set(name)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|