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,10 @@
1
+ # Rough monkey-patch to CassandraCQL::Row for access schema
2
+ module CassandraCQL
3
+ # CassandraCQL::Row
4
+ #
5
+ # @!attribute [r] schema
6
+ # @return [CassandraCQL::Schema] Row schema
7
+ class Row
8
+ attr_reader :schema
9
+ end
10
+ end
@@ -0,0 +1,271 @@
1
+ module CassandraComplex
2
+ # Class Table which wraps CQL3 operations
3
+ #
4
+ # @!attribute [rw] keyspace
5
+ # @return [String] The keyspace is being connected to
6
+ # @!attribute [rw] configuration
7
+ # @return [String] The configuration of the connection
8
+ # @example Selecting all rows with given primary
9
+ # class Timeline < CassandraComplex::Table
10
+ # set_table_name 'timeline'
11
+ # end
12
+ #
13
+ # rows = Timeline.all('some_primary_key') do |row|
14
+ # # this puts is outputed on each row is being fetch
15
+ # puts row['body']
16
+ # end
17
+ # rows.each do |row|
18
+ # # puts is being outputed when all rows are fetched from Cassandra
19
+ # puts row['body']
20
+ # end
21
+ class Table
22
+ #not neccessary to allow .new
23
+ private_class_method :new
24
+
25
+ class << self
26
+
27
+ attr_accessor :keyspace
28
+ attr_accessor :configuration
29
+
30
+ # Specify keyspace for particular table
31
+ #
32
+ # @param [String] kyspc keyspace
33
+ def set_keyspace(kyspc)
34
+ self.keyspace = kyspc
35
+ end
36
+
37
+ # Return connection for given keyspace(or default keyspace if no keyspace is given)
38
+ #
39
+ # @param [String] kyspc keyspace for which return the connection
40
+ # @return [CassandraComplex::Connection] new instance of Connection
41
+ def connection(kyspc=nil)
42
+ CassandraComplex::Connection.connection(kyspc || self.keyspace)
43
+ end
44
+
45
+ # Execute code within specific keyspace
46
+ #
47
+ # @param [String] kyspc keyspace in which code should be executed
48
+ # @yieldparam [Proc] blck custom code to be executed within keyspace
49
+ def with_keyspace(kyspc, &blck)
50
+ connection.with_keyspace(kyspc, &blck)
51
+ end
52
+
53
+ # Set table name for particular table
54
+ #
55
+ # @param[String] tbl table name
56
+ def set_table_name(tbl)
57
+ @tbl_name = tbl
58
+ end
59
+
60
+ # Return table name
61
+ #
62
+ # @return [String] table name
63
+ def table_name
64
+ @tbl_name || self.name.downcase
65
+ end
66
+
67
+ # Return first part of primary key
68
+ #
69
+ # @return [String] first part of primary key
70
+ def id
71
+ @id ||= connection.key_alias(table_name)
72
+ @id
73
+ end
74
+
75
+ # Return if table is empty
76
+ #
77
+ # @return [Boolean] if table is empty
78
+ def empty?
79
+ all.empty?
80
+ end
81
+
82
+ # Truncate table
83
+ # @return [Boolean] true
84
+ def truncate
85
+ command = "truncate #{table_name};"
86
+ rs = connection.execute(command, true, self)
87
+ true
88
+ end
89
+
90
+ # Raw query execution
91
+ # @param [String] cql_query_string
92
+ # @yieldparam [Proc] blck custom code to be executed within keyspace
93
+ # @return [Array<Hash>] result set as array of hashes
94
+ def execute(cql_query_string, &blck)
95
+ rs = connection.execute(cql_query_string, true, self, [], &blck)
96
+ rs
97
+ end
98
+
99
+ # Return result set for given primary key
100
+ # @see .build_select_clause
101
+ #
102
+ # @param [String, Array<String>, Hash] key key for where clause
103
+ # @param [Hash] clauses select clauses
104
+ # @option clauses [String, Array<String>] where where clause
105
+ # @option clauses [String] order order clause
106
+ # @option clauses [String] limit limit clause
107
+ # @return [Array<Hash>] array of hashes
108
+ def all(key=nil, clauses={}, &blck)
109
+ key = nil if key == :all
110
+
111
+ return_value = nil
112
+ if (!clauses[:select_expression])
113
+ if (clauses[:distinct])
114
+ clauses.merge!({:select_expression=>clauses[:distinct]})
115
+ else
116
+ clauses.merge!({:select_expression=>"*"})
117
+ end
118
+ end
119
+
120
+ command = build_select_clause(key, clauses)
121
+ if clauses[:where].kind_of?(Array)
122
+ bind = clauses[:where][1..-1]
123
+ else
124
+ bind = []
125
+ end
126
+ return_value = connection.execute(command, true, self, bind, &blck)
127
+
128
+ #distinct
129
+ return_value = return_value.map{|row| row[clauses[:distinct]]}.uniq if clauses[:distinct]
130
+
131
+ return_value
132
+ end
133
+
134
+ alias find all
135
+
136
+ # Return count of result set for given primary key
137
+ # @see .build_select_clause
138
+ #
139
+ # @param [String, Array<String>, Hash] key key for where clause
140
+ # @param [Hash] clauses select clauses
141
+ # @option clauses [String, Array<String>] where where clause
142
+ # @option clauses [String] order order clause
143
+ # @option clauses [String] limit limit clause
144
+ # @return [Array<Hash>] result as as array of hashes
145
+ def count(key=nil, clauses={}, &blck)
146
+ key = nil if key == :all
147
+ return_value = nil
148
+ command = build_select_clause(key, clauses.merge({:select_expression=>"count(1)"}))
149
+ if clauses[:where].kind_of?(Array)
150
+ bind = clauses[:where][1..-1]
151
+ else
152
+ bind = []
153
+ end
154
+ rs = connection.execute(command, true, self, bind, &blck)
155
+ if !rs.empty? && rs[0].has_key?('count')
156
+ return_value = rs[0]['count']
157
+ end
158
+
159
+ return_value
160
+ end
161
+
162
+ # Insert record
163
+ # @param [Hash] clauses hash of fields, which need to be inserted
164
+ # @param [Hash] options create options, such as consistency
165
+ # @return [Boolean] always true
166
+ def create(clauses={}, options={})
167
+ return false if clauses.empty?
168
+
169
+ keys = clauses.keys.join(', ')
170
+ values = clauses.values.map{|x| !!options[:sanitize] ? x : CassandraCQL::Statement.quote(CassandraCQL::Statement.cast_to_cql(x))}.join(', ')
171
+
172
+ options_clause = ''
173
+
174
+ if !options.empty?
175
+ options_clause = "using " + options.map{|x,y| ' ' + x.to_s + ' ' + y.to_s + ' '}.join(' AND ')
176
+ end
177
+
178
+ command = "insert into #{table_name} (#{keys}) values (#{values}) #{options_clause}"
179
+ rs = connection.execute(command, true, self)
180
+
181
+ return true
182
+ end
183
+
184
+
185
+ alias update create
186
+
187
+ # Delete record(s)
188
+ # @param [String, Array<String>, Hash] key key for where clause
189
+ # @param [Hash] options options for delete
190
+ # @option options [String] timestamp timestamp of operation
191
+ # @option options [Array<String>] columns columns which should be deleted
192
+ # @option options [Array, String] where where options for delete operation
193
+ # @return [Boolean] always true
194
+ def delete(key=nil,options={})
195
+ return false unless (key || options.has_key?(:where))
196
+
197
+ where_clause = build_where_clause(key, options)
198
+
199
+ consistency_clause = ''
200
+ consistency_clause = " using consistency quorum and timestamp #{options[:timestamp]} " if options[:timestamp]
201
+
202
+ columns_clause = ''
203
+ columns_clause = options[:columns].join(', ') if options[:columns]
204
+ command = "delete #{columns_clause} from #{table_name} #{consistency_clause} #{where_clause}"
205
+
206
+ if options[:where].kind_of?(Array)
207
+ bind = options[:where][1..-1]
208
+ else
209
+ bind = []
210
+ end
211
+ rs = connection.execute(command, true, self, bind)
212
+
213
+ return true
214
+ end
215
+
216
+ protected
217
+
218
+ # Build select clause from clauses hash
219
+ # @see .build_where_clause
220
+ #
221
+ # @param [String, Array<String>, Hash] key key for where clause
222
+ # @param [Hash] clauses select clauses
223
+ # @option clauses [String, Array<String>] where where clause
224
+ # @option clauses [String] order order clause
225
+ # @option clauses [String] limit limit clause
226
+ # @return [String] CQL command
227
+ def build_select_clause(key=nil, clauses={})
228
+ where_clause = build_where_clause(key, clauses)
229
+ order_clause = ''
230
+ limit_clause = ''
231
+ if !clauses.empty?
232
+ order_clause = ' order by ' + clauses[:order] if clauses[:order]
233
+ limit_clause = ' limit ' + clauses[:limit].to_s if clauses[:limit]
234
+ end
235
+ command = "select #{clauses[:select_expression]} from #{table_name} #{where_clause} #{order_clause} #{limit_clause};"
236
+ command
237
+ end
238
+
239
+ # Build where clause from clauses hash
240
+ #
241
+ # @param [String, Array<String>, Hash] key key for where clause
242
+ # @param [Hash] clauses where clauses
243
+ # @option clauses [String, Array<String>] where where clause
244
+ # @return [String] where part of CQL command
245
+ def build_where_clause(key, clauses)
246
+ where_clause = ''
247
+ if clauses[:where].kind_of?(Array)
248
+ where = clauses[:where][0]
249
+ else
250
+ where = clauses[:where]
251
+ end
252
+ if key
253
+ if key.kind_of?(String)
254
+ where_clause = "where #{id} = #{CassandraCQL::Statement.quote(CassandraCQL::Statement.cast_to_cql(key))}"
255
+ elsif key.kind_of?(Array)
256
+ where_clause = "where #{id} in (#{key.map{|x| CassandraCQL::Statement.quote(CassandraCQL::Statement.cast_to_cql(x))}.join(', ')})"
257
+ elsif key.kind_of?(Hash)
258
+ where_clause = "where " + key.map{|x,y| "#{x} = #{CassandraCQL::Statement.quote(CassandraCQL::Statement.cast_to_cql(y))}"}.join(' and ')
259
+ end
260
+ if !clauses.empty? && clauses[:where]
261
+ where_clause << ' and ' + where
262
+ end
263
+ elsif !clauses.empty? && clauses[:where]
264
+ where_clause = 'where ' + where
265
+ end
266
+ where_clause
267
+ end
268
+
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,203 @@
1
+ require 'spec_helper'
2
+
3
+ class TimelineModel < CassandraComplex::Model
4
+ table 'timeline'
5
+
6
+ attribute :user_id, 'varchar'
7
+ attribute :tweet_id, 'int'
8
+ attribute :author, 'varchar'
9
+ attribute :body, 'varchar'
10
+
11
+ primary_key :user_id, :tweet_id
12
+ end
13
+
14
+ class Tickets < CassandraComplex::Model
15
+ table 'tickets'
16
+
17
+ attribute :user_id, 'int'
18
+ attribute :owner, 'varchar'
19
+ attribute :time, 'timestamp'
20
+
21
+ primary_key :user_id
22
+ end
23
+
24
+ #CassandraComplex::Configuration.logger = Logger.new('/dev/null')
25
+
26
+ describe 'Model' do
27
+
28
+ before :all do
29
+ conn = CassandraComplex::Connection.new('127.0.0.1:9160')
30
+ conn.execute('CREATE KEYSPACE cassandra_complex_test WITH strategy_class = \'SimpleStrategy\' AND strategy_options:replication_factor = 1;')
31
+ CassandraComplex::Configuration.read({'host'=>'127.0.0.1:9160', 'default_keyspace'=>'cassandra_complex_test'})
32
+ TimelineModel.create_table
33
+ Tickets.create_table
34
+ end
35
+
36
+ after :all do
37
+ conn = CassandraComplex::Connection.new('127.0.0.1:9160')
38
+ TimelineModel.drop_table
39
+ Tickets.drop_table
40
+ conn.execute('DROP KEYSPACE cassandra_complex;')
41
+ end
42
+
43
+ context 'basic operations' do
44
+ before :each do
45
+ TimelineModel.truncate
46
+ Tickets.truncate
47
+ end
48
+
49
+ it 'returns schema' do
50
+ TimelineModel.schema.should == {:table => 'timeline', :attributes=>{:user_id => 'varchar', :tweet_id => 'int', :author => 'varchar', :body => 'varchar'}, :primary_key => [:user_id, :tweet_id]}
51
+ Tickets.schema.should == {:table => 'tickets', :attributes=>{:user_id => 'int', :owner => 'varchar', :time => 'timestamp'}, :primary_key => [:user_id]}
52
+ end
53
+
54
+ it 'checks equality of two models' do
55
+ timeline1 = TimelineModel.new({'user_id' => 'test_user1', 'tweet_id' => 1, 'author' => 'test_author1', 'body' => 'test_body1'})
56
+ timeline2 = TimelineModel.new({'user_id' => 'test_user1', 'tweet_id' => 1, 'author' => 'test_author1', 'body' => 'test_body1'})
57
+
58
+ timeline1.should == timeline2
59
+
60
+ timeline3 = TimelineModel.new({'user_id' => 'test_user3', 'tweet_id' => 3, 'author' => 'test_author3', 'body' => 'test_body3'})
61
+ timeline1.should_not == timeline3
62
+
63
+ tickets = Tickets.new({:user_id => 10, :owner=> 'Tim Collins', :time => Time.now})
64
+
65
+ tickets.should_not == timeline3
66
+ end
67
+
68
+ it 'implements dirtiness' do
69
+ timeline1 = TimelineModel.new({'user_id' => 'test_user1', 'tweet_id' => 1, 'author' => 'test_author1', 'body' => 'test_body1'})
70
+ timeline1.dirty?.should == true
71
+
72
+ timeline1.save
73
+ timeline1.dirty?.should == false
74
+
75
+ timeline1 == TimelineModel.all[0]
76
+ timeline1.dirty?.should == false
77
+ timeline1.author = 'test_author42'
78
+ timeline1.dirty?.should == true
79
+ timeline1.save
80
+
81
+ timelines = TimelineModel.all
82
+ timelines.size.should == 1
83
+ timeline1 = timelines[0]
84
+
85
+ timeline1.user_id.should == 'test_user1'
86
+ timeline1.tweet_id.should == 1
87
+ timeline1.author.should == 'test_author42'
88
+ timeline1.body.should == 'test_body1'
89
+ end
90
+ end
91
+
92
+ context 'creating new model' do
93
+ before (:each) do
94
+ TimelineModel.truncate
95
+ Tickets.truncate
96
+ end
97
+
98
+ it 'with Hash' do
99
+ timeline1 = TimelineModel.new({'user_id' => 'test_user1', 'tweet_id' => 1, 'author' => 'test_author1', 'body' => 'test_body1'})
100
+
101
+ timeline1.user_id.should == 'test_user1'
102
+ timeline1.tweet_id.should == 1
103
+ timeline1.author.should == 'test_author1'
104
+ timeline1.body.should == 'test_body1'
105
+
106
+ ticket1 = Tickets.new({:user_id => 10, :owner=> 'Tim Collins', :time => Time.now})
107
+
108
+ ticket1.user_id.should == 10
109
+ ticket1.owner.should == 'Tim Collins'
110
+ end
111
+
112
+ it 'with assigment' do
113
+ timeline = TimelineModel.new
114
+
115
+ timeline.user_id = 'test_user2'
116
+ timeline.tweet_id = 2
117
+ timeline.author = 'test_author2'
118
+ timeline.body = 'test_body2'
119
+
120
+ timeline.user_id.should == 'test_user2'
121
+ timeline.tweet_id.should == 2
122
+ timeline.author.should == 'test_author2'
123
+ timeline.body.should == 'test_body2'
124
+ end
125
+
126
+ it 'creating and saving new model within DB' do
127
+ timeline_created = TimelineModel.create({'user_id' => 'test_user1', 'tweet_id' => 1, 'author' => 'test_author1', 'body' => 'test_body1'})
128
+
129
+ timeline_created.user_id.should == 'test_user1'
130
+ timeline_created.tweet_id.should == 1
131
+ timeline_created.author.should == 'test_author1'
132
+ timeline_created.body.should == 'test_body1'
133
+
134
+ timeline_found = TimelineModel.find('test_user1', :where=>['tweet_id = ?', 1])
135
+ timeline_created.should == timeline_found.first
136
+ end
137
+ end
138
+
139
+ context 'saving model' do
140
+
141
+ before (:each) do
142
+ TimelineModel.truncate
143
+ end
144
+
145
+ it 'just save' do
146
+ timeline = TimelineModel.new({'user_id' => 'test_user1', 'tweet_id' => 1, 'author' => 'test_author1', 'body' => 'test_body1'})
147
+
148
+ timeline.save
149
+
150
+ TimelineModel.all.size.should == 1
151
+ end
152
+ end
153
+
154
+ context 'deleting model' do
155
+ before (:each) do
156
+ TimelineModel.truncate
157
+ TimelineModel.create({'user_id' => 'test_user1', 'tweet_id' => 1, 'author' => 'test_author1', 'body' => 'test_body1'})
158
+ end
159
+
160
+ it 'as class method' do
161
+ TimelineModel.delete({'user_id' => 'test_user1', 'tweet_id' => 1})
162
+ TimelineModel.all.size.should == 0
163
+ end
164
+
165
+ it 'as instance method' do
166
+ TimelineModel.find('test_user1', :where=>['tweet_id = ?', 1])[0].delete
167
+ TimelineModel.all.size.should == 0
168
+ end
169
+
170
+ end
171
+
172
+ context 'all, find, count, etc' do
173
+
174
+ before (:each) do
175
+ TimelineModel.truncate
176
+ TimelineModel.create({'user_id' => 'test_user0', 'tweet_id' => 0, 'author' => 'test_author0', 'body' => 'test_body0'})
177
+ TimelineModel.create({'user_id' => 'test_user0', 'tweet_id' => 1, 'author' => 'test_author1', 'body' => 'test_body1'})
178
+ TimelineModel.create({'user_id' => 'test_user2', 'tweet_id' => 2, 'author' => 'test_author2', 'body' => 'test_body2'})
179
+ TimelineModel.create({'user_id' => 'test_user3', 'tweet_id' => 3, 'author' => 'test_author3', 'body' => 'test_body3'})
180
+ end
181
+
182
+
183
+ it 'count' do
184
+ TimelineModel.count.should == 4
185
+ TimelineModel.count('test_user0').should == 2
186
+ TimelineModel.count(:all, {:where=>['user_id = ? and tweet_id = ?', 'test_user0', 0]}).should == 1
187
+ TimelineModel.count({'user_id' => 'test_user0'}).should == 2
188
+ end
189
+
190
+ it 'all' do
191
+ TimelineModel.all.size.should == 4
192
+ TimelineModel.all({:where=>['user_id = ?', 'test_user0']}).size.should == 2
193
+ end
194
+
195
+ it 'find' do
196
+ TimelineModel.find(:all).size.should == 4
197
+ TimelineModel.find(:all, {:where=>['user_id = ?', 'test_user0']}).size.should == 2
198
+ TimelineModel.find({'user_id' => 'test_user0', 'tweet_id' => 1}).size.should == 1
199
+ end
200
+
201
+ end
202
+
203
+ end