active_column 0.0.2 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/docs/Migrate.md ADDED
@@ -0,0 +1,100 @@
1
+ ## Data Migrations
2
+
3
+ The very first thing I would like to say about ActiveColumn Cassandra data migration is that *I stole most of the code
4
+ for this from the Rails gem (in ActiveSupport)*. I made the necessary changes to update a Cassandra database
5
+ instead of a relational DB. These changes were sort of significant, but I just wanted to give credit where credit
6
+ is due.
7
+
8
+ With that out of the way, we can discuss how you would use ActiveColumn to perform data migrations.
9
+
10
+ ### Creating keyspaces
11
+
12
+ First we will create our project's keyspaces.
13
+
14
+ 1. Make sure your cassandra 0.7 (or above) server is running.
15
+
16
+ 2. Make sure you have your _config/cassandra.yml_ file created. The [README](../README.md) has an example of
17
+ this file.
18
+
19
+ The ActiveColumn gem gives you several rake tasks within the **ks:** namespace. "ks" stands for keyspace, which is
20
+ the equivalent of a database in MySQL (or other relational dbs). To see the available tasks, run this rake command:
21
+
22
+ <pre>
23
+ rake -T ks
24
+ </pre>
25
+
26
+ 3. Create your databases with the **ks:create:all** rake task:
27
+
28
+ <pre>
29
+ rake ks:create:all
30
+ </pre>
31
+
32
+ Voila! You have now successfully created your keyspaces. Now let's generate some migration files.
33
+
34
+ ### Creating and running migrations
35
+
36
+ 4. ActiveColumn includes a generator to help you create blank migration files. To create a new migration, run this
37
+ command:
38
+
39
+ <pre>
40
+ rails g active_column:migration NameOfYourMigration
41
+ </pre>
42
+
43
+ The name of the migration might be something like "CreateUsersColumnFamily". After you run this command, you should see
44
+ a new file that is located here:
45
+
46
+ <pre>
47
+ ks/migrate/20101229183849_create_users_column_family.rb
48
+ </pre>
49
+
50
+ Note that the date stamp on the file will be different depending on when you create the migration. The migration file
51
+ will look like this:
52
+
53
+ <pre>
54
+ class CreateUsersColumnFamily &lt; ActiveColumn::Migration
55
+
56
+ def self.up
57
+
58
+ end
59
+
60
+ def self.down
61
+
62
+ end
63
+
64
+ end
65
+ </pre>
66
+
67
+ 5. Edit your new migration file to do what you want it to. For this migration, it would probably wind up looking like
68
+ this:
69
+
70
+ <pre>
71
+ class CreateUsersColumnFamily &lt; ActiveColumn::Migration
72
+
73
+ def self.up
74
+ create_column_family :users
75
+ end
76
+
77
+ def self.down
78
+ drop_column_family :users
79
+ end
80
+
81
+ end
82
+ </pre>
83
+
84
+ 6. Run the migrate rake task (for development):
85
+
86
+ <pre>
87
+ rake ks:migrate
88
+ </pre>
89
+
90
+ This will create the column family for your development environment. But you also need it in your test environment.
91
+ For now, you have to do this like the following. However, soon this will be updated to work more like ActiveRecord
92
+ migrations.
93
+
94
+ 7. Run the migrate rake task (for test):
95
+
96
+ <pre>
97
+ RAILS_ENV=test rake ks:migrate
98
+ </pre>
99
+
100
+ And BAM! You have your development and test keyspaces set up correctly.
data/docs/Query.md ADDED
@@ -0,0 +1,43 @@
1
+ ### Finding data
2
+
3
+ Ok, congratulations - now you have a bunch of fantastic data in Cassandra. How do you get it out? ActiveColumn can
4
+ help you here too.
5
+
6
+ Here is how you look up data that have a simple key:
7
+
8
+ <pre>
9
+ tweets = Tweet.find( 'mwynholds', :reversed => true, :count => 3 )
10
+ </pre>
11
+
12
+ This code will find the last 10 tweets for the 'mwynholds' user in reverse order. It comes back as a hash of arrays,
13
+ and would looks like this if represented in JSON:
14
+
15
+ <pre>
16
+ {
17
+ 'mwynholds': [ { 'user_id': 'mwynholds', 'message': 'I\'m going to bed now' },
18
+ { 'user_id': 'mwynholds', 'message': 'It\'s lunch time' },
19
+ { 'user_id': 'mwynholds', 'message': 'Just woke up' } ]
20
+ }
21
+ </pre>
22
+
23
+ Here are some other examples and their return values:
24
+
25
+ <pre>
26
+ Tweet.find( [ 'mwynholds', 'all' ], :count => 2 )
27
+
28
+ {
29
+ 'mwynholds': [ { 'user_id': 'mwynholds', 'message': 'Good morning' },
30
+ { 'user_id': 'mwynholds', 'message': 'Good afternoon' } ],
31
+ 'all': [ { 'user_id': 'mwynholds', 'message': 'Good morning' },
32
+ 'user_id': 'bmurray', 'message': 'Who ya gonna call!' } ]
33
+ }
34
+ </pre>
35
+
36
+ <pre>
37
+ Tweet.find( { 'user_id' => 'all', 'recipient_id' => [ 'fsinatra', 'dmartin' ] }, :reversed => true, :count => 1 )
38
+
39
+ {
40
+ 'all:fsinatra' => [ { 'user_id': 'mwynholds', 'recipient_ids' => [ 'fsinatra', 'dmartin' ], 'message' => 'Here we come Vegas!' } ],
41
+ 'all:dmartin' => [ { 'user_id': 'fsinatra', 'recipient_ids' => [ 'dmartin' ], 'message' => 'Vegas was fun' } ]
42
+ }
43
+ </pre>
data/lib/active_column.rb CHANGED
@@ -1,8 +1,27 @@
1
+ require 'cassandra/0.7'
2
+ require 'active_support'
3
+ require 'active_support/core_ext/string'
4
+
1
5
  module ActiveColumn
