cassilds-model 0.0.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.
@@ -0,0 +1,280 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+ require 'active_support/core_ext/array/extract_options'
3
+ module CassandraModel
4
+ module Persistence
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.send :include, InstanceMethods
8
+ end
9
+
10
+ module InstanceMethods
11
+ def save
12
+ raise ActiveRecord::ReadOnlyRecord if readonly?
13
+ return self unless valid?
14
+ run_callbacks :before_save
15
+ newrec = new_record?
16
+ callback = newrec ? :before_create : :before_update
17
+ run_callbacks callback
18
+ write(attributes)
19
+ callback = newrec ? :after_create : :after_update
20
+ run_callbacks callback
21
+ run_callbacks :after_save
22
+ end
23
+
24
+ def destroy
25
+ run_callbacks :before_destroy
26
+ self.class.send(:remove, key)
27
+ self.deleted!
28
+ run_callbacks :after_destroy
29
+ end
30
+
31
+ def reload
32
+ self.class.get(key)
33
+ end
34
+
35
+ def update_attributes(attrs)
36
+ self.attributes = attrs
37
+ save
38
+ end
39
+
40
+ def remove_attributes(attrs)
41
+ attrs.each {|attr| self.class.remove_column(key, attr) }
42
+ end
43
+
44
+ private
45
+
46
+ def write(attrs)
47
+ self.class.send(:write, key, attrs)
48
+ @new_record = false
49
+ self
50
+ end
51
+ end
52
+
53
+ module ClassMethods
54
+ attr_writer :write_consistency_level, :read_consistency_level
55
+
56
+ def create(attributes)
57
+ new(attributes).tap do |object|
58
+ object.save
59
+ end
60
+ end
61
+
62
+ def write_consistency_level(level)
63
+ @write_consistency_level = level
64
+ end
65
+
66
+ def read_consistency_level(level)
67
+ @read_consistency_level = level
68
+ end
69
+
70
+ def get(key, options = {})
71
+ find_one(key, options)
72
+ end
73
+
74
+ def [](key)
75
+ record = get(key)
76
+ raise RecordNotFound, "cannot find out key=`#{key}` in `#{column_family}`" unless record
77
+ record
78
+ end
79
+
80
+ def exists?(key)
81
+ return false if key.nil? || key == ''
82
+ #connection.exists?(column_family, key)
83
+ key = validate_key_type(key)
84
+ !connection.get(column_family, key).empty?
85
+ end
86
+
87
+ def all(*args)
88
+ find(:all, *args)
89
+ end
90
+
91
+ def first(*args)
92
+ find(:first, *args)
93
+ end
94
+
95
+ def last(*args)
96
+ find(:last, *args)
97
+ end
98
+
99
+ def find(*args)
100
+ options = args.extract_options!
101
+ validate_find_options(options)
102
+ # set_readonly_option!(options) Not Implemented Yet
103
+
104
+ case args.first
105
+ when :first then find_initial(options)
106
+ when :last then find_last(options)
107
+ when :all then find_every(options)
108
+ else find_from_ids(args, options)
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def write(key, attributes)
115
+ benchmark("CassandraModel[#{self.name}].write, key: #{key.to_s}") do
116
+ connection.insert(column_family, key, attrs2write(attributes),
117
+ :consistency => @write_consistency_level || Cassandra::Consistency::QUORUM)
118
+ end
119
+ key
120
+ end
121
+
122
+ def attrs2write attrs
123
+ res = {}
124
+ attrs.each do |key, item|
125
+ res[key] = item.kind_of?(Integer) ? Cassandra::Long.new(item).to_s : item
126
+ end
127
+ return res
128
+ end
129
+
130
+ def remove(key)
131
+ key = validate_key_type(key)
132
+ benchmark("CassandraModel[#{self.name}].remove, key: #{key.to_s}") do
133
+ connection.remove(column_family, key,
134
+ :consistency => @write_consistency_level || Cassandra::Consistency::QUORUM)
135
+ end
136
+ end
137
+
138
+ def remove_column(key, column)
139
+ key = validate_key_type(key)
140
+ benchmark("CassandraModel[#{self.name}].remove_column, key: #{key.to_s}, #{column.to_s}") do
141
+ connection.remove(column_family, key, column,
142
+ :consistency => @write_consistency_level || Cassandra::Consistency::QUORUM)
143
+ end
144
+ end
145
+
146
+ def init_model(key, attrs)
147
+ return new(attrs, false).tap do |object|
148
+ object.key = key
149
+ object.new_record = false
150
+ object.deleted! if attrs.empty?
151
+ end
152
+ end
153
+
154
+ def find_one(key, options)
155
+ key = validate_key_type(key)
156
+ raise RecordNotFound, "Couldn't find #{name} without an ID" if key.blank?
157
+ options = options || {}
158
+ readonly = options.delete(:readonly)
159
+
160
+ model = nil
161
+ benchmark("CassandraModel[#{self.name}].find_one, key: #{key.to_s}") do
162
+ attrs = connection.get_columns(column_family, key, columns.stringify_keys.keys, options)
163
+ unless attrs.empty?
164
+ model = init_model(key, attrs)
165
+ model.readonly! if readonly
166
+ end
167
+ end
168
+ return model
169
+ end
170
+
171
+ def find_initial(options)
172
+ options = options || {}
173
+ options.update(:limit => 1)
174
+ return find_every(options).first
175
+ end
176
+
177
+ def find_last(options)
178
+ options = options || {}
179
+ # in practice this does NOT work (Thrift API limitation)
180
+ options.update({:limit => 1, :order => :desc, :reversed => true})
181
+ return find_every(options).first
182
+ end
183
+
184
+ def find_every(options)
185
+ options = options || {}
186
+ nofilter = options.delete(:no_filter)
187
+ readonly = options.delete(:readonly)
188
+ keyrange = options[:keyrange]
189
+ keyrange = ''..'' if keyrange.blank?
190
+
191
+ records = []
192
+ benchmark("CassandraModel[#{self.name}].find_every, keyrange: #{keyrange.to_s}") do
193
+ results = connection.get_range_columns(column_family, columns.stringify_keys.keys,
194
+ :start => keyrange.first, :finish => keyrange.last, :count => (options[:limit] || 100))
195
+
196
+ (results || {}).each { |key, columns|
197
+ model = init_model(key, columns)
198
+ model.readonly! if readonly
199
+ records.push(model) unless nofilter && model.deleted?
200
+ }
201
+ end
202
+ return records
203
+ end
204
+
205
+ def find_from_ids(ids, options)
206
+ if ids.first.kind_of?(Range)
207
+ find_range(ids.first, options)
208
+ else
209
+ expects_array = ids.first.kind_of?(Array)
210
+ return ids.first if expects_array && ids.first.empty?
211
+
212
+ ids = ids.flatten.compact.uniq
213
+
214
+ case ids.size
215
+ when 0
216
+ raise RecordNotFound, "Couldn't find #{name} without an ID"
217
+ when 1
218
+ result = find_one(ids.first, options)
219
+ expects_array ? [ result ] : result
220
+ else
221
+ find_some(ids, options)
222
+ end
223
+ end
224
+ end
225
+
226
+ def find_range(ids, options)
227
+ options = options || {}
228
+ options.update({:keyrange => ids})
229
+ find_every(options)
230
+ end
231
+
232
+ def find_some(ids, options)
233
+ options = options || {}
234
+ result = find_every(options)
235
+
236
+ # Determine expected size from limit and offset, not just ids.size.
237
+ expected_size =
238
+ if options[:limit] && ids.size > options[:limit]
239
+ options[:limit]
240
+ else
241
+ ids.size
242
+ end
243
+
244
+ # 11 ids with limit 3, offset 9 should give 2 results.
245
+ if options[:offset] && (ids.size - options[:offset] < expected_size)
246
+ expected_size = ids.size - options[:offset]
247
+ end
248
+
249
+ if result.size == expected_size
250
+ result
251
+ else
252
+ raise RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids.inspect}) (found #{result.size} results, but was looking for #{expected_size})"
253
+ end
254
+ end
255
+
256
+ VALID_FIND_OPTIONS = [ :select, :keyrange, :conditions, :limit, :offset,
257
+ :order, :readonly ]
258
+
259
+ def validate_find_options(options) #:nodoc:
260
+ options.assert_valid_keys(VALID_FIND_OPTIONS)
261
+ end
262
+
263
+ def validate_key_type(key)
264
+ if !self.key_type.blank? and self.key_type.downcase == 'time_uuid'
265
+ key = SimpleUUID::UUID.new(key)
266
+ end
267
+ return key.to_s
268
+ end
269
+
270
+ # def first(keyrange = ''..'', options = {})
271
+ # all(keyrange, options.merge(:limit => 1)).first
272
+ # end
273
+
274
+ # def last(keyrange = ''..'', options = {})
275
+ # all(keyrange, options.merge({:reversed => true, :limit => 1})).first
276
+ # end
277
+
278
+ end
279
+ end
280
+ end
@@ -0,0 +1,15 @@
1
+ require 'cassandra-model/config'
2
+ module CassandraModel
3
+ if defined? Rails::Railtie
4
+ require 'rails'
5
+ class Railtie < Rails::Railtie
6
+ initializer 'cassandra-model.config_init' do
7
+ CassandraModel::Config.initialize
8
+ end
9
+ rake_tasks do
10
+ load "tasks/paperclip.rake"
11
+ end
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,61 @@
1
+ module CassandraModel
2
+ class StringType
3
+ def self.dump(v)
4
+ v && v
5
+ end
6
+
7
+ def self.load(v)
8
+ v && v.to_s
9
+ end
10
+ end
11
+
12
+ class IntegerType
13
+ def self.dump(v)
14
+ v && v.to_i
15
+ end
16
+
17
+ def self.load(v)
18
+ v && Cassandra::Long.new(v).to_i
19
+ end
20
+ end
21
+
22
+ class FloatType
23
+ def self.dump(v)
24
+ v && v.to_s
25
+ end
26
+
27
+ def self.load(v)
28
+ v && v.to_f
29
+ end
30
+ end
31
+
32
+ class DatetimeType
33
+ def self.dump(v)
34
+ !v.blank? && v.strftime('%FT%T%z')
35
+ end
36
+
37
+ def self.load(v)
38
+ !v.blank? && ::DateTime.strptime(v, '%FT%T%z')
39
+ end
40
+ end
41
+
42
+ class JsonType
43
+ def self.dump(v)
44
+ v && ::JSON.dump(v)
45
+ end
46
+
47
+ def self.load(v)
48
+ v && ::JSON.load(v)
49
+ end
50
+ end
51
+
52
+ class BooleanType
53
+ def self.dump(v)
54
+ v == '1'
55
+ end
56
+
57
+ def self.load(v)
58
+ v ? '1' : '0'
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,3 @@
1
+ class CassandraModel
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,30 @@
1
+ require 'cassandra/0.7'
2
+ require 'forwardable'
3
+ require 'date'
4
+
5
+ $:.unshift(File.dirname(__FILE__))
6
+
7
+ module CassandraModel
8
+ class CassandraModelError < StandardError; end
9
+ class UnknownRecord < CassandraModelError; end
10
+ class InvalidRecord < CassandraModelError; end
11
+ class RecordNotFound < CassandraModelError; end
12
+ end
13
+
14
+ unless Object.respond_to? :tap
15
+ class Object
16
+ def tap(value)
17
+ yield(value)
18
+ value
19
+ end
20
+ end
21
+ end
22
+
23
+ require 'cassandra-model/types'
24
+ require 'active_support/callbacks'
25
+ require 'cassandra-model/config'
26
+ require 'cassandra-model/connection'
27
+ require 'cassandra-model/persistence'
28
+ require 'cassandra-model/batches'
29
+ require 'cassandra-model/base'
30
+ #require 'cassandra-model/railtie'
data/test/base_test.rb ADDED
@@ -0,0 +1,45 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2
+
3
+ class CassandraModelCallbacksTest < Test::Unit::TestCase
4
+ context "CassandraModel::Base" do
5
+ setup do
6
+ @klass = Class.new(CassandraModel::Base) do
7
+ key :name
8
+ column :age, :integer
9
+ column :dob, :datetime
10
+ column :note, :json
11
+
12
+ validate do
13
+ self.errors << "dob required" if dob.nil?
14
+ end
15
+ end
16
+
17
+ @klass.establish_connection 'cassandra-model'
18
+ end
19
+
20
+ should "connect to cassandra" do
21
+ assert_kind_of Cassandra, @klass.connection
22
+ end
23
+
24
+ should "store all defined columns" do
25
+ assert_equal({:age => :integer ,
26
+ :dob => :datetime,
27
+ :note => :json} , @klass.columns)
28
+ end
29
+
30
+ should "validate model by provided block" do
31
+ assert_kind_of Proc, @klass.validation
32
+
33
+ model = @klass.new()
34
+ assert !model.valid?
35
+
36
+ model = @klass.new(:name => "tl")
37
+ assert !model.valid?
38
+
39
+ model = @klass.new(:name => "tl", :dob => DateTime.now)
40
+ assert model.valid?
41
+ assert_equal "tl", model.key
42
+ assert_kind_of DateTime, model.dob
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2
+
3
+ class CassandraModelCallbacksTest < Test::Unit::TestCase
4
+ context "CassandraModel::Callbacks" do
5
+ setup do
6
+ @base = Class.new(Object) do
7
+ include CassandraModel::Callbacks
8
+ define_callbacks :foo
9
+ end
10
+
11
+ @klass = Class.new(@base) do
12
+ def bar; @n = [:bar]; end
13
+
14
+ def foo
15
+ run_callbacks(:foo) { @n << :foo }
16
+ end
17
+
18
+ def baz(v)
19
+ @n << :baz if v == [:bar, :foo]
20
+ end
21
+
22
+ def quux; @n << :quux; end
23
+ end
24
+ end
25
+
26
+ should "provide before and after callbacks for foo function" do
27
+ assert @klass.respond_to?(:define_callbacks)
28
+ assert @klass.respond_to?(:callbacks)
29
+ assert @klass.respond_to?(:before_foo)
30
+ assert @klass.respond_to?(:after_foo)
31
+ assert_equal Hash.new, @klass.callbacks
32
+ end
33
+
34
+ should "invoke callback functions when foo executed" do
35
+ @klass.send(:before_foo, :bar)
36
+ @klass.send(:after_foo, :baz, :quux)
37
+ assert_equal 2, @klass.callbacks.length
38
+ assert_equal [:bar], @klass.callbacks[:before_foo]
39
+ assert_equal [:baz, :quux], @klass.callbacks[:after_foo]
40
+ assert_equal [:bar, :foo, :baz, :quux], @klass.new.foo
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,84 @@
1
+
2
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
3
+
4
+ class User < CassandraModel::Base
5
+ column_family :Users
6
+
7
+ key :username
8
+ column :full_name
9
+ column :created_at, :datetime
10
+
11
+ write_consistency_level Cassandra::Consistency::ALL
12
+
13
+ before_save :set_default_time
14
+
15
+ validate do
16
+ errors << "full name required" if full_name.nil? || full_name.empty?
17
+ end
18
+
19
+ private
20
+
21
+ def set_default_time
22
+ self.created_at = Time.now
23
+ end
24
+ end
25
+
26
+ class CassandraModelTest < Test::Unit::TestCase
27
+ context "CassandraModel" do
28
+ setup do
29
+ @connection = CassandraModel::Base.establish_connection("CassandraModel")
30
+ @connection.clear_keyspace!
31
+
32
+ @user = User.create(:username => "tl", :full_name => "tien le")
33
+ end
34
+
35
+ should "be able to connect to Cassandra" do
36
+ assert_kind_of Cassandra, @connection
37
+ assert_equal "CassandraModel", @connection.keyspace
38
+ end
39
+
40
+ should "not create a new user when validation fails" do
41
+ user = User.create(:username => "tl")
42
+ assert !user.valid?
43
+ assert user.new_record?
44
+
45
+ user = User.new(:username => "tl").save
46
+ assert user.new_record?
47
+ assert_equal "full name required", user.errors.first
48
+
49
+ user = User.new(:full_name => "tl").save
50
+ assert_equal "key required", user.errors.first
51
+ end
52
+
53
+ should "create a new user when validation passed" do
54
+ assert !@user.new_record?
55
+ assert @user.eql?(User.get("tl"))
56
+ assert_equal @user, User.get("tl")
57
+ assert_equal "tien le", User.get("tl").full_name
58
+
59
+ user = User.new(:username => "abc", :full_name => "Foo")
60
+ user.save
61
+ assert_equal ["created_at", "full_name"], @connection.get(:Users, "abc").keys
62
+ end
63
+
64
+ should "destroy a record" do
65
+ @user.destroy
66
+ assert User.get("tl").nil?
67
+ assert User.get(nil).nil?
68
+
69
+ assert_raise(CassandraModel::RecordNotFound) { User["tl"] }
70
+ assert_raise(CassandraModel::RecordNotFound) { User[nil] }
71
+ end
72
+
73
+ should "return true if record exists and otherwise" do
74
+ assert User.exists?("tl")
75
+ assert !User.exists?("foo")
76
+ end
77
+
78
+ should "only take defined attributes" do
79
+ user = User.new(:username => "abc", :full_name => "Foo", :hachiko => 'dog')
80
+ user.save
81
+ assert_equal ["created_at", "full_name"], @connection.get(:Users, "abc").keys
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,47 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ # The directory where Cassandra's configs live (required)
18
+ CASSANDRA_CONF=$CASSANDRA_CONF
19
+
20
+ # This can be the path to a jar file, or a directory containing the
21
+ # compiled classes. NOTE: This isn't needed by the startup script,
22
+ # it's just used here in constructing the classpath.
23
+ cassandra_bin=$CASSANDRA_HOME/build/classes
24
+
25
+ # The java classpath (required)
26
+ CLASSPATH=$CASSANDRA_CONF:$CASSANDRA_BIN
27
+
28
+ for jar in $CASSANDRA_HOME/lib/*.jar $CASSANDRA_HOME/build/lib/jars/*.jar; do
29
+ CLASSPATH=$CLASSPATH:$jar
30
+ done
31
+
32
+ # Arguments to pass to the JVM
33
+ JVM_OPTS=" \
34
+ -ea \
35
+ -Xms128M \
36
+ -Xmx1G \
37
+ -XX:TargetSurvivorRatio=90 \
38
+ -XX:+AggressiveOpts \
39
+ -XX:+UseParNewGC \
40
+ -XX:+UseConcMarkSweepGC \
41
+ -XX:+CMSParallelRemarkEnabled \
42
+ -XX:+HeapDumpOnOutOfMemoryError \
43
+ -XX:SurvivorRatio=128 \
44
+ -XX:MaxTenuringThreshold=0 \
45
+ -Dcom.sun.management.jmxremote.port=8080 \
46
+ -Dcom.sun.management.jmxremote.ssl=false \
47
+ -Dcom.sun.management.jmxremote.authenticate=false"
@@ -0,0 +1,27 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ # for production, you should probably set the root to INFO
18
+ # and the pattern to %c instead of %l. (%l is slower.)
19
+
20
+ # output messages into a rolling log file as well as stdout
21
+ log4j.rootLogger=WARN,stderr
22
+
23
+ # stderr
24
+ log4j.appender.stderr=org.apache.log4j.ConsoleAppender
25
+ log4j.appender.stderr.target=System.err
26
+ log4j.appender.stderr.layout=org.apache.log4j.PatternLayout
27
+ log4j.appender.stderr.layout.ConversionPattern=%5p %d{HH:mm:ss,SSS} %m%n
@@ -0,0 +1,40 @@
1
+ # Licensed to the Apache Software Foundation (ASF) under one
2
+ # or more contributor license agreements. See the NOTICE file
3
+ # distributed with this work for additional information
4
+ # regarding copyright ownership. The ASF licenses this file
5
+ # to you under the Apache License, Version 2.0 (the
6
+ # "License"); you may not use this file except in compliance
7
+ # with the License. You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ # for production, you should probably set the root to INFO
18
+ # and the pattern to %c instead of %l. (%l is slower.)
19
+
20
+ # output messages into a rolling log file as well as stdout
21
+ log4j.rootLogger=INFO,stdout,R
22
+
23
+ # stdout
24
+ log4j.appender.stdout=org.apache.log4j.ConsoleAppender
25
+ log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
26
+ log4j.appender.stdout.layout.ConversionPattern=%5p %d{HH:mm:ss,SSS} %m%n
27
+
28
+ # rolling log file
29
+ log4j.appender.R=org.apache.log4j.RollingFileAppender
30
+ log4j.appender.file.maxFileSize=20MB
31
+ log4j.appender.file.maxBackupIndex=50
32
+ log4j.appender.R.layout=org.apache.log4j.PatternLayout
33
+ log4j.appender.R.layout.ConversionPattern=%5p [%t] %d{ISO8601} %F (line %L) %m%n
34
+ # Edit the next line to point to your logs directory
35
+ log4j.appender.R.File=data/logs/system.log
36
+
37
+ # Application logging options
38
+ #log4j.logger.com.facebook=DEBUG
39
+ #log4j.logger.com.facebook.infrastructure.gms=DEBUG
40
+ #log4j.logger.com.facebook.infrastructure.db=DEBUG