active_column 0.0.2 → 0.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/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