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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in cassandra-model.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Tien Le
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,57 @@
1
+ = cassilds-model
2
+
3
+ Simple model support for Cassandra (http://github.com/fauna/cassandra)
4
+
5
+ Currently, it supports:
6
+ * Keys (_String_, _Time_UUID_)
7
+ * Serialisation (_String_, _Integer_, _Float_, _Boolean_, _DateTime_, _JSON_)
8
+ * Callbacks
9
+ * Validations
10
+
11
+ == Getting started
12
+
13
+ gem install cassilds-model
14
+
15
+ == Define
16
+ require 'cassandra-model'
17
+
18
+ class User << CassandraModel::Base
19
+ column_family :Users
20
+
21
+ key :username, :string
22
+ column :full_name
23
+ column :created, :datetime
24
+ column :profile, :json
25
+
26
+ write_consistency_level Cassandra::Consistency::ALL
27
+ before_save :set_created_at
28
+
29
+ validation do
30
+ errors << "full name required" if full_name.blank?
31
+ end
32
+
33
+ private
34
+
35
+ def set_created_at
36
+ self.created = Time.now
37
+ end
38
+ end
39
+
40
+
41
+ == CRUD
42
+ User.create(:username => 'foo', :full_name => 'foo bar')
43
+
44
+ foo = User.new(:username => 'foo', :full_name => 'foo bar')
45
+ foo.save
46
+
47
+ foo.full_name = 'foo baz'
48
+ foo.save
49
+
50
+ foo = User['foo']
51
+ foo = User.get('foo')
52
+
53
+ foo.destroy
54
+
55
+ == Copyright
56
+
57
+ Copyright (c) 2010 Tien Le. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler'
2
+ require "rspec/core/rake_task"
3
+
4
+ task :default => [:spec]
5
+
6
+ desc "Run specs"
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "cassandra-model/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "cassilds-model"
7
+ s.version = CassandraModel::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Tien Le", "Umanni"]
10
+ s.email = ["tienlx@gmail.com", "ygor@umanni.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Simple model support for Cassandra ColumnFamily/SuperColumnFamily mapping}
13
+ s.description = %q{Cassandra-model allows you to map ColumnFamily/SuperColumnFamily in Cassandra to Ruby objects. It was designed to be fast and simple.}
14
+ s.extra_rdoc_files = [
15
+ "LICENSE",
16
+ "README.rdoc"
17
+ ]
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ s.require_paths = ["lib"]
23
+
24
+ s.add_dependency 'shoulda', [">= 0"]
25
+ s.add_dependency 'cassilds', [">= 0.9.1"]
26
+ s.add_dependency 'simple_uuid', [">= 0"]
27
+ end
@@ -0,0 +1,183 @@
1
+ require 'benchmark'
2
+ require 'simple_uuid'
3
+ module CassandraModel
4
+ class Base
5
+ extend Forwardable
6
+ include ActiveSupport::Callbacks
7
+ include CassandraModel::Persistence
8
+ include CassandraModel::Batches
9
+
10
+ def_delegators :self.class, :connection, :connection=
11
+ define_callbacks :before_save, :after_save, :before_create, :after_create, :before_update, :after_update, :before_destroy, :after_destroy
12
+
13
+ class << self
14
+
15
+ @@logger = nil
16
+
17
+ def establish_connection
18
+ Connection.establish_connection
19
+ end
20
+
21
+ def connection
22
+ # Connection.establish_connection
23
+ Connection.connection
24
+ end
25
+
26
+ def column_family(name = nil)
27
+ @column_family || (@column_family = name || self.name.split('::').last)
28
+ end
29
+
30
+ def key(name, type = :string)
31
+ @key_type = type.to_s
32
+ class_eval <<-EVAL, __FILE__, __LINE__ + 1
33
+ def #{name}=(value)
34
+ @key_type = '#{type}'
35
+ if @key_type.downcase == 'time_uuid'
36
+ value = SimpleUUID::UUID.new(value)
37
+ end
38
+ @key = value.to_s;
39
+ end
40
+ def #{name}
41
+ @key;
42
+ end
43
+ EVAL
44
+ end
45
+
46
+ def column(name, type = :string)
47
+ columns[name] = type
48
+ class_eval "def #{name}; #{type.capitalize}Type.load(@attributes['#{name}']); end"
49
+ class_eval "def #{name}=(value); @attributes['#{name}'] = #{type.capitalize}Type.dump(value); end"
50
+ end
51
+
52
+ def validate(&block)
53
+ raise ArgumentError.new('provide a block that does validation') unless block_given?
54
+ @validation = block
55
+ end
56
+
57
+ def validation
58
+ @validation
59
+ end
60
+
61
+ def columns
62
+ @columns ||= {}
63
+ end
64
+
65
+ def logger
66
+ return @@logger || RAILS_DEFAULT_LOGGER
67
+ end
68
+
69
+ def logger=(obj)
70
+ @@logger = obj
71
+ end
72
+
73
+ # Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the
74
+ # primary_key_prefix_type setting, though.
75
+ def primary_key
76
+ 'id'
77
+ end
78
+
79
+ def key_type
80
+ @key_type
81
+ end
82
+
83
+ def key_type=(value)
84
+ @key_type = value
85
+ end
86
+
87
+ # Log and benchmark multiple statements in a single block. Example:
88
+ #
89
+ # Project.benchmark("Creating project") do
90
+ # project = Project.create("name" => "stuff")
91
+ # project.create_manager("name" => "David")
92
+ # project.milestones << Milestone.find(:all)
93
+ # end
94
+ #
95
+ # The benchmark is only recorded if the current level of the logger is less than or equal to the <tt>log_level</tt>,
96
+ # which makes it easy to include benchmarking statements in production software that will remain inexpensive because
97
+ # the benchmark will only be conducted if the log level is low enough.
98
+ #
99
+ # The logging of the multiple statements is turned off unless <tt>use_silence</tt> is set to false.
100
+ def benchmark(title, log_level = Logger::DEBUG, use_silence = true)
101
+ if logger && logger.level <= log_level
102
+ result = nil
103
+ ms = 1000 * Benchmark.realtime { result = use_silence ? silence { yield } : yield }
104
+ logger.add(log_level, '%s (%.1fms)' % [title, ms])
105
+ result
106
+ else
107
+ yield
108
+ end
109
+ end
110
+
111
+ # Silences the logger for the duration of the block.
112
+ def silence
113
+ old_logger_level, logger.level = logger.level, Logger::ERROR if logger
114
+ yield
115
+ ensure
116
+ logger.level = old_logger_level if logger
117
+ end
118
+
119
+ private
120
+
121
+ def inherited(child)
122
+ child.instance_variable_set('@connection', @connection)
123
+ super
124
+ end
125
+ end
126
+
127
+ attr_accessor :key, :new_record, :key_type
128
+ attr_reader :attributes, :errors
129
+
130
+ def initialize(attrs = {}, convert = true)
131
+ @new_record = true
132
+ @errors = []
133
+ @attributes = {}
134
+ if convert
135
+ self.attributes = attrs
136
+ else
137
+ @attributes = attrs
138
+ end
139
+ end
140
+
141
+ def attributes=(attrs)
142
+ attrs.each {|k, v| send("#{k}=", v) if respond_to? "#{k}=".to_sym }
143
+ end
144
+
145
+ def valid?
146
+ @errors << "key required" if key.to_s !~ /\S/
147
+ self.instance_eval(&self.class.validation) unless self.class.validation.nil?
148
+ @errors.empty?
149
+ end
150
+
151
+ def new_record?
152
+ @new_record
153
+ end
154
+
155
+ def ==(other)
156
+ true
157
+ end
158
+
159
+ # Returns +true+ if the record is read only.
160
+ def readonly?
161
+ defined?(@readonly) && @readonly == true
162
+ end
163
+
164
+ # Marks a entity as readonly
165
+ def readonly!
166
+ @readonly = true
167
+ end
168
+
169
+ # When a entity with no attributes is read from the datasource it is auto
170
+ # marked as deleted.
171
+ def deleted?
172
+ defined?(@deleted) && @deleted == true
173
+ end
174
+
175
+ # Marks a entity as deleted
176
+ def deleted!
177
+ @deleted = true
178
+ end
179
+
180
+ alias :eql? ==
181
+ end
182
+
183
+ end
@@ -0,0 +1,89 @@
1
+ #Mimic ActiveRecord Batches
2
+ module CassandraModel
3
+ module Batches # :nodoc:
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # When processing large numbers of records, it's often a good idea to do
9
+ # so in batches to prevent memory ballooning.
10
+ module ClassMethods
11
+ # Yields each record that was found by the find +options+. The find is
12
+ # performed by find_in_batches with a batch size of 1000 (or as
13
+ # specified by the <tt>:batch_size</tt> option).
14
+ #
15
+ # Example:
16
+ #
17
+ # Person.find_each(:conditions => "age > 21") do |person|
18
+ # person.party_all_night!
19
+ # end
20
+ #
21
+ # Note: This method is only intended to use for batch processing of
22
+ # large amounts of records that wouldn't fit in memory all at once. If
23
+ # you just need to loop over less than 1000 records, it's probably
24
+ # better just to use the regular find methods.
25
+ def find_each(options = {})
26
+ find_in_batches(options) do |records|
27
+ records.each { |record| yield record }
28
+ end
29
+
30
+ self
31
+ end
32
+
33
+ # Yields each batch of records that was found by the find +options+ as
34
+ # an array. The size of each batch is set by the <tt>:batch_size</tt>
35
+ # option; the default is 1000.
36
+ #
37
+ # You can control the starting point for the batch processing by
38
+ # supplying the <tt>:start</tt> option. This is especially useful if you
39
+ # want multiple workers dealing with the same processing queue. You can
40
+ # make worker 1 handle all the records between id 0 and 10,000 and
41
+ # worker 2 handle from 10,000 and beyond (by setting the <tt>:start</tt>
42
+ # option on that worker).
43
+ #
44
+ # It's not possible to set the order. That is automatically set to
45
+ # ascending on the primary key ("id ASC") to make the batch ordering
46
+ # work. This also mean that this method only works with integer-based
47
+ # primary keys. You can't set the limit either, that's used to control
48
+ # the the batch sizes.
49
+ #
50
+ # Example:
51
+ #
52
+ # Person.find_in_batches(:conditions => "age > 21") do |group|
53
+ # sleep(50) # Make sure it doesn't get too crowded in there!
54
+ # group.each { |person| person.party_all_night! }
55
+ # end
56
+ def find_in_batches(options = {})
57
+ raise "You can't specify an order." if options[:order]
58
+ raise "You can't specify a limit, it's forced to be the batch_size" if options[:limit]
59
+
60
+ start = options.delete(:start) || ''
61
+ nofilter = options[:no_filter]
62
+ batch_size = options.delete(:batch_size) || 500
63
+
64
+ records = self.find(:all, :keyrange => start..'', :limit => batch_size)
65
+
66
+ while records.any?
67
+ count = records.size
68
+ records = filter_deleted(records) unless nofilter
69
+ yield records
70
+ break if count < batch_size
71
+
72
+ last_value = records.last.id
73
+ records = self.find(:all, :keyrange => last_value..'', :limit => batch_size)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def filter_deleted records
80
+ ret = []
81
+ records.each do |item|
82
+ ret.push(item) unless item.deleted?
83
+ end
84
+ return ret
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,49 @@
1
+ module CassandraModel
2
+ module Callbacks
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.send :include, InstanceMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def define_callbacks(*callbacks)
10
+ callbacks.each do |callback|
11
+ [:before, :after].each do |chain|
12
+ callback_name = "#{chain}_#{callback}"
13
+ instance_eval <<-EVAL, __FILE__, __LINE__ + 1
14
+ def #{callback_name}(*args)
15
+ callbacks[:#{callback_name}] += args
16
+ end
17
+ EVAL
18
+ end
19
+ end
20
+ end
21
+
22
+ def callbacks
23
+ @callbacks ||= Hash.new {|h, k| h[k] = [] }
24
+ end
25
+ end
26
+
27
+ module InstanceMethods
28
+ def run_callbacks(name, &block)
29
+ _run_callbacks(:before, name)
30
+ result = block.call
31
+ _run_callbacks(:after, name, result)
32
+ result
33
+ end
34
+
35
+ private
36
+ def _run_callbacks(chain, name, value = nil)
37
+ callback_name = "#{chain}_#{name}".to_sym
38
+ self.class.callbacks[callback_name].each do |callback|
39
+ next unless callback.is_a? Symbol
40
+ if self.class.instance_method(callback).arity > 0
41
+ self.send(callback, value)
42
+ else
43
+ self.send(callback)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,66 @@
1
+ module CassandraModel
2
+ class Config
3
+
4
+ @@cfg_file = File.join(Rails.root,'config','cassandra_conf.yml')
5
+
6
+ @@hosts = ['127.0.0.1:9160']
7
+
8
+ @@username = nil
9
+
10
+ @@password = nil
11
+
12
+ @@keyspace = nil
13
+
14
+ def self.cfg_file
15
+ @@cfg_file
16
+ end
17
+
18
+ def self.cfg_file=(file)
19
+ @@cfg_file = file
20
+ end
21
+
22
+ def self.hosts
23
+ @@hosts
24
+ end
25
+
26
+ def self.hosts=(hosts)
27
+ @@hosts = hosts
28
+ end
29
+
30
+ def self.username
31
+ @@username
32
+ end
33
+
34
+ def self.username=(username)
35
+ @@username = username
36
+ end
37
+
38
+ def self.password
39
+ @@password
40
+ end
41
+
42
+ def self.password=(password)
43
+ @@password = password
44
+ end
45
+
46
+ def self.keyspace
47
+ @@keyspace
48
+ end
49
+
50
+ def self.keyspace=(keyspace)
51
+ @@keyspace = keyspace
52
+ end
53
+
54
+ def self.initialize
55
+ cfg = YAML.load_file(self.cfg_file)[Rails.env]
56
+ initialize_props cfg
57
+ end
58
+
59
+ def self.initialize_props props
60
+ self.hosts = props['hosts']
61
+ self.username = props['username']
62
+ self.password = props['password']
63
+ self.keyspace = props['keyspace']
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,21 @@
1
+ require 'cassandra-model/config'
2
+ module CassandraModel
3
+ class Connection
4
+
5
+ def self.establish_connection
6
+ connection = Thread.current[:cassandra_conn]
7
+ if ! connection
8
+ connection = Cassandra.new(Config.keyspace, Config.hosts)
9
+ if Config.username
10
+ connection.login!(Config.username, Config.password)
11
+ end
12
+ Thread.current[:cassandra_conn] = connection
13
+ end
14
+ return connection
15
+ end
16
+
17
+ def self.connection
18
+ return Thread.current[:cassandra_conn]
19
+ end
20
+ end
21
+ end