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 +0 -10
- data/VERSION +1 -1
- data/gotime-cassandra_object.gemspec +12 -3
- data/lib/cassandra_object.rb +20 -5
- data/lib/cassandra_object/associations.rb +4 -2
- data/lib/cassandra_object/base.rb +8 -20
- data/lib/cassandra_object/generators/migration_generator.rb +31 -0
- data/lib/cassandra_object/generators/templates/migration.rb.erb +11 -0
- data/lib/cassandra_object/identity.rb +4 -0
- data/lib/cassandra_object/log_subscriber.rb +14 -3
- data/lib/cassandra_object/migration.rb +126 -0
- data/lib/cassandra_object/migrator.rb +238 -0
- data/lib/cassandra_object/persistence.rb +38 -19
- data/lib/cassandra_object/tasks/column_family.rb +90 -0
- data/lib/cassandra_object/tasks/keyspace.rb +89 -0
- data/lib/cassandra_object/tasks/ks.rb +122 -0
- data/lib/cassandra_object/type_registration.rb +3 -1
- data/lib/cassandra_object/types.rb +17 -2
- data/lib/cassandra_object/validation.rb +13 -12
- data/test/active_model_test.rb +9 -0
- data/test/types_test.rb +3 -3
- data/test/validation_test.rb +3 -3
- metadata +13 -4
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.
|
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.
|
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{
|
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",
|
data/lib/cassandra_object.rb
CHANGED
@@ -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.
|
7
|
-
|
8
|
+
VERSION = "0.7.1"
|
9
|
+
extend ActiveSupport::Autoload
|
8
10
|
|
11
|
+
autoload :Base
|
9
12
|
|
10
|
-
require '
|
11
|
-
require '
|
13
|
+
require 'cassandra_object/migrator'
|
14
|
+
require 'cassandra_object/migration'
|
12
15
|
|
13
|
-
|
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
|
-
|
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
|
-
@
|
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
|
@@ -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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
152
|
-
|
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
|
-
|
169
|
+
_run_create_callbacks do
|
161
170
|
@key ||= self.class.next_key(self)
|
162
171
|
_write
|
163
|
-
@
|
164
|
-
|
172
|
+
@persisted = true
|
173
|
+
@key
|
165
174
|
end
|
166
175
|
end
|
167
176
|
|
168
177
|
def update
|
169
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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 ||
|
45
|
+
save || RecordInvalid.raise_error(self)
|
45
46
|
end
|
46
47
|
|
47
48
|
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({}
|
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
|
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
|
201
|
+
assert_equal(ActiveSupport::JSON.encode(h), CassandraObject::HashType.encode(h))
|
202
202
|
}
|
203
203
|
end
|
204
204
|
end
|
data/test/validation_test.rb
CHANGED
@@ -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::
|
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::
|
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
|
-
-
|
7
|
+
- 7
|
8
8
|
- 1
|
9
|
-
version: 0.
|
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:
|
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:
|
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
|