cassandra_model 0.9.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +13 -0
  3. data/README.md +170 -0
  4. data/lib/cassandra_model.rb +48 -0
  5. data/lib/cassandra_model/batch_reactor.rb +32 -0
  6. data/lib/cassandra_model/batch_reactor/future.rb +49 -0
  7. data/lib/cassandra_model/composite_record.rb +49 -0
  8. data/lib/cassandra_model/composite_record_static.rb +169 -0
  9. data/lib/cassandra_model/connection_cache.rb +24 -0
  10. data/lib/cassandra_model/counter_record.rb +58 -0
  11. data/lib/cassandra_model/data_inquirer.rb +105 -0
  12. data/lib/cassandra_model/data_modelling.rb +45 -0
  13. data/lib/cassandra_model/data_set.rb +84 -0
  14. data/lib/cassandra_model/displayable_attributes.rb +44 -0
  15. data/lib/cassandra_model/global_callbacks.rb +39 -0
  16. data/lib/cassandra_model/logging.rb +8 -0
  17. data/lib/cassandra_model/meta_columns.rb +162 -0
  18. data/lib/cassandra_model/meta_table.rb +66 -0
  19. data/lib/cassandra_model/query_builder.rb +122 -0
  20. data/lib/cassandra_model/query_helper.rb +44 -0
  21. data/lib/cassandra_model/query_result.rb +23 -0
  22. data/lib/cassandra_model/raw_connection.rb +163 -0
  23. data/lib/cassandra_model/record.rb +551 -0
  24. data/lib/cassandra_model/result_paginator.rb +37 -0
  25. data/lib/cassandra_model/rotating_table.rb +49 -0
  26. data/lib/cassandra_model/single_token_batch.rb +23 -0
  27. data/lib/cassandra_model/single_token_counter_batch.rb +5 -0
  28. data/lib/cassandra_model/single_token_logged_batch.rb +5 -0
  29. data/lib/cassandra_model/single_token_unlogged_batch.rb +5 -0
  30. data/lib/cassandra_model/table_definition.rb +72 -0
  31. data/lib/cassandra_model/table_descriptor.rb +49 -0
  32. data/lib/cassandra_model/table_redux.rb +58 -0
  33. data/lib/cassandra_model/type_guessing.rb +40 -0
  34. metadata +133 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 811a3063acc6a1abd58de21ba424e0784606718f
