cassandra_complex 0.5

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.
@@ -0,0 +1,7 @@
1
+ require 'cassandra-cql/1.1'
2
+ require 'cassandra_complex/configuration'
3
+ require 'cassandra_complex/row'
4
+ require 'cassandra_complex/connection'
5
+ require 'cassandra_complex/table'
6
+ require 'cassandra_complex/model'
7
+ require 'cassandra_complex/index'
@@ -0,0 +1,59 @@
1
+ module CassandraComplex
2
+
3
+ class ConfigurationError < Exception
4
+ end
5
+
6
+ class MissingConfiguration < ConfigurationError
7
+ end
8
+
9
+ # Configuration class to specify basic settings,
10
+ # such as host, default keyspace and logger.
11
+ #
12
+ # Your yaml configuration file should looks like:
13
+ # host: '127.0.0.1:9160, example.com:9160'
14
+ # default_keyspace: 'keyspace_production'
15
+ #
16
+ # @!attribute [r] host
17
+ # @return [String] The host is being connected to
18
+ # @!attribute [r] default_keyspace
19
+ # @return [String] The keyspace is being used within connection by default
20
+ # @!attribute [rw] logger
21
+ # @return [String] The logger(kind_of? Logger) is being used by default
22
+ class Configuration
23
+
24
+ class << self
25
+ attr_reader :host
26
+ attr_reader :default_keyspace
27
+
28
+ attr_accessor :logger
29
+
30
+ # Load yaml source
31
+ #
32
+ # @param [IO, String, Hash] something file path, IO, raw YAML string, or a pre-loaded Hash
33
+ # @return [Boolean, Hash] loaded yaml file or false if a RuntimeError occurred while loading
34
+ def read(something)
35
+ return_value = false
36
+
37
+ begin
38
+ if something.kind_of?(Hash)
39
+ return_value = something
40
+ elsif File.exists?(something)
41
+ return_value = YAML.load_file(something)
42
+ else
43
+ return_value = YAML.load(something)
44
+ end
45
+ raise ConfigurationError unless return_value.kind_of?(Hash)
46
+ rescue
47
+ return_value = false
48
+ end
49
+ @host = return_value['host']
50
+ @default_keyspace = return_value['default_keyspace']
51
+
52
+ @logger = Logger.new('/dev/null')
53
+
54
+ return_value
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,168 @@
1
+ require 'thread'
2
+
3
+ module CassandraComplex
4
+ # Basic class which encapsulate driver` connection to a Cassandra cluster.
5
+ # It executes raw CQL and can return result sets in two ways.
6
+ #
7
+ # @!attribute [r] keyspace
8
+ # @return [String] The keyspace is being connected to
9
+ # @example Usage of Connection
10
+ # connection = Connection.new('127.0.0.1:9160', {:keyspace=>'cassandra_complex_test'})
11
+ # row_set = connection.execute("select * from timeline")
12
+ # row_set.each |row|
13
+ # puts row['user_id']
14
+ # end
15
+ class Connection
16
+
17
+ attr_reader :keyspace
18
+ attr_reader :conn
19
+ # Connections pool, @see .connection
20
+ @@connections = {}
21
+
22
+ class << self
23
+
24
+ # Create( if not exists or not active) and return connection to kyspc
25
+ #
26
+ # @param [String] kyspc ('system') The keyspace to which connect
27
+ # @return [CassandraComplex::Connection] Connection instance
28
+ def connection(kyspc=nil)
29
+ raise MissingConfiguration if Configuration.host.nil? || Configuration.default_keyspace.nil?
30
+ @@connections[kyspc] = CassandraComplex::Connection.new(Configuration.host, {:keyspace=>kyspc || Configuration.default_keyspace || 'system'})\
31
+ unless ( @@connections[kyspc] && @@connections[kyspc].conn.active?)
32
+ Configuration.logger.info "Connected to: #{Configuration.host}:#{Configuration.default_keyspace}"\
33
+ if Configuration.logger.kind_of?(Logger)
34
+ @@connections[kyspc]
35
+ end
36
+ end
37
+
38
+
39
+ # Create new instance of Connection and initialize connection with Cassandra
40
+ #
41
+ # @param [Array, String] hosts list of hosts, a single host, to connect to
42
+ # @param [Hash] options list of options
43
+ # @option options [String] keyspace initial keyspace to connect, default is 'system'
44
+ # @return [CassandraComplex::Connection] new instance
45
+ def initialize(hosts, options = {})
46
+ @keyspace = options[:keyspace] || 'system'
47
+ Configuration.logger.info "Connecting to #{hosts.inspect} with params #{options.inspect}"\
48
+ if Configuration.logger.kind_of?(Logger)
49
+ @conn = CassandraCQL::Database.new(hosts, options.merge({:cql_version=>'3.0.0'}))
50
+ @mutex = Mutex.new
51
+ end
52
+
53
+ # Execute CQL3 query with Thread safety.
54
+ #
55
+ # @param [Array<String>, String] cql_string string with cql3 commands
56
+ # @param [Boolean] multi_commands if the cql_strings should be divided into separate commands
57
+ # @param [CassandraComplex::Table] table the table with describing schema
58
+ # @param [Array] bind bind for cql string
59
+ # @yieldparam [Proc] blck custom code to be executed on each new row adding
60
+ # @return [Array] row set
61
+ def execute(cql_string, multi_commands = true, table=nil, bind=[], &blck)
62
+ row_set = []
63
+ @mutex.synchronize {
64
+ begin
65
+ join_multi_commands(cql_string, multi_commands).each do |cql|
66
+ if !(cql.strip.empty?)
67
+ if bind.size > 0
68
+ cql = CassandraCQL::Statement.sanitize(cql, bind)
69
+ end
70
+ Configuration.logger.info "Going to execute CQL: '#{cql}'"\
71
+ if Configuration.logger.kind_of?(Logger)
72
+ new_rows = process_thrift_rows(@conn.execute(cql), &blck)
73
+ row_set << new_rows if new_rows
74
+ end
75
+ end
76
+ ensure
77
+ row_set_flatten = row_set.flatten
78
+ return row_set_flatten
79
+ end
80
+ }
81
+ end
82
+
83
+ # Change current keyspace temporarily; restore original keyspace upon return.
84
+ #
85
+ # @param [String] kyspc The keyspace of chaning context
86
+ # @yield Execute cassandra operations within context of kyspc
87
+ def with_keyspace(kyspc)
88
+ @mutex.synchronize {
89
+ if kyspc != @keyspace.strip
90
+ old_keyspace, @keyspace = @keyspace, kyspc
91
+
92
+ execute("use #{@keyspace};")
93
+ yield if block_given?
94
+ execute("use #{old_keyspace};")
95
+
96
+ @keyspace = old_keyspace
97
+ else
98
+ yield if block_given?
99
+ end
100
+ }
101
+ end
102
+
103
+ # Execute CQL3 commands within batch
104
+ # (see #execute)
105
+ #
106
+ # @param [String, Array] cql_commands CQL3 commands to be executed within batch
107
+ # @param [Hash] options Consistency options of batch command
108
+ # @option options[String] :write_consistency ('ANY') Write consistency
109
+ # @option options[Time] :write_timestamp (nil) Write timestamp
110
+ # @option options[String] :read_consistency ('QUORUM') Read consistency
111
+ # @option options[Time] :read_timestamp (nil) Read timestamp
112
+ # @return [CassandraModeCql::RowSet] row set
113
+ def execute_batch(cql_commands, options={:write_consistency=>'ANY', :write_timestamp=>nil, :read_consistency=>'QUORUM', :read_timestamp=>nil})
114
+ command = "\
115
+ BEGIN BATCH #{prepare_consistency_level(options)}
116
+ #{cql_commands}
117
+ APPLY BATCH;\
118
+ "
119
+ execute(command, false)
120
+ end
121
+
122
+ # Return key alias(first part of primary key) for given table
123
+ #
124
+ # @param [String] table_name Table name of given table
125
+ # @return [String] primary key for given table
126
+ def key_alias(table_name)
127
+ @conn.schema.column_families[table_name].cf_def.key_alias
128
+ end
129
+
130
+ private
131
+
132
+ # Process thrift rows
133
+ #
134
+ # @param [Array] rows thrift rows
135
+ # @yieldparam [Proc] blck custom code to be executed on each new row adding
136
+ def process_thrift_rows(rows, &blck)
137
+ return unless rows
138
+ return_value = []
139
+ rows.fetch do |thrift_row|
140
+ row = {}
141
+ thrift_row.row.columns.each do |thrift_column|
142
+ column_name = CassandraCQL::ColumnFamily.cast(thrift_column.name, thrift_row.schema.names[thrift_column.name])
143
+ column_value = CassandraCQL::ColumnFamily.cast(thrift_column.value, thrift_row.schema.values[thrift_column.name])
144
+ row.merge!({column_name=>column_value})
145
+ end
146
+ blck.call(row) if block_given?
147
+ return_value.push(row)
148
+ end
149
+ return_value
150
+ end
151
+
152
+ # Prepare cql statement before executing
153
+ #
154
+ # @param [String] cql_statement CQL3 statemenet that need to be prepared
155
+ # @param [Boolean] multi_commands If cql_statement consist multi commands
156
+ def join_multi_commands(cql_statement, multi_commands)
157
+ return_value = cql_statement
158
+ if multi_commands
159
+ return_value = return_value.gsub(/\n/, ' ')
160
+ return_value = return_value.each_line(';')
161
+ else
162
+ return_value = return_value.to_a
163
+ end
164
+ return_value
165
+ end
166
+
167
+ end
168
+ end
@@ -0,0 +1,39 @@
1
+ module CassandraComplex
2
+ # A composite index for table
3
+ # not yet implemented
4
+ # @example
5
+ # class Timeline < CassandraComplex::Model
6
+ #
7
+ # set_keyspace 'history'
8
+ #
9
+ # attribute :user_id, 'varchar'
10
+ # attribute :author_tweet_id, 'uuid'
11
+ # attribute :author, 'varchar'
12
+ # attribute :body, 'varchar'
13
+ #
14
+ # primary_key :user_id, :tweet_id
15
+ # end
16
+ #
17
+ # class Tweet < CassandraComplex::Model
18
+ # set_keyspace 'history'
19
+ #
20
+ # attribute :tweet_id, 'varchar'
21
+ # attribute :important_data, 'varchar'
22
+ # end
23
+ #
24
+ # class TwitIndex < CassandraComplex::Index
25
+ # index :with_table=>Timeline #, :attributes=>[:author, :author_tweet_id]
26
+ # indexing :table=> Tweet, :index=>[:tweet_id]
27
+ #
28
+ # indexing_rule Proc.new{|timeline| timeline.author.to_s + ':' + timeline.author_tweet_id.to_s}
29
+ # end
30
+ #
31
+ # t = Timeline.find('gmason')
32
+ # puts t.inspect
33
+ # => <Timeline:{:user_id=>'gmason', :author_tweet_id=>1, :author=>'gwashington', :body=>'Some text'}
34
+ # puts t.tweets.inspect
35
+ # <Tweet:{:tweet_id=>'gwashington:1', :important_data=>'Some data.'}>
36
+
37
+ class Index < Table
38
+ end
39
+ end
@@ -0,0 +1,309 @@
1
+ module CassandraComplex
2
+ # A little bit sugared model.
3
+ #
4
+ # @!attribute [rw] table_name
5
+ # @return [String] Table name
6
+ #
7
+ # @example Using model
8
+ # class Timeline < CassandraComplex::Model
9
+ #
10
+ # table 'timeline'
11
+ #
12
+ # attribute :user_id, 'varchar'
13
+ # attribute :tweet_id, 'uuid'
14
+ # attribute :author, 'varchar'
15
+ # attribute :body, 'varchar'
16
+ # #composite primary key
17
+ # primary_key :user_id, :tweet_id
18
+ # end
19
+ #
20
+ # #creating Column Family
21
+ # Timeline.create_table
22
+ #
23
+ # t = Timeline.new(:user_id=>'mickey', :tweet_id=>1715, :author=> 'mouse', :body=>"'Hello!'")
24
+ # t.save
25
+ #
26
+ # timelines = Timeline.all('mickey')
27
+ # t = timelines.first
28
+ # puts t.body
29
+ # => 'Hello!'
30
+ # t.body = "'Goodbye!'"
31
+ # puts t.dirty?
32
+ # => true
33
+ # t.save
34
+ #
35
+ # t.update(:tweet_id=>1777)
36
+ #
37
+ # #dropping Column Family
38
+ # Timeline.drop_table
39
+ class ModelError < Exception
40
+ end
41
+
42
+ class WrongModelDefinition < ModelError
43
+ end
44
+
45
+ class WrongModelInitialization < ModelError
46
+ end
47
+
48
+ class Model
49
+ #class` methods
50
+ class << self
51
+ attr_accessor :table_name
52
+
53
+ @@table = Hash.new {|hash, key| hash[key] = nil}
54
+ @@table_name = Hash.new {|hash, key| hash[key] = ''}
55
+
56
+ @@attributes = Hash.new {|hash, key| hash[key] = {}}
57
+ @@primary_key = Hash.new {|hash, key| hash[key] = []}
58
+
59
+ # Returns table executing all cql commands
60
+ #
61
+ # @return [Table] CassandraComplex::Table
62
+ def table_cql
63
+ @@table[self]
64
+ end
65
+
66
+ # Returns table name
67
+ #
68
+ # @return [String] Name of the table
69
+ def table_name
70
+ @@table_name[self]
71
+ end
72
+
73
+ # Returns all attributes within class
74
+ #
75
+ # @return [Hash] attributes of current Model
76
+ def attributes
77
+ @@attributes[self]
78
+ end
79
+
80
+ # Returns primary key for current Model
81
+ #
82
+ # @return [Array<String>] primary key for current Model
83
+ def get_primary_key
84
+ @@primary_key[self]
85
+ end
86
+
87
+ # Returns schema for current Model
88
+ #
89
+ # @return [Hash] schema for current model
90
+ def schema
91
+ attr = {}
92
+ attributes.each{|x,y| attr[x] = y[:type]}
93
+ {:table => table_name, :attributes => attr, :primary_key => get_primary_key}
94
+ end
95
+
96
+ # Set primary key(s) for current Model
97
+ #
98
+ # @param [Array<Symbol>] attr_names Primary key(s)
99
+ def primary_key(*attr_names)
100
+ attr_names.each do |attr_name|
101
+ raise WrongModelDefinition, 'Primary key could be choosen just from already introduced attribute.'\
102
+ unless attributes.has_key?(attr_name.intern)
103
+ @@primary_key[self] << attr_name.intern
104
+ end
105
+ end
106
+
107
+ # Introduce attribute for the Model.
108
+ # Valid attribute`s types: 'blog', 'ascii', 'text'/'varchar', 'varint', 'int', 'bigint', 'uuid', 'timestamp', 'boolean',
109
+ # 'float', 'double', 'decimal', 'counter'.
110
+ #
111
+ # @param [Symbol] attr_name Attribute`s name
112
+ # @param [String] attr_type Attribute`s type
113
+ def attribute(attr_name, attr_type)
114
+ attr_name = attr_name.intern
115
+ raise WrongModelDefinition, 'You can`t redefine already introduced attribute.' if self.instance_methods.include?(name)
116
+
117
+ attributes[attr_name] = {:type => attr_type}
118
+ define_method(attr_name) do
119
+ @_attributes[attr_name][:value]
120
+ end
121
+ define_method(:"#{attr_name}=") do |value|
122
+ @_attributes[attr_name][:value] = value
123
+ @_attributes[attr_name][:dirty?] = true
124
+ end
125
+ end
126
+
127
+ # Set table name for current Model
128
+ #
129
+ # @param [String] new_table_name
130
+ def table(new_table_name)
131
+ table_name = new_table_name.to_s.downcase
132
+ @@table[self] = Class.new(CassandraComplex::Table) do
133
+ set_table_name table_name
134
+ end
135
+ @@table_name[self] = table_name
136
+ end
137
+
138
+ # Return count of result set for given primary key
139
+ #
140
+ # @param [String, Array<String>, Hash] key
141
+ # @param [Hash] clauses select clauses
142
+ # @option clauses [String, Array<String>] where where clause
143
+ # @option clauses [String] order order clause
144
+ # @option clauses [String] limit limit clause
145
+ # @yieldparam [Proc] blck custom code
146
+ # @return [Array<Hash>] array of hashes
147
+ def count(key=nil, clauses={}, &blck)
148
+ key = nil if key == :all
149
+ table_cql.count(key, clauses, &blck)
150
+ end
151
+
152
+ # Return 'all' result set
153
+ #
154
+ # @param [Hash] clauses select clauses
155
+ # @option clauses [String, Array<String>] where where clause
156
+ # @option clauses [String] order order clause
157
+ # @option clauses [String] limit limit clause
158
+ # @yieldparam [Proc] blck custom code
159
+ # @return [Array<Hash>] array of hashes
160
+ def all(clauses={}, &blck)
161
+ find(:all, clauses, &blck)
162
+ end
163
+
164
+ # Return result set for given primary key
165
+ #
166
+ # @param [String, Array<String>, Hash] key
167
+ # @param [Hash] clauses select clauses
168
+ # @option clauses [String, Array<String>] where where clause
169
+ # @option clauses [String] order order clause
170
+ # @option clauses [String] limit limit clause
171
+ # @yieldparam [Proc] blck custom code
172
+ # @return [Array<Hash>] array of hashes
173
+ def find(key=nil, clauses={}, &blck)
174
+ key = nil if key == :all
175
+ return_value = table_cql.find(key, clauses).map do |record|
176
+ new_instance = self.new(record, {:dirty => false})
177
+ blck.call(new_instance) if block_given?
178
+ new_instance
179
+ end
180
+ return_value
181
+ end
182
+
183
+ # Delete record(s)
184
+ #
185
+ # @param [String, Array<String>, Hash] key
186
+ # @param [Hash] clauses
187
+ # @option clauses [String] timestamp timestamp of operation
188
+ # @option clauses [Array<String>] columns columns which should be deleted
189
+ # @option clauses [Array, String] where where options for delete operation
190
+ # @return [Boolean] always true
191
+ def delete(key, clauses={}, &blck)
192
+ key = nil if key == :all
193
+ table_cql.delete(key, clauses, &blck)
194
+ end
195
+
196
+ # Create record from hash
197
+ #
198
+ # @param [Hash] hsh attributes for new record
199
+ # @return [Model] created model
200
+ def create(hsh={})
201
+ new_model = self.new(hsh)
202
+ new_model.save
203
+ new_model
204
+ end
205
+
206
+ # Create table for model within Cassandra
207
+ def create_table
208
+ attrs = attributes.map{|x,y| "#{x.to_s} #{y[:type].to_s}"}.join(', ')
209
+ p_key = ''
210
+ p_key = " PRIMARY KEY (#{get_primary_key.map{|x| x.to_s}.join(', ')})"
211
+ create_table_command = <<-eos
212
+ CREATE TABLE table_name (
213
+ #{attrs}
214
+ #{p_key}
215
+ );
216
+ eos
217
+ table_cql.execute(create_table_command)
218
+ end
219
+
220
+ # Drop table for model within Cassandra
221
+ def drop_table
222
+ drop_table_command = <<-eos
223
+ DROP TABLE table_name;
224
+ eos
225
+ table_cql.execute(drop_table_command)
226
+ end
227
+
228
+ # Truncate table within Cassandra
229
+ def truncate
230
+ table_cql.truncate
231
+ end
232
+ end
233
+
234
+ # Returns if model is dirty(some field were changed but model still not saved)
235
+ #
236
+ # @return [Boolean] is model dirty
237
+ def dirty?
238
+ return_value = false
239
+ @_attributes.each_value do |attr_value|
240
+ return_value = true if attr_value[:dirty?]
241
+ end
242
+ return_value
243
+ end
244
+
245
+ # Initialize model with hash due to options
246
+ #
247
+ # @param [Hash] hsh initialization attributes
248
+ # @param [Hash] options initalization options
249
+ # @option options [Bolean] :dirty dirtiness of initialized model
250
+ # @raise [WrongModelInitialization] raised if passed previously not described attribute
251
+ def initialize(hsh = {}, options={})
252
+ @_attributes = Hash.new{|hash, key| hash[key] = {}}
253
+ hsh.each_pair do |key, value|
254
+ if self.class.attributes.has_key?(key.intern)
255
+ @_attributes[key.intern][:value] = value
256
+ @_attributes[key.intern][:dirty?] = options[:dirty].nil? ? true : options[:dirty]
257
+ else
258
+ raise WrongModelInitialization, "Can`t initialize Model with attribute - #{key} ,that are not described in Model definition."
259
+ end
260
+ end
261
+ end
262
+
263
+ # Comparator for current and other model
264
+ #
265
+ # @param [Model] other model to compare with
266
+ # @return [Boolean] true if attrbitues of models are equal and each attribute of other model equal to proper attribute of current model, false otherwise
267
+ def ==(other)
268
+ return false unless other.kind_of?(self.class)
269
+ return false unless self.class.attributes.keys.sort == other.class.attributes.keys.sort
270
+
271
+ self.class.attributes.keys.each do |attr|
272
+ return false unless self.send(attr) == other.send(attr)
273
+ end
274
+
275
+ return true
276
+ end
277
+
278
+ # Save the Model within Cassandra
279
+ def save
280
+ insert_hash = {}
281
+
282
+ @_attributes.keys.each do |key|
283
+ insert_hash[key.to_s] = self.send(key) if self.class.get_primary_key.include?(key) || @_attributes[key.intern][:dirty?]
284
+ end
285
+
286
+ self.class.table_cql.create(insert_hash)
287
+ self.dirty = false
288
+ end
289
+
290
+ # Delete the Model within Cassandra
291
+ def delete
292
+ delete_hash = {}
293
+ self.class.get_primary_key.each{|pk| delete_hash[pk.to_s]=self.send(pk)}
294
+ self.class.table_cql.delete(delete_hash)
295
+ self.dirty = false
296
+ end
297
+
298
+ private
299
+
300
+ # Set dirtiness for the whole Model
301
+ def dirty=(new_dirty_value, columns = nil)
302
+ columns ||= @_attributes.keys
303
+ @_attributes.each_key do |attr_name|
304
+ @_attributes[attr_name][:dirty?] = new_dirty_value if columns.include?(attr_name)
305
+ end
306
+ end
307
+
308
+ end
309
+ end