gotime-cassandra_object 0.6.1 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -59,16 +59,6 @@ task :cleanup do
59
59
  puts "Cleared"
60
60
  end
61
61
 
62
- task :config_snippet do
63
- unless defined?(CassandraObject)
64
- $: << 'test'
65
- $: << 'lib'
66
- require 'test_helper'
67
- end
68
-
69
- puts CassandraObject::Base.storage_config_xml
70
- end
71
-
72
62
  task :default=>[:test, :cleanup] do
73
63
  end
74
64
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.1
1
+ 0.7.1
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{gotime-cassandra_object}
8
- s.version = "0.6.1"
8
+ s.version = "0.7.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Michael Koziarski", "grantr"]
12
- s.date = %q{2010-12-10}
12
+ s.date = %q{2011-01-31}
13
13
  s.description = %q{Cassandra ActiveModel}
14
14
  s.email = %q{grantr@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -26,7 +26,7 @@ Gem::Specification.new do |s|
26
26
  "Rakefile",
27
27
  "TODO",
28
28
  "VERSION",
29
- "cassandra_object.gemspec",
29
+ "gotime-cassandra_object.gemspec",
30
30
  "lib/cassandra_object.rb",
31
31
  "lib/cassandra_object/associations.rb",
32
32
  "lib/cassandra_object/associations/one_to_many.rb",
@@ -37,6 +37,8 @@ Gem::Specification.new do |s|
37
37
  "lib/cassandra_object/collection.rb",
38
38
  "lib/cassandra_object/cursor.rb",
39
39
  "lib/cassandra_object/dirty.rb",
40
+ "lib/cassandra_object/generators/migration_generator.rb",
41
+ "lib/cassandra_object/generators/templates/migration.rb.erb",
40
42
  "lib/cassandra_object/identity.rb",
41
43
  "lib/cassandra_object/identity/abstract_key_factory.rb",
42
44
  "lib/cassandra_object/identity/key.rb",
@@ -44,13 +46,19 @@ Gem::Specification.new do |s|
44
46
  "lib/cassandra_object/identity/uuid_key_factory.rb",
45
47
  "lib/cassandra_object/indexes.rb",
46
48
  "lib/cassandra_object/log_subscriber.rb",
49
+ "lib/cassandra_object/migration.rb",
47
50
  "lib/cassandra_object/migrations.rb",
51
+ "lib/cassandra_object/migrator.rb",
48
52
  "lib/cassandra_object/mocking.rb",
49
53
  "lib/cassandra_object/persistence.rb",
50
54
  "lib/cassandra_object/serialization.rb",
55
+ "lib/cassandra_object/tasks/column_family.rb",
56
+ "lib/cassandra_object/tasks/keyspace.rb",
57
+ "lib/cassandra_object/tasks/ks.rb",
51
58
  "lib/cassandra_object/type_registration.rb",
52
59
  "lib/cassandra_object/types.rb",
53
60
  "lib/cassandra_object/validation.rb",
61
+ "test/active_model_test.rb",
54
62
  "test/basic_scenarios_test.rb",
55
63
  "test/callbacks_test.rb",
56
64
  "test/config/cassandra.in.sh",
@@ -78,6 +86,7 @@ Gem::Specification.new do |s|
78
86
  s.rubygems_version = %q{1.3.7}
79
87
  s.summary = %q{Cassandra ActiveModel}
80
88
  s.test_files = [
89
+ "test/active_model_test.rb",
81
90
  "test/basic_scenarios_test.rb",
82
91
  "test/callbacks_test.rb",
83
92
  "test/connection.rb",
@@ -1,13 +1,28 @@
1
1
  require 'rubygems'
2
2
  require 'i18n'
3
3
  require 'active_support'
4
+ require 'active_model'
5
+
4
6
 
5
7
  module CassandraObject
6
- VERSION = "0.5.0"
7
- end
8
+ VERSION = "0.7.1"
9
+ extend ActiveSupport::Autoload
8
10
 
11
+ autoload :Base
9
12
 
10
- require 'active_support/all'
11
- require 'active_model'
13
+ require 'cassandra_object/migrator'
14
+ require 'cassandra_object/migration'
12
15
 
13
- require 'cassandra_object/base'
16
+ module Tasks
17
+ extend ActiveSupport::Autoload
18
+ autoload :Keyspace
19
+ autoload :ColumnFamily
20
+
21
+ require 'cassandra_object/tasks/ks'
22
+ end
23
+
24
+ module Generators
25
+ require 'cassandra_object/generators/migration_generator'
26
+ end
27
+
28
+ end
@@ -24,7 +24,9 @@ module CassandraObject
24
24
 
25
25
  def remove(key)
26
26
  begin
27
- connection.remove("#{name}Relationships", key.to_s)
27
+ ActiveSupport::Notifications.instrument("remove.cassandra_object", :key => key) do
28
+ connection.remove("#{name}Relationships", key.to_s)
29
+ end
28
30
  rescue Cassandra::AccessError => e
29
31
  raise e unless e.message =~ /Invalid column family/
30
32
  end
@@ -32,4 +34,4 @@ module CassandraObject
32
34
  end
33
35
  end
34
36
  end
35
- end
37
+ end
@@ -1,4 +1,4 @@
1
- require 'cassandra'
1
+ require 'cassandra/0.7'
2
2
  require 'set'
3
3
  require 'cassandra_object/attributes'
4
4
  require 'cassandra_object/dirty'
@@ -49,24 +49,6 @@ module CassandraObject
49
49
 
50
50
  extend ActiveModel::Naming
51
51
 
52
- module ConfigurationDumper
53
- def storage_config_xml
54
- subclasses.map(&:constantize).map(&:column_family_configuration).flatten.map do |config|
55
- config_to_xml(config)
56
- end.join("\n")
57
- end
58
-
59
- def config_to_xml(config)
60
- xml = "<ColumnFamily "
61
- config.each do |(attr_name, attr_value)|
62
- xml << " #{attr_name}=\"#{attr_value}\""
63
- end
64
- xml << " />"
65
- xml
66
- end
67
- end
68
- extend ConfigurationDumper
69
-
70
52
  include Callbacks
71
53
  include Identity
72
54
  include Attributes
@@ -86,11 +68,17 @@ module CassandraObject
86
68
 
87
69
  def initialize(attributes={})
88
70
  @key = attributes.delete(:key)
89
- @new_record = true
71
+ @persisted = false
72
+ @destroyed = false
90
73
  @attributes = {}.with_indifferent_access
91
74
  self.attributes = attributes
92
75
  @schema_version = self.class.current_schema_version
93
76
  end
77
+
78
+ def to_model
79
+ self
80
+ end
81
+
94
82
  end
95
83
  end
96
84
 
@@ -0,0 +1,31 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/named_base'
3
+
4
+ module CassandraObject
5
+ module Generators
6
+ class MigrationGenerator < Rails::Generators::NamedBase
7
+
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ def self.banner
11
+ "rails g cassandra_object:migration NAME"
12
+ end
13
+
14
+ def self.desc(description = nil)
15
+ <<EOF
16
+ Description:
17
+ Create an empty Cassandra migration file in 'ks/migrate'. Very similar to Rails database migrations.
18
+
19
+ Example:
20
+ `rails g cassandra_object:migration CreateFooColumnFamily`
21
+ EOF
22
+ end
23
+
24
+ def create
25
+ timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
26
+ template 'migration.rb.erb', "ks/migrate/#{timestamp}_#{file_name.underscore}.rb"
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ class <%= name %> < CassandraObject::Migration
2
+
3
+ def self.up
4
+
5
+ end
6
+
7
+ def self.down
8
+
9
+ end
10
+
11
+ end
@@ -56,6 +56,10 @@ module CassandraObject
56
56
  def to_param
57
57
  key.to_param
58
58
  end
59
+
60
+ def to_key
61
+ [key] if key
62
+ end
59
63
  end
60
64
  end
61
65
  end
@@ -1,16 +1,27 @@
1
1
  module CassandraObject
2
2
  class LogSubscriber < ActiveSupport::LogSubscriber
3
3
  def multi_get(event)
4
- name = 'CassandraObject multi_get (%.1fms)'
5
- keys = event.payload[:keys].join(" ")
4
+ name = 'CassandraObject multi_get (%.1fms)' % event.duration
6
5
 
7
- debug " #{name} (#{keys.size}) #{keys}"
6
+ debug " #{name} (#{event.payload[:keys].size}) #{event.payload[:keys].join(" ")}"
8
7
  end
9
8
 
10
9
  def remove(event)
10
+ name = 'CassandraObject remove (%.1fms)' % event.duration
11
+
12
+ debug " #{name} #{event.payload[:key]}"
11
13
  end
12
14
 
13
15
  def insert(event)
16
+ name = 'CassandraObject insert (%.1fms)' % event.duration
17
+
18
+ debug " #{name} #{event.payload[:key]} #{event.payload[:attributes].inspect}"
19
+ end
20
+
21
+ def get_range(event)
22
+ name = 'CassandraObject get_range (%.1fms)' % event.duration
23
+
24
+ debug " #{name} (#{event.payload[:count]}) #{event.payload[:start]} => #{event.payload[:finish]}"
14
25
  end
15
26
  end
16
27
  end
@@ -0,0 +1,126 @@
1
+ module CassandraObject
2
+
3
+ class Migration
4
+
5
+ @@verbose = true
6
+ cattr_accessor :verbose
7
+
8
+ # Returns the raw connection to Cassandra
9
+ def self.connection
10
+ CassandraObject::Base.connection
11
+ end
12
+
13
+ def self.migrate(direction)
14
+ return unless respond_to?(direction)
15
+
16
+ case direction
17
+ when :up then announce "migrating"
18
+ when :down then announce "reverting"
19
+ end
20
+
21
+ result = nil
22
+ time = Benchmark.measure { result = send("#{direction}") }
23
+
24
+ case direction
25
+ when :up then announce "migrated (%.4fs)" % time.real; write
26
+ when :down then announce "reverted (%.4fs)" % time.real; write
27
+ end
28
+
29
+ result
30
+ end
31
+
32
+ # Creates a new column family with the given name. Column family configurations can be set within
33
+ # a block like this:
34
+ #
35
+ # create_column_family(:users) do |cf|
36
+ # cf.comment = 'Users column family'
37
+ # cf.comparator_type = 'TimeUUIDType'
38
+ # end
39
+ #
40
+ # A complete list of available configuration settings is here:
41
+ #
42
+ # http://github.com/fauna/cassandra/blob/master/vendor/0.7/gen-rb/cassandra_types.rb
43
+ #
44
+ # Scroll down to the CfDef definition.
45
+ def self.create_column_family(name, &block)
46
+ say_with_time("create_column_family #{name}") do
47
+ column_family_tasks.create(name, &block)
48
+ end
49
+ end
50
+
51
+ # Drops the given column family
52
+ def self.drop_column_family(name)
53
+ say_with_time("drop_column_family #{name}") do
54
+ column_family_tasks.drop(name)
55
+ end
56
+ end
57
+
58
+ # Renames the column family from the old name to the new name
59
+ def self.rename_column_family(old_name, new_name)
60
+ say_with_time("rename_column_family #{name}") do
61
+ column_family_tasks.rename(old_name, new_name)
62
+ end
63
+ end
64
+
65
+ def self.write(text="")
66
+ puts(text) if verbose
67
+ end
68
+
69
+ def self.announce(message)
70
+ version = defined?(@version) ? @version : nil
71
+
72
+ text = "#{version} #{name}: #{message}"
73
+ length = [0, 75 - text.length].max
74
+ write "== %s %s" % [text, "=" * length]
75
+ end
76
+
77
+ def self.say(message, subitem=false)
78
+ write "#{subitem ? " ->" : "--"} #{message}"
79
+ end
80
+
81
+ def self.say_with_time(message)
82
+ say(message)
83
+ result = nil
84
+ time = Benchmark.measure { result = yield }
85
+ say "%.4fs" % time.real, :subitem
86
+ say("#{result} rows", :subitem) if result.is_a?(Integer)
87
+ result
88
+ end
89
+
90
+ def self.suppress_messages
91
+ save, self.verbose = verbose, false
92
+ yield
93
+ ensure
94
+ self.verbose = save
95
+ end
96
+
97
+ private
98
+
99
+ def self.column_family_tasks
100
+ Tasks::ColumnFamily.new(CassandraObject::Base.connection.keyspace)
101
+ end
102
+
103
+ end
104
+
105
+ # MigrationProxy is used to defer loading of the actual migration classes
106
+ # until they are needed
107
+ class MigrationProxy
108
+
109
+ attr_accessor :name, :version, :filename
110
+
111
+ delegate :migrate, :announce, :write, :to=>:migration
112
+
113
+ private
114
+
115
+ def migration
116
+ @migration ||= load_migration
117
+ end
118
+
119
+ def load_migration
120
+ require(File.expand_path(filename))
121
+ name.constantize
122
+ end
123
+
124
+ end
125
+
126
+ end
@@ -0,0 +1,238 @@
1
+ module CassandraObject
2
+ class IrreversibleMigration < StandardError
3
+ end
4
+
5
+ class DuplicateMigrationVersionError < StandardError#:nodoc:
6
+ def initialize(version)
7
+ super("Multiple migrations have the version number #{version}")
8
+ end
9
+ end
10
+
11
+ class DuplicateMigrationNameError < StandardError#:nodoc:
12
+ def initialize(name)
13
+ super("Multiple migrations have the name #{name}")
14
+ end
15
+ end
16
+
17
+ class UnknownMigrationVersionError < StandardError #:nodoc:
18
+ def initialize(version)
19
+ super("No migration with version number #{version}")
20
+ end
21
+ end
22
+
23
+ class IllegalMigrationNameError < StandardError#:nodoc:
24
+ def initialize(name)
25
+ super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
26
+ end
27
+ end
28
+
29
+ class Migrator
30
+
31
+ def self.migrate(migrations_path, target_version = nil)
32
+ case
33
+ when target_version.nil?
34
+ up(migrations_path, target_version)
35
+ when current_version == 0 && target_version == 0
36
+ when current_version > target_version
37
+ down(migrations_path, target_version)
38
+ else
39
+ up(migrations_path, target_version)
40
+ end
41
+ end
42
+
43
+ def self.rollback(migrations_path, steps = 1)
44
+ move(:down, migrations_path, steps)
45
+ end
46
+
47
+ def self.forward(migrations_path, steps = 1)
48
+ move(:up, migrations_path, steps)
49
+ end
50
+
51
+ def self.up(migrations_path, target_version = nil)
52
+ new(:up, migrations_path, target_version).migrate
53
+ end
54
+
55
+ def self.down(migrations_path, target_version = nil)
56
+ new(:down, migrations_path, target_version).migrate
57
+ end
58
+
59
+ def self.run(direction, migrations_path, target_version)
60
+ new(direction, migrations_path, target_version).run
61
+ end
62
+
63
+ def self.migrations_path
64
+ 'ks/migrate'
65
+ end
66
+
67
+ def self.schema_migrations_column_family
68
+ :schema_migrations
69
+ end
70
+
71
+ def self.column_family_tasks
72
+ cas = CassandraObject::Base.connection
73
+ Tasks::ColumnFamily.new(cas.keyspace)
74
+ end
75
+
76
+ def self.get_all_versions
77
+ cas = CassandraObject::Base.connection
78
+ cas.get(schema_migrations_column_family, 'all').map {|(name, _value)| name.to_i}.sort
79
+ end
80
+
81
+ def self.current_version
82
+ sm_cf = schema_migrations_column_family
83
+ if column_family_tasks.exists?(sm_cf)
84
+ get_all_versions.max || 0
85
+ else
86
+ 0
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def self.move(direction, migrations_path, steps)
93
+ migrator = self.new(direction, migrations_path)
94
+ start_index = migrator.migrations.index(migrator.current_migration)
95
+
96
+ if start_index
97
+ finish = migrator.migrations[start_index + steps]
98
+ version = finish ? finish.version : 0
99
+ send(direction, migrations_path, version)
100
+ end
101
+ end
102
+
103
+ public
104
+
105
+ def initialize(direction, migrations_path, target_version = nil)
106
+ sm_cf = self.class.schema_migrations_column_family
107
+
108
+ unless column_family_tasks.exists?(sm_cf)
109
+ column_family_tasks.create(sm_cf) do |cf|
110
+ cf.comparator_type = 'LongType'
111
+ end
112
+ end
113
+
114
+ @direction, @migrations_path, @target_version = direction, migrations_path, target_version
115
+ end
116
+
117
+ def current_version
118
+ migrated.last || 0
119
+ end
120
+
121
+ def current_migration
122
+ migrations.detect { |m| m.version == current_version }
123
+ end
124
+
125
+ def run
126
+ target = migrations.detect { |m| m.version == @target_version }
127
+ raise UnknownMigrationVersionError.new(@target_version) if target.nil?
128
+ unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
129
+ target.migrate(@direction)
130
+ record_version_state_after_migrating(target)
131
+ end
132
+ end
133
+
134
+ def migrate
135
+ current = migrations.detect { |m| m.version == current_version }
136
+ target = migrations.detect { |m| m.version == @target_version }
137
+
138
+ if target.nil? && !@target_version.nil? && @target_version > 0
139
+ raise UnknownMigrationVersionError.new(@target_version)
140
+ end
141
+
142
+ start = up? ? 0 : (migrations.index(current) || 0)
143
+ finish = migrations.index(target) || migrations.size - 1
144
+ runnable = migrations[start..finish]
145
+
146
+ # skip the last migration if we're headed down, but not ALL the way down
147
+ runnable.pop if down? && !target.nil?
148
+
149
+ runnable.each do |migration|
150
+ #puts "Migrating to #{migration.name} (#{migration.version})"
151
+
152
+ # On our way up, we skip migrating the ones we've already migrated
153
+ next if up? && migrated.include?(migration.version.to_i)
154
+
155
+ # On our way down, we skip reverting the ones we've never migrated
156
+ if down? && !migrated.include?(migration.version.to_i)
157
+ migration.announce 'never migrated, skipping'; migration.write
158
+ next
159
+ end
160
+
161
+ migration.migrate(@direction)
162
+ record_version_state_after_migrating(migration)
163
+ end
164
+ end
165
+
166
+ def migrations
167
+ @migrations ||= begin
168
+ files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
169
+
170
+ migrations = files.inject([]) do |klasses, file|
171
+ version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
172
+
173
+ raise IllegalMigrationNameError.new(file) unless version
174
+ version = version.to_i
175
+
176
+ if klasses.detect { |m| m.version == version }
177
+ raise DuplicateMigrationVersionError.new(version)
178
+ end
179
+
180
+ if klasses.detect { |m| m.name == name.camelize }
181
+ raise DuplicateMigrationNameError.new(name.camelize)
182
+ end
183
+
184
+ migration = MigrationProxy.new
185
+ migration.name = name.camelize
186
+ migration.version = version
187
+ migration.filename = file
188
+ klasses << migration
189
+ end
190
+
191
+ migrations = migrations.sort_by { |m| m.version }
192
+ down? ? migrations.reverse : migrations
193
+ end
194
+ end
195
+
196
+ def pending_migrations
197
+ already_migrated = migrated
198
+ migrations.reject { |m| already_migrated.include?(m.version.to_i) }
199
+ end
200
+
201
+ def migrated
202
+ @migrated_versions ||= self.class.get_all_versions
203
+ end
204
+
205
+ private
206
+
207
+ def column_family_tasks
208
+ Tasks::ColumnFamily.new(connection.keyspace)
209
+ end
210
+
211
+ def connection
212
+ CassandraObject::Base.connection
213
+ end
214
+
215
+ def record_version_state_after_migrating(migration)
216
+ sm_cf = self.class.schema_migrations_column_family
217
+
218
+ @migrated_versions ||= []
219
+ if down?
220
+ @migrated_versions.delete(migration.version)
221
+ connection.remove sm_cf, 'all', migration.version
222
+ else
223
+ @migrated_versions.push(migration.version).sort!
224
+ connection.insert sm_cf, 'all', { migration.version => migration.name }
225
+ end
226
+ end
227
+
228
+ def up?
229
+ @direction == :up
230
+ end
231
+
232
+ def down?
233
+ @direction == :down
234
+ end
235
+
236
+ end
237
+
238
+ end
@@ -7,7 +7,7 @@ module CassandraObject
7
7
  end
8
8
 
9
9
  VALID_READ_CONSISTENCY_LEVELS = [:one, :quorum, :all]
10
- VALID_WRITE_CONSISTENCY_LEVELS = VALID_READ_CONSISTENCY_LEVELS + [:zero]
10
+ VALID_WRITE_CONSISTENCY_LEVELS = VALID_READ_CONSISTENCY_LEVELS
11
11
 
12
12
  module ClassMethods
13
13
  def consistency_levels(levels)
@@ -59,11 +59,20 @@ module CassandraObject
59
59
  end
60
60
 
61
61
  def remove(key)
62
- connection.remove(column_family, key.to_s, :consistency => write_consistency_for_thrift)
62
+ ActiveSupport::Notifications.instrument("remove.cassandra_object", :key => key) do
63
+ connection.remove(column_family, key.to_s, :consistency => write_consistency_for_thrift)
64
+ end
65
+ end
66
+
67
+ def delete_all
68
+ connection.truncate!(column_family)
63
69
  end
64
70
 
65
71
  def all(keyrange = ''..'', options = {})
66
- results = connection.get_range(column_family, :start => keyrange.first, :finish => keyrange.last, :count=>(options[:limit] || 100))
72
+ count = options[:limit] || 100
73
+ results = ActiveSupport::Notifications.instrument("get_range.cassandra_object", :start => keyrange.first, :finish => keyrange.last, :count => count) do
74
+ connection.get_range(column_family, :start => keyrange.first, :finish => keyrange.last, :count => count)
75
+ end
67
76
  keys = results.map(&:key)
68
77
  keys.map {|key| get(key) }
69
78
  end
@@ -80,7 +89,10 @@ module CassandraObject
80
89
 
81
90
  def write(key, attributes, schema_version)
82
91
  key.tap do |key|
83
- connection.insert(column_family, key.to_s, encode_columns_hash(attributes, schema_version), :consistency => write_consistency_for_thrift)
92
+ attributes = encode_columns_hash(attributes, schema_version)
93
+ ActiveSupport::Notifications.instrument("insert.cassandra_object", :key => key, :attributes => attributes) do
94
+ connection.insert(column_family, key.to_s, attributes, :consistency => write_consistency_for_thrift)
95
+ end
84
96
  end
85
97
  end
86
98
 
@@ -97,7 +109,9 @@ module CassandraObject
97
109
 
98
110
  def encode_columns_hash(attributes, schema_version)
99
111
  attributes.inject(Hash.new) do |memo, (column_name, value)|
100
- memo[column_name.to_s] = model_attributes[column_name].converter.encode(value)
112
+ # cassandra stores bytes, not strings, so it has no concept of encodings. The ruby thrift gem
113
+ # expects all strings to be encoded as ascii-8bit.
114
+ memo[column_name.to_s] = model_attributes[column_name].converter.encode(value).force_encoding('ASCII-8BIT')
101
115
  memo
102
116
  end.merge({"schema_version" => schema_version.to_s})
103
117
  end
@@ -132,7 +146,6 @@ module CassandraObject
132
146
 
133
147
  def consistency_for_thrift(consistency)
134
148
  {
135
- :zero => Cassandra::Consistency::ZERO,
136
149
  :one => Cassandra::Consistency::ONE,
137
150
  :quorum => Cassandra::Consistency::QUORUM,
138
151
  :all => Cassandra::Consistency::ALL
@@ -142,31 +155,27 @@ module CassandraObject
142
155
 
143
156
  module InstanceMethods
144
157
  def save
145
- run_callbacks :save do
158
+ _run_save_callbacks do
146
159
  create_or_update
147
160
  end
148
161
  end
149
162
 
150
163
  def create_or_update
151
- if new_record?
152
- create
153
- else
154
- update
155
- end
156
- true
164
+ result = persisted? ? update : create
165
+ result != false
157
166
  end
158
167
 
159
168
  def create
160
- run_callbacks :create do
169
+ _run_create_callbacks do
161
170
  @key ||= self.class.next_key(self)
162
171
  _write
163
- @new_record = false
164
- true
172
+ @persisted = true
173
+ @key
165
174
  end
166
175
  end
167
176
 
168
177
  def update
169
- run_callbacks :update do
178
+ _run_update_callbacks do
170
179
  _write
171
180
  end
172
181
  end
@@ -177,12 +186,22 @@ module CassandraObject
177
186
  end
178
187
 
179
188
  def new_record?
180
- @new_record || false
189
+ !@persisted
190
+ end
191
+
192
+ def destroyed?
193
+ @destroyed
194
+ end
195
+
196
+ def persisted?
197
+ @persisted && !destroyed?
181
198
  end
182
199
 
183
200
  def destroy
184
- run_callbacks :destroy do
201
+ _run_destroy_callbacks do
185
202
  self.class.remove(key)
203
+ @destroyed = true
204
+ freeze
186
205
  end
187
206
  end
188
207
 
@@ -0,0 +1,90 @@
1
+ module CassandraObject
2
+
3
+ module Tasks
4
+
5
+ class ColumnFamily
6
+
7
+ COMPARATOR_TYPES = { :time => 'TimeUUIDType',
8
+ :timestamp => 'TimeUUIDType',
9
+ :long => 'LongType',
10
+ :string => 'BytesType',
11
+ :utf8 => 'UTF8Type' }
12
+
13
+ COLUMN_TYPES = { :standard => 'Standard',
14
+ :super => 'Super' }
15
+
16
+ def initialize(keyspace)
17
+ @keyspace = keyspace
18
+ end
19
+
20
+ def exists?(name)
21
+ connection.schema.cf_defs.find { |cf_def| cf_def.name == name.to_s }
22
+ end
23
+
24
+ def create(name, &block)
25
+ cf = Cassandra::ColumnFamily.new
26
+ cf.name = name.to_s
27
+ cf.keyspace = @keyspace.to_s
28
+ cf.comparator_type = 'BytesType'
29
+ cf.column_type = 'Standard'
30
+
31
+ block.call cf if block
32
+
33
+ post_process_column_family(cf)
34
+ connection.add_column_family(cf)
35
+ end
36
+
37
+ def drop(name)
38
+ connection.drop_column_family(name.to_s)
39
+ end
40
+
41
+ def rename(old_name, new_name)
42
+ connection.rename_column_family(old_name.to_s, new_name.to_s)
43
+ end
44
+
45
+ def clear(name)
46
+ connection.truncate!(name.to_s)
47
+ end
48
+
49
+ private
50
+
51
+ def connection
52
+ CassandraObject::Base.connection
53
+ end
54
+
55
+ def post_process_column_family(cf)
56
+ comp_type = cf.comparator_type
57
+ if comp_type && COMPARATOR_TYPES.has_key?(comp_type)
58
+ cf.comparator_type = COMPARATOR_TYPES[comp_type]
59
+ end
60
+
61
+ comp_type = cf.subcomparator_type
62
+ if comp_type && COMPARATOR_TYPES.has_key?(comp_type)
63
+ cf.subcomparator_type = COMPARATOR_TYPES[comp_type]
64
+ end
65
+
66
+ col_type = cf.column_type
67
+ if col_type && COLUMN_TYPES.has_key?(col_type)
68
+ cf.column_type = COLUMN_TYPES[col_type]
69
+ end
70
+
71
+ cf
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+
80
+ class Cassandra
81
+ class ColumnFamily
82
+ def with_fields(options)
83
+ struct_fields.collect { |f| f[1][:name] }.each do |f|
84
+ send("#{f}=", options[f.to_sym] || options[f.to_s])
85
+ end
86
+ self
87
+ end
88
+ end
89
+ end
90
+
@@ -0,0 +1,89 @@
1
+ module CassandraObject
2
+
3
+ module Tasks
4
+
5
+ class Keyspace
6
+
7
+ def self.parse(hash)
8
+ ks = Cassandra::Keyspace.new.with_fields hash
9
+ ks.cf_defs = []
10
+ hash['cf_defs'].each do |cf|
11
+ ks.cf_defs << Cassandra::ColumnFamily.new.with_fields(cf)
12
+ end
13
+ ks
14
+ end
15
+
16
+ def exists?(name)
17
+ connection.keyspaces.include? name.to_s
18
+ end
19
+
20
+ def create(name, options = {})
21
+ opts = { :name => name.to_s,
22
+ :strategy_class => 'org.apache.cassandra.locator.LocalStrategy',
23
+ :replication_factor => 1,
24
+ :cf_defs => [] }.merge(options)
25
+
26
+ ks = Cassandra::Keyspace.new.with_fields(opts)
27
+ connection.add_keyspace ks
28
+ end
29
+
30
+ def drop(name)
31
+ connection.drop_keyspace name.to_s
32
+ end
33
+
34
+ def set(name)
35
+ connection.keyspace = name.to_s
36
+ end
37
+
38
+ def get
39
+ connection.keyspace
40
+ end
41
+
42
+ def clear
43
+ return puts 'Cannot clear system keyspace' if connection.keyspace == 'system'
44
+
45
+ connection.clear_keyspace!
46
+ end
47
+
48
+ def schema_dump
49
+ connection.schema
50
+ end
51
+
52
+ def schema_load(schema)
53
+ connection.schema.cf_defs.each do |cf|
54
+ connection.drop_column_family cf.name
55
+ end
56
+
57
+ keyspace = get
58
+ schema.cf_defs.each do |cf|
59
+ cf.keyspace = keyspace
60
+ connection.add_column_family cf
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def connection
67
+ unless @connection
68
+ c = CassandraObject::Base.connection
69
+ @connection = Cassandra.new('system', c.servers, c.thrift_client_options)
70
+ end
71
+ @connection
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+
80
+ class Cassandra
81
+ class Keyspace
82
+ def with_fields(options)
83
+ struct_fields.collect { |f| f[1][:name] }.each do |f|
84
+ send("#{f}=", options[f.to_sym] || options[f.to_s])
85
+ end
86
+ self
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,122 @@
1
+ require 'rake'
2
+
3
+ namespace :ks do
4
+
5
+ task :configure => :environment do
6
+ @configs = YAML.load_file(Rails.root.join("config", "cassandra.yml"))
7
+ @config = @configs[Rails.env || 'development']
8
+ end
9
+
10
+ #task :set_keyspace => :configure do
11
+ #set_keyspace
12
+ #end
13
+
14
+ desc 'Create the keyspace in config/cassandra.yml for the current environment'
15
+ task :create => :configure do
16
+ CassandraObject::Tasks::Keyspace.new.create @config['keyspace'], @config
17
+ puts "Created keyspace: #{@config['keyspace']}"
18
+ end
19
+
20
+ namespace :create do
21
+ desc 'Create keyspaces in config/cassandra.yml for all environments'
22
+ task :all => :configure do
23
+ created = []
24
+ @configs.values.each do |config|
25
+ CassandraObject::Tasks::Keyspace.new.create config['keyspace'], config
26
+ created << config['keyspace']
27
+ end
28
+ puts "Created keyspaces: #{created.join(', ')}"
29
+ end
30
+ end
31
+
32
+ desc 'Drop keyspace in config/cassandra.yml for the current environment'
33
+ task :drop => :configure do
34
+ CassandraObject::Tasks::Keyspace.new.drop @config['keyspace']
35
+ puts "Dropped keyspace: #{@config['keyspace']}"
36
+ end
37
+
38
+ namespace :drop do
39
+ desc 'Drop keyspaces in config/cassandra.yml for all environments'
40
+ task :all => :configure do
41
+ dropped = []
42
+ @configs.values.each do |config|
43
+ CassandraObject::Tasks::Keyspace.new.drop config['keyspace']
44
+ dropped << config['keyspace']
45
+ end
46
+ puts "Dropped keyspaces: #{dropped.join(', ')}"
47
+ end
48
+ end
49
+
50
+ desc 'Migrate the keyspace (options: VERSION=x)'
51
+ task :migrate => :configure do
52
+ version = ( ENV['VERSION'] ? ENV['VERSION'].to_i : nil )
53
+ CassandraObject::Migrator.migrate CassandraObject::Migrator.migrations_path, version
54
+ schema_dump
55
+ end
56
+
57
+ desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n)'
58
+ task :rollback => :set_keyspace do
59
+ step = ENV['STEP'] ? ENV['STEP'].to_i : 1
60
+ CassandraObject::Migrator.rollback CassandraObject::Migrator.migrations_path, step
61
+ schema_dump
62
+ end
63
+
64
+ desc 'Pushes the schema to the next version (specify steps w/ STEP=n)'
65
+ task :forward => :set_keyspace do
66
+ step = ENV['STEP'] ? ENV['STEP'].to_i : 1
67
+ CassandraObject::Migrator.forward CassandraObject::Migrator.migrations_path, step
68
+ schema_dump
69
+ end
70
+
71
+ namespace :schema do
72
+ desc 'Create ks/schema.json file that can be portably used against any Cassandra instance supported by CassandraObject'
73
+ task :dump => :configure do
74
+ schema_dump
75
+ end
76
+
77
+ desc 'Load ks/schema.json file into Cassandra'
78
+ task :load => :configure do
79
+ schema_load
80
+ end
81
+ end
82
+
83
+ namespace :test do
84
+ desc 'Load the development schema in to the test keyspace'
85
+ task :prepare => :configure do
86
+ schema_dump :development
87
+ schema_load :test
88
+ end
89
+ end
90
+
91
+ desc 'Retrieves the current schema version number'
92
+ task :version => :set_keyspace do
93
+ version = CassandraObject::Migrator.current_version
94
+ puts "Current version: #{version}"
95
+ end
96
+
97
+ private
98
+
99
+ def schema_dump(env = Rails.env)
100
+ ks = set_keyspace env
101
+ File.open "#{Rails.root}/ks/schema.json", 'w' do |file|
102
+ file.puts ks.schema_dump.to_json
103
+ end
104
+ end
105
+
106
+ def schema_load(env = Rails.env)
107
+ ks = set_keyspace env
108
+ File.open "#{Rails.root}/ks/schema.json", 'r' do |file|
109
+ hash = JSON.parse(file.read(nil))
110
+ ks.schema_load CassandraObject::Tasks::Keyspace.parse(hash)
111
+ end
112
+ end
113
+
114
+ def set_keyspace(env = Rails.env)
115
+ config = @configs[env.to_s || 'development']
116
+ ks = CassandraObject::Tasks::Keyspace.new
117
+ ks.set config['keyspace']
118
+ ks
119
+ end
120
+
121
+ end
122
+
@@ -3,5 +3,7 @@ CassandraObject::Base.register_attribute_type(:float, Float, CassandraObject::Fl
3
3
  CassandraObject::Base.register_attribute_type(:date, Date, CassandraObject::DateType)
4
4
  CassandraObject::Base.register_attribute_type(:time, Time, CassandraObject::TimeType)
5
5
  CassandraObject::Base.register_attribute_type(:time_with_zone, ActiveSupport::TimeWithZone, CassandraObject::TimeWithZoneType)
6
- CassandraObject::Base.register_attribute_type(:string, String, CassandraObject::StringType)
6
+ CassandraObject::Base.register_attribute_type(:string, String, CassandraObject::UTF8StringType) #This could be changed to StringType to support non-utf8 strings
7
+ CassandraObject::Base.register_attribute_type(:utf8, String, CassandraObject::UTF8StringType)
7
8
  CassandraObject::Base.register_attribute_type(:hash, Hash, CassandraObject::HashType)
9
+ CassandraObject::Base.register_attribute_type(:boolean, Object, CassandraObject::BooleanType)
@@ -83,11 +83,11 @@ module CassandraObject
83
83
  end
84
84
  module_function :decode
85
85
  end
86
-
86
+
87
87
  module StringType
88
88
  def encode(str)
89
89
  raise ArgumentError.new("#{self} requires a String") unless str.kind_of?(String)
90
- str
90
+ str.dup
91
91
  end
92
92
  module_function :encode
93
93
 
@@ -96,6 +96,21 @@ module CassandraObject
96
96
  end
97
97
  module_function :decode
98
98
  end
99
+
100
+ module UTF8StringType
101
+ def encode(str)
102
+ # This is technically the most correct, but it is a pain to require utf-8 encoding for all strings. Should revisit.
103
+ #raise ArgumentError.new("#{self} requires a UTF-8 encoded String") unless str.kind_of?(String) && str.encoding == Encoding::UTF_8
104
+ raise ArgumentError.new("#{self} requires a String") unless str.kind_of?(String)
105
+ str.dup
106
+ end
107
+ module_function :encode
108
+
109
+ def decode(str)
110
+ str.force_encoding('UTF-8')
111
+ end
112
+ module_function :decode
113
+ end
99
114
 
100
115
  module HashType
101
116
  def encode(hash)
@@ -1,16 +1,17 @@
1
1
  module CassandraObject
2
- module Validation
3
- class RecordInvalidError < StandardError
4
- attr_reader :record
5
- def initialize(record)
6
- @record = record
7
- super("Invalid record: #{@record.errors.full_messages.to_sentence}")
8
- end
9
-
10
- def self.raise_error(record)
11
- raise new(record)
12
- end
2
+ class RecordInvalid < StandardError
3
+ attr_reader :record
4
+ def initialize(record)
5
+ @record = record
6
+ super("Invalid record: #{@record.errors.full_messages.to_sentence}")
13
7
  end
8
+
9
+ def self.raise_error(record)
10
+ raise new(record)
11
+ end
12
+ end
13
+
14
+ module Validation
14
15
  extend ActiveSupport::Concern
15
16
  include ActiveModel::Validations
16
17
 
@@ -41,7 +42,7 @@ module CassandraObject
41
42
  end
42
43
 
43
44
  def save!
44
- save || RecordInvalidError.raise_error(self)
45
+ save || RecordInvalid.raise_error(self)
45
46
  end
46
47
 
47
48
  end
@@ -0,0 +1,9 @@
1
+ require 'test_helper'
2
+
3
+ class ActiveModelTest < ActiveModel::TestCase
4
+ include ActiveModel::Lint::Tests
5
+
6
+ def setup
7
+ @model = Customer.new
8
+ end
9
+ end
data/test/types_test.rb CHANGED
@@ -184,21 +184,21 @@ class TypesTest < CassandraObjectTestCase
184
184
  context "encode" do
185
185
  should "handle an empty Hash" do
186
186
  assert_nothing_raised {
187
- assert_equal({}.to_json, CassandraObject::HashType.encode({}))
187
+ assert_equal(ActiveSupport::JSON.encode({}), CassandraObject::HashType.encode({}))
188
188
  }
189
189
  end
190
190
 
191
191
  should "handle string keys" do
192
192
  assert_nothing_raised {
193
193
  h = {'foo' => 'bar'}
194
- assert_equal(h.to_json, CassandraObject::HashType.encode(h))
194
+ assert_equal(ActiveSupport::JSON.encode(h), CassandraObject::HashType.encode(h))
195
195
  }
196
196
  end
197
197
 
198
198
  should "handle symbol keys" do
199
199
  assert_nothing_raised {
200
200
  h = {:foo => 'bar'}
201
- assert_equal(h.to_json, CassandraObject::HashType.encode(h))
201
+ assert_equal(ActiveSupport::JSON.encode(h), CassandraObject::HashType.encode(h))
202
202
  }
203
203
  end
204
204
  end
@@ -7,7 +7,7 @@ class ValidationTest < CassandraObjectTestCase
7
7
  customer = Customer.new :first_name=>"steve", :date_of_birth=>Date.parse("1979/12/25")
8
8
  customer.save!
9
9
  flunk "Should have failed to save"
10
- rescue CassandraObject::Validation::RecordInvalidError => e
10
+ rescue CassandraObject::Validation::RecordInvalid => e
11
11
  assert_equal customer, e.record
12
12
  end
13
13
  end
@@ -16,10 +16,10 @@ class ValidationTest < CassandraObjectTestCase
16
16
  begin
17
17
  customer = Customer.create! :first_name=>"steve", :date_of_birth=>Date.parse("1979/12/25")
18
18
  flunk "Should have failed to create!"
19
- rescue CassandraObject::Validation::RecordInvalidError => e
19
+ rescue CassandraObject::Validation::RecordInvalid => e
20
20
  assert_kind_of Customer, e.record
21
21
  end
22
22
  end
23
23
 
24
24
 
25
- end
25
+ end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 6
7
+ - 7
8
8
  - 1
9
- version: 0.6.1
9
+ version: 0.7.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Michael Koziarski
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-12-10 00:00:00 -08:00
18
+ date: 2011-01-31 00:00:00 -08:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -156,6 +156,8 @@ files:
156
156
  - lib/cassandra_object/collection.rb
157
157
  - lib/cassandra_object/cursor.rb
158
158
  - lib/cassandra_object/dirty.rb
159
+ - lib/cassandra_object/generators/migration_generator.rb
160
+ - lib/cassandra_object/generators/templates/migration.rb.erb
159
161
  - lib/cassandra_object/identity.rb
160
162
  - lib/cassandra_object/identity/abstract_key_factory.rb
161
163
  - lib/cassandra_object/identity/key.rb
@@ -163,13 +165,19 @@ files:
163
165
  - lib/cassandra_object/identity/uuid_key_factory.rb
164
166
  - lib/cassandra_object/indexes.rb
165
167
  - lib/cassandra_object/log_subscriber.rb
168
+ - lib/cassandra_object/migration.rb
166
169
  - lib/cassandra_object/migrations.rb
170
+ - lib/cassandra_object/migrator.rb
167
171
  - lib/cassandra_object/mocking.rb
168
172
  - lib/cassandra_object/persistence.rb
169
173
  - lib/cassandra_object/serialization.rb
174
+ - lib/cassandra_object/tasks/column_family.rb
175
+ - lib/cassandra_object/tasks/keyspace.rb
176
+ - lib/cassandra_object/tasks/ks.rb
170
177
  - lib/cassandra_object/type_registration.rb
171
178
  - lib/cassandra_object/types.rb
172
179
  - lib/cassandra_object/validation.rb
180
+ - test/active_model_test.rb
173
181
  - test/basic_scenarios_test.rb
174
182
  - test/callbacks_test.rb
175
183
  - test/config/cassandra.in.sh
@@ -204,7 +212,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
204
212
  requirements:
205
213
  - - ">="
206
214
  - !ruby/object:Gem::Version
207
- hash: 2344157015665262405
215
+ hash: -3516092246670549921
208
216
  segments:
209
217
  - 0
210
218
  version: "0"
@@ -224,6 +232,7 @@ signing_key:
224
232
  specification_version: 3
225
233
  summary: Cassandra ActiveModel
226
234
  test_files:
235
+ - test/active_model_test.rb
227
236
  - test/basic_scenarios_test.rb
228
237
  - test/callbacks_test.rb
229
238
  - test/connection.rb