2
6
 
3
- autoload :Connection, 'active_column/connection'
4
- autoload :Base, 'active_column/base'
5
- autoload :Version, 'active_column/version'
7
+ autoload :Base, 'active_column/base'
8
+ autoload :Connection, 'active_column/connection'
9
+ autoload :KeyConfig, 'active_column/key_config'
10
+ autoload :Version, 'active_column/version'
11
+
12
+ require 'active_column/errors'
13
+ require 'active_column/migration'
14
+
15
+ module Tasks
16
+ autoload :Keyspace, 'active_column/tasks/keyspace'
17
+ autoload :ColumnFamily, 'active_column/tasks/column_family'
18
+
19
+ require 'active_column/tasks/ks'
20
+ end
21
+
22
+ module Generators
23
+ require 'active_column/generators/migration_generator'
24
+ end
6
25
 
7
26
  extend Connection
8
27
 
@@ -1,59 +1,33 @@
1
1
  module ActiveColumn
2
2
 
3
- class Base
4
-
5
- attr_reader :attributes
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
6
 
7
- def initialize(attrs = {})
8
- @attributes = attrs
9
- end
7
+ module ClassMethods
10
8
 
11
- def self.column_family(column_family = nil)
12
- return @column_family if column_family.nil?
9
+ def column_family(column_family = nil)
10
+ return @column_family || self.name.tableize.to_sym if column_family.nil?
13
11
  @column_family = column_family
14
12
  end
15
13
 
16
- def self.key(key, options = {})
14
+ def key(key, options = {})
17
15
  @keys ||= []
18
16
  @keys << KeyConfig.new(key, options)
19
17
  end
20
18
 
21
- def save()
22
- value = { SimpleUUID::UUID.new => self.to_json }
23
- key_parts = self.class.keys.each_with_object( {} ) do |key_config, key_parts|
24
- key_parts[key_config.key] = get_keys(key_config)
25
- end
26
- keys = self.class.generate_keys(key_parts)
27
-
28
- keys.each do |key|
29
- ActiveColumn.connection.insert(self.class.column_family, key, value)
30
- end
31
-
32
- self
19
+ def keys
20
+ @keys
33
21
  end
