gotime-cassandra_object 0.6.1 → 0.7.1

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/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