4
+ data.tar.gz: af46520692a4f205f6cd8f996d928d09d50ef553
5
+ SHA512:
6
+ metadata.gz: 3e580bb73ed0999532c19b0a6ec881caa2b265ab0fead7bef262bd772d09eeac53885217777ee3a42245aaea02d6ee2c6f33f917e8cec5d67bb1ad1dd1b832d1
7
+ data.tar.gz: f309db41c5212ce53996f4d5e3b71e3a1755e628a32ceeed0d3de36f451c984fb46b8406f702c15c1cdd9def00abc09f1a76b2b8e9819a1c26329b08a2b6f04c
data/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2014-2015 Thomas RM Rogers
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # Cassandra Model
2
+
3
+ The Cassandra Model gem aims at providing intuitive, simple data modelling capabilities for use with Ruby with Apache Cassandra, while still providing access to functionality that makes using Cassandra really powerful.
4
+
5
+ ## Installation
6
+
7
+ As this project is currently in pre-version 1.0, it is only available through github.
8
+
9
+ To integrate Cassandra Model in to your project, add the following lines to your application's Gemfile:
10
+
11
+ gem 'thomas_utils', github: 'thomasrogers03/thomas_utils'
12
+ gem 'cassandra_model', github: 'thomasrogers03/cassandra_model'
13
+
14
+ The Thomas Utils gem is a separate project (located at https://github.com/thomasrogers03/thomas_utils.git) used for some minor helper utitilities used within this project.
15
+
16
+ ## Getting started
17
+
18
+ Cassandra Model offers a number of different ways to construct and uses models, from using existing tables created through your own migration framework, to building tables dynamically simply by describing how a table is intended to be used.
19
+
20
+ ### A familiar starting point, for those of you are used to using ActiveRecord:
21
+
22
+ ```ruby
23
+ require 'cassandra_model'
24
+
25
+ class Car < CassandraModel::Record
26
+ end
27
+
28
+ Car.create(make: 'Honda', year: 2014, model: 'Civic', colour: 'Green')
29
+ Car.create(make: 'Honda', year: 2013, model: 'Civic', colour: 'Blue')
30
+
31
+ recent_hondas = Car.where(make: 'Honda', :year.gt => 2.years.ago).get
32
+ ```
33
+
34
+ This example illustrates how an existing **cars** table can be modelled to grab all the Honda vehicles whose year is at least 2 years ago. It assumes that you have an existing table **cars**, with a partition key *make* and a clustering column *year*.
35
+
36
+ ### A more interesting example, demonstrating how to take advantage of asynchronous queries and token distribution in Cassandra:
37
+
38
+ ```ruby
39
+ class Car < CassandraModel::Record
40
+ end
41
+
42
+ futures = []
43
+ futures << Car.create_async(make: 'Honda', year: 2014, model: 'Civic', colour: 'Green')
44
+ futures << Car.create_async(make: 'Honda', year: 2013, model: 'Civic', colour: 'Blue')
45
+ futures << Car.create_async(make: 'Toyota', year: 2014, model: 'Highlander', colour: 'Blue')
46
+ futures.map(&:join)
47
+
48
+ makes = %(Honda Toyota GM)
49
+ cars = makes.map { |make| Car.where(make: make, :year.gt => 2.years.ago).async }.map(&:get).flatten
50
+ ```
51
+
52
+ This example shows how we can use Future/Promise API that the Datastax Cassandra ruby-driver provides to execute a number of queries asynchronously and wait for the results.
53
+
54
+ More importantly, however, it shows us how we can take advantages of very fast writes across nodes in a Cassandra backed application. Such design principles quickly become very important when dealing with Cassandra in both write and read friendly requirements.
55
+
56
+ ## Generating tables dynamically
57
+
58
+ There are two ways we can define meta tables (or tables that exist as we model them in Ruby). This can be done by defining a table definition, and then associating a MetaTable with the model, or by using built in data modelling features.
59
+
60
+ ### Before we can create an meta tables
61
+
62
+ A table to keep track of all of these tables must be created
63
+
64
+ ```ruby
65
+ CassandraModel::TableDescriptor.create_descriptor_table
66
+ ```
67
+
68
+ This method will create a table of descriptors for meta tables, if it does not already exist.
69
+
70
+ ### Creating a MetaTable based model
71
+
72
+ ```ruby
73
+ class Car < CassandraModel::Record
74
+ TABLE_ATTRIBUTES = {
75
+ name: :cars,
76
+ partition_key: { make: :text },
77
+ clustering_columns: { year: :int, model: :text },
78
+ remaining_columns: { attributes: 'map<text, text>' }
79
+ }
80
+ TABLE_DEFINITION = CassandraModel::TableDefinition.new(TABLE_ATTRIBUTES)
81
+ self.table = CassandraModel::MetaTable.new(TABLE_DEFINITION)
82
+ end
83
+ ```
84
+
85
+ This model is now ready to be used similarly to the examples above, without having to write any CQL!
86
+
87
+ __Note__: Tables defined in this way will have unique identifiers appended to their name in Cassandra. This is done so we can modify the table design without dropping (!) and re-creating tables in Cassandra. The history of these tables is recorded in the **table_descriptors** table.
88
+
89
+ ### Using data modelling helpers
90
+
91
+ The data modelling features in Cassandra Model are meant to be a somewhat verbose, high-level table designing tool to help build performant Cassandra tables with meaning. The following code demonstrates how we can re-create the **cars** table using this method.
92
+
93
+ ```ruby
94
+ class Car < CassandraModel::Record
95
+ extend CassandraModel::DataModelling
96
+
97
+ model_data do |inquirer, data_set|
98
+ inquirer.knows_about(:make)
99
+
100
+ data_set.is_defined_by(:year, :model)
101
+ data_set.change_type_of(:year).to(:int)
102
+ data_set.knows_about(:colour)
103
+ end
104
+ end
105
+ ```
106
+
107
+ Here, we try to define the table in terms of what kind of questions we'd like to ask about our data. In this particular case, we can ask about all of the cars given a specific make, and learn about their details. Note that a columns by default are assumed to be text columns.
108
+
109
+ ## Records with composite columns
110
+
111
+ One of the ways to avoid numerous queries to Cassandra is to de-normalize data as much a possible when saving records. However, sometimes we want to learn about data without having complete knowledge as to how it is defined. This can be accomplished using the CompositeRecord helpers.
112
+
113
+ To use this, we define an inquirer and a data set with the pieces of information we know, and let it handle what we don't know.
114
+
115
+ ```ruby
116
+ class Car < CassandraModel::Record
117
+ extend CassandraModel::DataModelling
118
+
119
+ model_data do |inquirer, data_set|
120
+ inquirer.knows_about(:make, :model, :year, :colour)
121
+ inquirer.knows_about(:make)
122
+ inquirer.knows_about(:make, :model, :year)
123
+ inquirer.knows_about(:vin)
124
+ inquirer.defaults(:year).to(1900)
125
+
126
+ data_set.is_defined_by(:price, :vin, :make, :model, :colour)
127
+ data_set.change_type_of(:price).to(:double)
128
+ data_set.knows_about(:description)
129
+ end
130
+ end
131
+
132
+ Car.create(make: 'Honda', model: 'Civic', year: 2003, colour: 'Blue', vin: '123456789', price: 2_000.0, description: 'A very reliable car')
133
+ ```
134
+
135
+ With this model in hand, now we can ask questions like "What are all the cars we have for Toyota?" Or "How many 2001 Honda Civics do we have in blue?" We can also ask for the price range of a very specific model if we want to.
136
+
137
+ ## Additional features
138
+
139
+ * Flexible table sharding
140
+ * Automatically rotating tables in use for maintenance
141
+ * Configuring multiple Cassandra connections over multiple keyspaces
142
+ * Mixing query helpers with ActiveRecord to build intuitive relations between a relational database and Cassandra
143
+
144
+ ## Undocumented features
145
+
146
+ There are a number of features in Cassandra Model that may have missing or incomplete documentation. This will change as time progresses.
147
+
148
+ ## Known issues
149
+
150
+ * As Cassandra Model uses splat arguments for providing query arguments, only Datastax ruby-driver versions up to 2.0.1 are supported.
151
+ * There is currently no elegant method of migrating data between different versions of meta tables
152
+ * CassandraModel::TableDescriptor.create_descriptor_table does not wait for table persistence in Cassandra
153
+ * CassandraModel::TableDescriptor.create_descriptor_table is vulnerable to being created multiple times when multiple applications are running with the same code
154
+
155
+
156
+ ## Copyright
157
+
158
+ Copyright 2014-2015 Thomas Rogers.
159
+
160
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
161
+
162
+ [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
163
+
164
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
165
+
166
+
167
+
168
+
169
+
170
+
@@ -0,0 +1,48 @@
1
+ #--
2
+ # Copyright 2014-2015 Thomas RM Rogers
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #++
16
+
17
+ require 'concurrent'
18
+ require 'cassandra'
19
+ require 'active_support/all'
20
+ require 'active_support/core_ext/class/attribute_accessors'
21
+
22
+ require 'cassandra_model/logging'
23
+ require 'cassandra_model/global_callbacks'
24
+ require 'cassandra_model/single_token_batch'
25
+ require 'cassandra_model/single_token_unlogged_batch'
26
+ require 'cassandra_model/single_token_logged_batch'
27
+ require 'cassandra_model/single_token_counter_batch'
28
+ require 'cassandra_model/batch_reactor'
29
+ require 'cassandra_model/batch_reactor/future'
30
+ require 'cassandra_model/raw_connection'
31
+ require 'cassandra_model/connection_cache'
32
+ require 'cassandra_model/table_definition'
33
+ require 'cassandra_model/table_redux'
34
+ require 'cassandra_model/result_paginator'
35
+ require 'cassandra_model/query_result'
36
+ require 'cassandra_model/query_builder'
37
+ require 'cassandra_model/displayable_attributes'
38
+ require 'cassandra_model/record'
39
+ require 'cassandra_model/counter_record'
40
+ require 'cassandra_model/table_descriptor'
41
+ require 'cassandra_model/meta_table'
42
+ require 'cassandra_model/rotating_table'
43
+ require 'cassandra_model/composite_record_static'
44
+ require 'cassandra_model/composite_record'
45
+ require 'cassandra_model/type_guessing'
46
+ require 'cassandra_model/data_inquirer'
47
+ require 'cassandra_model/data_set'
48
+ require 'cassandra_model/data_modelling'
@@ -0,0 +1,32 @@
1
+ module CassandraModel
2
+ class BatchReactor < ::BatchReactor::ReactorCluster
3
+
4
+ def initialize(cluster, session, batch_klass, options)
5
+ @cluster = cluster
6
+ @session = session
7
+ @batch_klass = batch_klass
8
+
9
+ define_partitioner(&method(:partition))
10
+ super(cluster.hosts.count, options, &method(:batch_callback))
11
+ end
12
+
13
+ def perform_within_batch(statement)
14
+ ione_future = super(statement)
15
+ Future.new(ione_future)
16
+ end
17
+
18
+ private
19
+
20
+ def partition(statement)
21
+ hosts = @cluster.find_replicas(@session.keyspace, statement)
22
+ @cluster.hosts.find_index(hosts.first) || 0
23
+ end
24
+
25
+ def batch_callback(_)
26
+ batch = @batch_klass.new
27
+ yield batch
28
+ @session.execute_async(batch).on_success { |result| batch.result = result }
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,49 @@
1
+ module CassandraModel
2
+ class BatchReactor
3
+ class Future < Cassandra::Future
4
+ extend Forwardable
5
+
6
+ def self.define_handler(internal_name, external_name = internal_name)
7
+ define_method(external_name) do |&block|
8
+ @future.public_send(internal_name, &block)
9
+ self
10
+ end
11
+ end
12
+
13
+ define_handler :on_complete
14
+ define_handler :on_failure
15
+ define_handler :on_value, :on_success
16
+ def_delegator :@future, :get
17
+
18
+ def initialize(ione_future)
19
+ @future = ione_future
20
+ end
21
+
22
+ def add_listener(listener)
23
+ @future.on_complete do |value, error|
24
+ error ? listener.failure(error) : listener.success(value)
25
+ end
26
+ self
27
+ end
28
+
29
+ def promise
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def fallback(&_)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def then(&block)
38
+ internal_future = @future.then(&block)
39
+ Future.new(internal_future)
40
+ end
41
+
42
+ def join
43
+ @future.get
44
+ self
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ module CassandraModel
2
+ module CompositeRecord
3
+ def self.included(klass)
4
+ klass.extend CompositeRecordStatic
5
+ end
6
+
7
+ def save_async(options = {})
8
+ futures = composite_rows.map { |record| record.internal_save_async(options) }
9
+
10
+ futures << internal_save_async(options)
11
+ Cassandra::Future.all(futures).then { self }
12
+ end
13
+
14
+ def delete_async
15
+ futures = composite_rows.map { |record| record.internal_delete_async }
16
+
17
+ futures << internal_delete_async
18
+ Cassandra::Future.all(futures).then { self }
19
+ end
20
+
21
+ def update_async(new_attributes)
22
+ futures = composite_rows.map { |record| record.internal_update_async(new_attributes) }
23
+
24
+ futures << internal_update_async(new_attributes)
25
+ Cassandra::Future.all(futures).then { self }
26
+ end
27
+
28
+ private
29
+
30
+ def composite_rows
31
+ (self.class.composite_defaults || []).map do |row|
32
+ merged_attributes = attributes.merge(row)
33
+ self.class.new(merged_attributes, validate: false)
34
+ end
35
+ end
36
+
37
+ def attribute(column)
38
+ attributes[column] ||
39
+ attributes[self.class.composite_ck_map[column]] ||
40
+ attributes[self.class.composite_pk_map[column]]
41
+ end
42
+
43
+ def internal_attributes
44
+ internal_columns.inject({}) do |memo, column|
45
+ memo.merge(column => attribute(column))
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,169 @@
1
+ module CassandraModel
2
+ module CompositeRecordStatic
3
+ extend Forwardable
4
+
5
+ def_delegator :table_config, :composite_defaults=
6
+
7
+ def partition_key
8
+ table_data.composite_partition_key ||= internal_partition_key.map { |column| trimmed_column(column, /^rk_/, composite_pk_map) || column }
9
+ end
10
+
11
+ def clustering_columns
12
+ table_data.composite_clustering_columns ||= internal_clustering_columns.map { |column| trimmed_column(column, /^ck_/, composite_ck_map) || column }
13
+ end
14
+
15
+ def primary_key
16
+ table_data.composite_primary_key ||= (internal_partition_key + internal_clustering_columns).map do |column|
17
+ trimmed_column(column, /^rk_/, composite_pk_map) ||
18
+ trimmed_column(column, /^ck_/, composite_ck_map) ||
19
+ column
20
+ end.uniq
21
+ end
22
+
23
+ def columns
24
+ table_data.composite_columns ||= composite_columns.each { |column| define_attribute(column) }
25
+ end
26
+
27
+ def composite_pk_map
28
+ unless table_data.composite_pk_map
29
+ table_data.composite_pk_map = {}
30
+ columns
31
+ end
32
+ table_data.composite_pk_map
33
+ end
34
+
35
+ def composite_ck_map
36
+ unless table_data.composite_ck_map
37
+ table_data.composite_ck_map = {}
38
+ columns
39
+ end
40
+ table_data.composite_ck_map
41
+ end
42
+
43
+ def composite_defaults
44
+ table_data.internal_defaults ||= build_composite_map
45
+ end
46
+
47
+ def generate_composite_defaults(column_defaults, truth_table)
48
+ table_config.composite_defaults = truth_table.map { |row| column_defaults.except(*row) }
49
+ end
50
+
51
+ def generate_composite_defaults_from_inquirer(inquirer)
52
+ table_config.composite_defaults = inquirer.composite_rows.map do |row|
53
+ row.inject({}) do |memo, column|
54
+ memo.merge!(column => inquirer.column_defaults[column])
55
+ end
56
+ end
57
+ end
58
+
59
+ def shard_key
60
+ table_data.composite_shard_key ||= begin
61
+ column = super
62
+ column =~ /^rk_/ ? composite_pk_map[column] : column
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def build_composite_map
69
+ if table_config.composite_defaults
70
+ table_config.composite_defaults.map { |row| row_composite_default(row) }
71
+ end
72
+ end
73
+
74
+ def composite_columns
75
+ internal_columns.map do |column|
76
+ trimmed_column(column, /^rk_/, composite_pk_map) ||
77
+ trimmed_column(column, /^ck_/, composite_ck_map) ||
78
+ column
79
+ end.uniq
80
+ end
81
+
82
+ def trimmed_column(column, column_trim, map)
83
+ column_str = column.to_s
84
+ if column_str =~ column_trim
85
+ column_str.gsub(column_trim, '').to_sym.tap do |result_column|
86
+ map[result_column] = column
87
+ map[column] = result_column
88
+ end
89
+ end
90
+ end
91
+
92
+ def select_clause(select)
93
+ select = mapped_select_columns(select) if select
94
+ super(select)
95
+ end
96
+
97
+ def order_by_clause(order_by)
98
+ order_by = mapped_select_columns(order_by) if order_by
99
+ super(order_by)
100
+ end
101
+
102
+ def mapped_select_columns(select)
103
+ select.map do |column|
104
+ if internal_columns.include?(column)
105
+ column
106
+ else
107
+ mapped_column(column)
108
+ end
109
+ end
110
+ end
111
+
112
+ def where_params(clause)
113
+ updated_clause = clause.inject({}) do |memo, (key, value)|
114
+ updated_key = key_for_where_params(key)
115
+ memo.merge!(updated_key => value)
116
+ end
117
+
118
+ missing_keys = Set.new(internal_partition_key - updated_clause.keys)
119
+ default_clause = composite_defaults.find { |row| (missing_keys ^ row.keys).empty? }
120
+ updated_clause.merge!(default_clause) if default_clause
121
+
122
+ super(updated_clause)
123
+ end
124
+
125
+ def key_for_where_params(key)
126
+ key.is_a?(ThomasUtils::KeyComparer) ? mapped_key_comparer(key) : mapped_key(key)
127
+ end
128
+
129
+ def mapped_key_comparer(key)
130
+ mapped_key = key.key.is_a?(Array) ? key.key.map { |part| mapped_ck(part) } : mapped_ck(key.key)
131
+ key.new_key(mapped_key)
132
+ end
133
+
134
+ def mapped_key(key)
135
+ composite_pk_map[key] || mapped_ck(key)
136
+ end
137
+
138
+ def mapped_ck(key)
139
+ composite_ck_map[key] || key
140
+ end
141
+
142
+ def row_composite_default(row)
143
+ row.inject({}) do |memo, (key, value)|
144
+ memo.merge!(composite_default_row_key(key) => value)
145
+ end
146
+ end
147
+
148
+ def composite_default_row_key(key)
149
+ composite_pk_map[key] || key
150
+ end
151
+
152
+ def row_attributes(row)
153
+ row = super(row)
154
+
155
+ row.inject({}) do |memo, (column, value)|
156
+ if column =~ /^rk_/ || column =~ /^ck_/
157
+ memo.merge!(mapped_column(column) => value)
158
+ else
159
+ memo.merge!(column => value)
160
+ end
161
+ end
162
+ end
163
+
164
+ def mapped_column(column)
165
+ (composite_ck_map[column] || composite_pk_map[column] || column)
166
+ end
167
+
168
+ end
169
+ end