34
22
 
35
- def self.find(key_parts, options = {})
23
+ def find(key_parts, options = {})
36
24
  keys = generate_keys key_parts
37
25
  ActiveColumn.connection.multi_get(column_family, keys, options).each_with_object( {} ) do |(user, row), results|
38
- results[user] = row.to_a.collect { |(_uuid, col)| new(JSON.parse(col)) }
26
+ results[user] = row.to_a.collect { |(_uuid, col)| new(ActiveSupport::JSON.decode(col)) }
39
27
  end
40
28
  end
41
29
 
42
- def to_json(*a)
43
- @attributes.to_json(*a)
44
- end
45
-
46
- private
47
-
48
- def self.keys
49
- @keys
50
- end
51
-
52
- def get_keys(key_config)
53
- key_config.func.nil? ? attributes[key_config.key] : self.send(key_config.func)
54
- end
55
-
56
- def self.generate_keys(key_parts)
30
+ def generate_keys(key_parts)
57
31
  if keys.size == 1
58
32
  key_config = keys.first
59
33
  value = key_parts.is_a?(Hash) ? key_parts[key_config.key] : key_parts
@@ -73,17 +47,29 @@ module ActiveColumn
73
47
 
74
48
  end
75
49
 
76
- class KeyConfig
77
- attr_accessor :key, :func
50
+ def initialize(attrs = {})
51
+ attrs.each do |attr, value|
52
+ send("#{attr}=", value) if respond_to?("#{attr}=")
53
+ end
54
+ end
78
55
 
79
- def initialize(key, options)
80
- @key = key
81
- @func = options[:values]
56
+ def save()
57
+ value = { SimpleUUID::UUID.new => ActiveSupport::JSON.encode(self) }
58
+ key_parts = self.class.keys.each_with_object( {} ) do |key_config, key_parts|
59
+ key_parts[key_config.key] = self.send(key_config.func)
82
60
  end
61
+ keys = self.class.generate_keys(key_parts)
83
62
 
84
- def to_s
85
- "KeyConfig[#{key}, #{func or '-'}]"
63
+ keys.each do |key|
64
+ ActiveColumn.connection.insert(self.class.column_family, key, value)
86
65
  end
66
+
67
+ self
68
+ end
69
+
70
+ class Base
71
+ include ActiveColumn
87
72
  end
88
73
 
89
- end
74
+ end
75
+
@@ -0,0 +1,6 @@
1
+ module ActiveColumn
2
+
3
+ class ActiveColumnError < StandardError
4
+ end
5
+
6
+ end
@@ -0,0 +1,31 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/named_base'
3
+
4
+ module ActiveColumn
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 active_column: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 active_column: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.tableize}.rb"
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,11 @@
1
+ class <%= name %> < ActiveColumn::Migration
2
+
3
+ def self.up
4
+
5
+ end
6
+
7
+ def self.down
8
+
9
+ end
10
+
11
+ end
@@ -0,0 +1,16 @@
1
+ module ActiveColumn
2
+
3
+ class KeyConfig
4
+ attr_accessor :key, :func
5
+
6
+ def initialize(key, options)
7
+ @key = key
8
+ @func = options[:values] || key
9
+ end
10
+
11
+ def to_s
12
+ "KeyConfig[#{key}, #{func or '-'}]"
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,269 @@
1
+ module ActiveColumn
2
+
3
+ class IrreversibleMigration < ActiveColumnError
4
+ end
5
+
6
+ class DuplicateMigrationVersionError < ActiveColumnError#:nodoc:
7
+ def initialize(version)
8
+ super("Multiple migrations have the version number #{version}")
9
+ end
10
+ end
11
+
12
+ class DuplicateMigrationNameError < ActiveColumnError#:nodoc:
13
+ def initialize(name)
14
+ super("Multiple migrations have the name #{name}")
15
+ end
16
+ end
17
+
18
+ class UnknownMigrationVersionError < ActiveColumnError #:nodoc:
19
+ def initialize(version)
20
+ super("No migration with version number #{version}")
21
+ end
22
+ end
23
+
24
+ class IllegalMigrationNameError < ActiveColumnError#:nodoc:
25
+ def initialize(name)
26
+ super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
27
+ end
28
+ end
29
+
30
+ class Migration
31
+
32
+ def self.connection
33
+ $cassandra
34
+ end
35
+
36
+ def self.migrate(direction)
37
+ return unless respond_to?(direction)
38
+ send direction
39
+ end
40
+
41
+ def self.create_column_family(name, options = {})
42
+ ActiveColumn::Tasks::ColumnFamily.new.create(name, options)
43
+ end
44
+
45
+ def self.drop_column_family(name)
46
+ ActiveColumn::Tasks::ColumnFamily.new.drop(name)
47
+ end
48
+
49
+ end
50
+
51
+ # MigrationProxy is used to defer loading of the actual migration classes
52
+ # until they are needed
53
+ class MigrationProxy
54
+
55
+ attr_accessor :name, :version, :filename
56
+
57
+ delegate :migrate, :announce, :write, :to=>:migration
58
+
59
+ private
60
+
61
+ def migration
62
+ @migration ||= load_migration
63
+ end
64
+
65
+ def load_migration
66
+ require(File.expand_path(filename))
67
+ name.constantize
68
+ end
69
+
70
+ end
71
+
72
+ class Migrator
73
+
74
+ def self.migrate(migrations_path, target_version = nil)
75
+ case
76
+ when target_version.nil?
77
+ up(migrations_path, target_version)
78
+ when current_version == 0 && target_version == 0
79
+ when current_version > target_version
80
+ down(migrations_path, target_version)
81
+ else
82
+ up(migrations_path, target_version)
83
+ end
84
+ end
85
+
86
+ def self.rollback(migrations_path, steps = 1)
87
+ move(:down, migrations_path, steps)
88
+ end
89
+
90
+ def self.forward(migrations_path, steps = 1)
91
+ move(:up, migrations_path, steps)
92
+ end
93
+
94
+ def self.up(migrations_path, target_version = nil)
95
+ self.new(:up, migrations_path, target_version).migrate
96
+ end
97
+
98
+ def self.down(migrations_path, target_version = nil)
99
+ self.new(:down, migrations_path, target_version).migrate
100
+ end
101
+
102
+ def self.run(direction, migrations_path, target_version)
103
+ self.new(direction, migrations_path, target_version).run
104
+ end
105
+
106
+ def self.migrations_path
107
+ 'ks/migrate'
108
+ end
109
+
110
+ def self.schema_migrations_column_family
111
+ :schema_migrations
112
+ end
113
+
114
+ def self.get_all_versions
115
+ cas = ActiveColumn.connection
116
+ cas.get(schema_migrations_column_family, 'all').map {|(name, _value)| name.to_i}.sort
117
+ end
118
+
119
+ def self.current_version
120
+ sm_cf = schema_migrations_column_family
121
+ cf = ActiveColumn::Tasks::ColumnFamily.new
122
+ if cf.exists?(sm_cf)
123
+ get_all_versions.max || 0
124
+ else
125
+ 0
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ def self.move(direction, migrations_path, steps)
132
+ migrator = self.new(direction, migrations_path)
133
+ start_index = migrator.migrations.index(migrator.current_migration)
134
+
135
+ if start_index
136
+ finish = migrator.migrations[start_index + steps]
137
+ version = finish ? finish.version : 0
138
+ send(direction, migrations_path, version)
139
+ end
140
+ end
141
+
142
+ public
143
+
144
+ def initialize(direction, migrations_path, target_version = nil)
145
+ cf = ActiveColumn::Tasks::ColumnFamily.new
146
+ sm_cf = self.class.schema_migrations_column_family
147
+
148
+ unless cf.exists?(sm_cf)
149
+ cf.create(sm_cf, :comparator_type => 'LongType')
150
+ end
151
+
152
+ @direction, @migrations_path, @target_version = direction, migrations_path, target_version
153
+ end
154
+
155
+ def current_version
156
+ migrated.last || 0
157
+ end
158
+
159
+ def current_migration
160
+ migrations.detect { |m| m.version == current_version }
161
+ end
162
+
163
+ def run
164
+ target = migrations.detect { |m| m.version == @target_version }
165
+ raise UnknownMigrationVersionError.new(@target_version) if target.nil?
166
+ unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
167
+ target.migrate(@direction)
168
+ record_version_state_after_migrating(target)
169
+ end
170
+ end
171
+
172
+ def migrate
173
+ current = migrations.detect { |m| m.version == current_version }
174
+ target = migrations.detect { |m| m.version == @target_version }
175
+
176
+ if target.nil? && !@target_version.nil? && @target_version > 0
177
+ raise UnknownMigrationVersionError.new(@target_version)
178
+ end
179
+
180
+ start = up? ? 0 : (migrations.index(current) || 0)
181
+ finish = migrations.index(target) || migrations.size - 1
182
+ runnable = migrations[start..finish]
183
+
184
+ # skip the last migration if we're headed down, but not ALL the way down
185
+ runnable.pop if down? && !target.nil?
186
+
187
+ runnable.each do |migration|
188
+ #puts "Migrating to #{migration.name} (#{migration.version})"
189
+
190
+ # On our way up, we skip migrating the ones we've already migrated
191
+ next if up? && migrated.include?(migration.version.to_i)
192
+
193
+ # On our way down, we skip reverting the ones we've never migrated
194
+ if down? && !migrated.include?(migration.version.to_i)
195
+ migration.announce 'never migrated, skipping'; migration.write
196
+ next
197
+ end
198
+
199
+ migration.migrate(@direction)
200
+ record_version_state_after_migrating(migration)
201
+ end
202
+ end
203
+
204
+ def migrations
205
+ @migrations ||= begin
206
+ files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
207
+
208
+ migrations = files.inject([]) do |klasses, file|
209
+ version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
210
+
211
+ raise IllegalMigrationNameError.new(file) unless version
212
+ version = version.to_i
213
+
214
+ if klasses.detect { |m| m.version == version }
215
+ raise DuplicateMigrationVersionError.new(version)
216
+ end
217
+
218
+ if klasses.detect { |m| m.name == name.camelize }
219
+ raise DuplicateMigrationNameError.new(name.camelize)
220
+ end
221
+
222
+ migration = MigrationProxy.new
223
+ migration.name = name.camelize
224
+ migration.version = version
225
+ migration.filename = file
226
+ klasses << migration
227
+ end
228
+
229
+ migrations = migrations.sort_by { |m| m.version }
230
+ down? ? migrations.reverse : migrations
231
+ end
232
+ end
233
+
234
+ def pending_migrations
235
+ already_migrated = migrated
236
+ migrations.reject { |m| already_migrated.include?(m.version.to_i) }
237
+ end
238
+
239
+ def migrated
240
+ @migrated_versions ||= self.class.get_all_versions
241
+ end
242
+
243
+ private
244
+
245
+ def record_version_state_after_migrating(migration)
246
+ cas = ActiveColumn.connection
247
+ sm_cf = self.class.schema_migrations_column_family
248
+
249
+ @migrated_versions ||= []
250
+ if down?
251
+ @migrated_versions.delete(migration.version)
252
+ cas.remove sm_cf, 'all', migration.version
253
+ else
254
+ @migrated_versions.push(migration.version).sort!
255
+ cas.insert sm_cf, 'all', { migration.version => migration.name }
256
+ end
257
+ end
258
+
259
+ def up?
260
+ @direction == :up
261
+ end
262
+
263
+ def down?
264
+ @direction == :down
265
+ end
266
+
267
+ end
268
+
269
+ end