├── .gitignore
├── .rvmrc
├── .yardopts
├── Gemfile
├── Gemfile.lock
├── README.md
├── Rakefile
├── active_column.gemspec
├── docs
├── Create.md
├── Migrate.md
└── Query.md
├── generators
└── ks_migration
│ ├── USAGE
│ ├── ks_migration_generator.rb
│ └── templates
│ └── migration.rb.erb
├── lib
├── active_column.rb
└── active_column
│ ├── base.rb
│ ├── configuration.rb
│ ├── errors.rb
│ ├── generators
│ ├── migration_generator.rb
│ └── templates
│ │ └── migration.rb.erb
│ ├── helpers.rb
│ ├── key_config.rb
│ ├── migration.rb
│ ├── migrator.rb
│ ├── tasks
│ ├── column_family.rb
│ ├── keyspace.rb
│ └── ks.rake
│ └── version.rb
└── spec
├── active_column
├── base_crud_spec.rb
├── base_finders_spec.rb
├── migration_spec.rb
├── migrator_spec.rb
└── tasks
│ ├── column_family_spec.rb
│ └── keyspace_spec.rb
├── spec_helper.rb
└── support
├── aggregating_tweet.rb
├── compound_key.rb
├── config
└── storage-conf.xml
├── migrate
└── migrator_spec
│ ├── 1_migration1.rb
│ ├── 2_migration2.rb
│ ├── 3_migration3.rb
│ └── 4_migration4.rb
├── simple_key.rb
├── tweet.rb
└── tweet_dm.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | pkg/*
2 | doc/*
3 | .yardoc/*
4 | *.gem
5 | .bundle
6 | .idea
7 | vendor
8 |
--------------------------------------------------------------------------------
/.rvmrc:
--------------------------------------------------------------------------------
1 | rvm --create 1.9.2@active_column
2 |
--------------------------------------------------------------------------------
/.yardopts:
--------------------------------------------------------------------------------
1 | --no-private --protected - docs/*.md
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in active_column.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | active_column (0.2)
5 | cassandra (>= 0.12)
6 | rake
7 | simple_uuid (~> 0.1.0)
8 |
9 | GEM
10 | remote: http://rubygems.org/
11 | specs:
12 | ParseTree (3.0.7)
13 | RubyInline (>= 3.7.0)
14 | sexp_processor (>= 3.0.0)
15 | RubyInline (3.9.0)
16 | ZenTest (~> 4.3)
17 | ZenTest (4.5.0)
18 | abstract (1.0.0)
19 | actionmailer (3.0.7)
20 | actionpack (= 3.0.7)
21 | mail (~> 2.2.15)
22 | actionpack (3.0.7)
23 | activemodel (= 3.0.7)
24 | activesupport (= 3.0.7)
25 | builder (~> 2.1.2)
26 | erubis (~> 2.6.6)
27 | i18n (~> 0.5.0)
28 | rack (~> 1.2.1)
29 | rack-mount (~> 0.6.14)
30 | rack-test (~> 0.5.7)
31 | tzinfo (~> 0.3.23)
32 | activemodel (3.0.7)
33 | activesupport (= 3.0.7)
34 | builder (~> 2.1.2)
35 | i18n (~> 0.5.0)
36 | activerecord (3.0.7)
37 | activemodel (= 3.0.7)
38 | activesupport (= 3.0.7)
39 | arel (~> 2.0.2)
40 | tzinfo (~> 0.3.23)
41 | activeresource (3.0.7)
42 | activemodel (= 3.0.7)
43 | activesupport (= 3.0.7)
44 | activesupport (3.0.7)
45 | arel (2.0.9)
46 | bluecloth (2.1.0)
47 | builder (2.1.2)
48 | cassandra (0.12.0)
49 | json
50 | rake
51 | simple_uuid (>= 0.1.0)
52 | thrift_client (>= 0.7.0)
53 | diff-lcs (1.1.2)
54 | erubis (2.6.6)
55 | abstract (>= 1.0.0)
56 | file-tail (1.0.5)
57 | spruz (>= 0.1.0)
58 | i18n (0.5.0)
59 | json (1.5.3)
60 | mail (2.2.19)
61 | activesupport (>= 2.3.6)
62 | i18n (>= 0.4.0)
63 | mime-types (~> 1.16)
64 | treetop (~> 1.4.8)
65 | mime-types (1.16)
66 | mocha (0.9.12)
67 | polyglot (0.3.1)
68 | predicated (0.2.6)
69 | rack (1.2.2)
70 | rack-mount (0.6.14)
71 | rack (>= 1.0.0)
72 | rack-test (0.5.7)
73 | rack (>= 1.0)
74 | rails (3.0.7)
75 | actionmailer (= 3.0.7)
76 | actionpack (= 3.0.7)
77 | activerecord (= 3.0.7)
78 | activeresource (= 3.0.7)
79 | activesupport (= 3.0.7)
80 | bundler (~> 1.0)
81 | railties (= 3.0.7)
82 | railties (3.0.7)
83 | actionpack (= 3.0.7)
84 | activesupport (= 3.0.7)
85 | rake (>= 0.8.7)
86 | thor (~> 0.14.4)
87 | rake (0.9.2)
88 | rspec (2.5.0)
89 | rspec-core (~> 2.5.0)
90 | rspec-expectations (~> 2.5.0)
91 | rspec-mocks (~> 2.5.0)
92 | rspec-core (2.5.1)
93 | rspec-expectations (2.5.0)
94 | diff-lcs (~> 1.1.2)
95 | rspec-mocks (2.5.0)
96 | rspec-rails (2.5.0)
97 | actionpack (~> 3.0)
98 | activesupport (~> 3.0)
99 | railties (~> 3.0)
100 | rspec (~> 2.5.0)
101 | ruby2ruby (1.2.5)
102 | ruby_parser (~> 2.0)
103 | sexp_processor (~> 3.0)
104 | ruby_parser (2.0.6)
105 | sexp_processor (~> 3.0)
106 | sexp_processor (3.0.5)
107 | simple_uuid (0.1.2)
108 | sourcify (0.4.2)
109 | file-tail (>= 1.0.5)
110 | ruby2ruby (>= 1.2.5)
111 | sexp_processor (>= 3.0.5)
112 | spruz (0.2.5)
113 | thor (0.14.6)
114 | thrift (0.7.0)
115 | thrift_client (0.7.0)
116 | thrift (~> 0.7.0)
117 | treetop (1.4.9)
118 | polyglot (>= 0.3.1)
119 | tzinfo (0.3.27)
120 | wrong (0.5.0)
121 | ParseTree (~> 3.0)
122 | diff-lcs (~> 1.1.2)
123 | file-tail (~> 1.0)
124 | predicated (>= 0.2.2)
125 | ruby2ruby (~> 1.2)
126 | ruby_parser (~> 2.0.4)
127 | sexp_processor (~> 3.0)
128 | sourcify (>= 0.3.0)
129 | yard (0.6.8)
130 |
131 | PLATFORMS
132 | ruby
133 |
134 | DEPENDENCIES
135 | active_column!
136 | bluecloth
137 | mocha
138 | rails (>= 3.0)
139 | rspec-rails (>= 2.5.0)
140 | wrong
141 | yard
142 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **IMPORTANT**: If you are reading this on the main ActiveColumn page on github, please go to
2 | [the actual README page](./active_column/blob/master/README.md) so that links bring you to the right place.
3 |
4 | # ActiveColumn
5 |
6 | ActiveColumn is a framework for working with data in Cassandra. It currently includes two features:
7 |
8 | - Database migrations
9 | - "Time line" model data management
10 |
11 | Data migrations are very similar to those in ActiveRecord, and are documented in [Migrate](./docs/Migrate.md).
12 |
13 | Time line data management is loosely based on concepts in ActiveRecord, but is adapted to saving data in which rows in
14 | Cassandra grow indefinitely over time, such as in the oft-used Twitter example for Cassandra. This usage is documented
15 | in:
16 |
17 | - [Create](./docs/Create.md) - how to create data
18 | - [Query](./docs/Query.md) - how to find data
19 |
20 | ## Installation
21 |
22 | Add ActiveColumn to your Gemfile:
23 |
24 | gem 'active_column'
25 |
26 |
27 | Install with bundler:
28 |
29 | bundle install
30 |
31 |
32 | ## Usage
33 |
34 | ### Configuration
35 |
36 | ActiveColumn requires Cassandra 0.7 or above, as well as the [cassandra gem](https://github.com/twitter/cassandra),
37 | version 0.9 or above. You must also be sure to use the Cassandra 0.7 support in the gem, which can be done by
38 | adding Cassandra to your Gemfile like this:
39 |
40 | gem 'cassandra', '>= 0.9', :require => 'cassandra/0.7'
41 |
42 |
43 | Data migrations in ActiveColumn are used within a Rails project, and are driven off of a configuration file,
44 | config/cassandra.yml. It should look something like this:
45 |
46 | _config/cassandra.yml_
47 |
48 | test:
49 | servers: "127.0.0.1:9160"
50 | keyspace: "myapp_test"
51 | thrift:
52 | timeout: 3
53 | retries: 2
54 |
55 | development:
56 | servers: "127.0.0.1:9160"
57 | keyspace: "myapp_development"
58 | thrift:
59 | timeout: 3
60 | retries: 2
61 |
62 |
63 | You can use embedded ruby code in the YAML file to determine host/machine specific settings.
64 |
65 |
66 | production:
67 | servers: "<%=get_from_file('abc.conf')%>:9160"
68 | keyspace: "<%=get_from_file('abc.conf')%>"
69 | disable_node_auto_discovery: true
70 | thrift:
71 | timeout: 3
72 | retries: 2
73 |
74 |
75 | Node Auto Discovery
76 |
77 | You can set disable_node_auto_discovery to off by setting disable_node_auto_discovery flag in your cassandra.yml
78 |
79 | In order to get time line modeling support, you must provide ActiveColumn with an instance of a Cassandra object.
80 | Since you have your cassandra.yml from above, you can do this very simply like this:
81 |
82 |
83 | _config/initializers/cassandra.rb_
84 |
85 | config = YAML.load_file(Rails.root.join("config", "cassandra.yml"))[Rails.env]
86 | $cassandra = Cassandra.new(config['keyspace'],
87 | config['servers'],
88 | config['thrift'])
89 |
90 | ActiveColumn.connection = $cassandra
91 |
92 |
93 | As you can see, I create a global $cassandra variable, which I use in my tests to validate data directly in Cassandra.
94 |
95 | ### Examples
96 |
97 | Add column family
98 |
99 | create_column_family :impressions do |cf|
100 | cf.comment = 'impressions for something'
101 | cf.comparator_type = :utf8
102 | cf.key_validation_class = :utf8
103 | end
104 |
105 |
106 | Drop column family
107 |
108 | drop_column_family :impressions
109 |
110 |
111 | Rename column family
112 |
113 | rename_column_family :impressions, :showings
114 |
115 |
116 | Update column family
117 |
118 | update_column_family :impressions do |cf|
119 | cf.comment = "blah"
120 | cf.gc_grace_seconds = 3600
121 | end
122 |
123 |
124 | One other thing to note is that you obviously must have Cassandra installed and running! Please take a look at the
125 | [mama_cass gem](https://github.com/carbonfive/mama_cass) for a quick way to get up and running with Cassandra for
126 | development and testing.
127 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler'
2 | Bundler::GemHelper.install_tasks
3 |
4 | require 'rspec/core/rake_task'
5 | RSpec::Core::RakeTask.new(:spec)
6 |
7 | task :default => :spec
8 |
--------------------------------------------------------------------------------
/active_column.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path("../lib", __FILE__)
3 | require "active_column/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = "active_column"
7 | s.version = ActiveColumn::VERSION
8 | s.platform = Gem::Platform::RUBY
9 | s.authors = ["Michael Wynholds"]
10 | s.email = ["mike@wynholds.com"]
11 | s.homepage = "https://github.com/carbonfive/active_column"
12 | s.summary = %q{Provides time line support and database migrations for Cassandra}
13 | s.description = %q{Provides time line support and database migrations for Cassandra}
14 |
15 | s.rubyforge_project = "active_column"
16 |
17 | s.files = `git ls-files`.split("\n")
18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20 | s.require_paths = ["lib"]
21 |
22 | s.add_dependency 'cassandra', '>= 0.12'
23 | s.add_dependency 'simple_uuid', '>= 0.1.0'
24 | s.add_dependency 'rake'
25 |
26 | s.add_development_dependency 'rails', '>= 3.0'
27 | s.add_development_dependency 'rspec-rails', '>= 2.5.0'
28 | s.add_development_dependency 'wrong'
29 | s.add_development_dependency 'yard'
30 | s.add_development_dependency 'bluecloth'
31 | s.add_development_dependency 'mocha'
32 | end
33 |
--------------------------------------------------------------------------------
/docs/Create.md:
--------------------------------------------------------------------------------
1 | ### Saving data
2 |
3 | To make a model in to an ActiveColumn model, just extend ActiveColumn::Base, and provide two pieces of information:
4 |
5 | - Column Family (optional)
6 | - Function(s) to generate keys for your rows of data
7 |
8 | If you do not specify a column family, it will default to the "tabelized" class name, just like ActiveRecord.
9 | Example: Tweet --> tweets
10 | Example: TweetDM --> tweet_dms
11 |
12 | The most basic form of using ActiveColumn looks like this:
13 |
14 | class Tweet < ActiveColumn::Base
15 | key :user_id
16 | attr_accessor :user_id, :message
17 | end
18 |
19 |
20 | Note that you can also use ActiveColumn as a mix-in, like this:
21 |
22 | class Tweet
23 | include ActiveColumn
24 |
25 | key :user_id
26 | attr_accessor :user_id, :message
27 | end
28 |
29 |
30 | Then in your app you can create and save a tweet like this:
31 |
32 | tweet = Tweet.new( :user_id => 'mwynholds', :message => "I'm going for a bike ride" )
33 | tweet.save
34 |
35 |
36 | When you run #save, ActiveColumn saves a new column in the "tweets" column family in the row with key "mwynholds". The
37 | content of the row is the Tweet instance JSON-encoded.
38 |
39 | *Key Generator Functions*
40 |
41 | This is great, but quite often you want to save the content in multiple rows for the sake of speedy lookups. This is
42 | basically de-normalizing data, and is extremely common in Cassandra data. ActiveColumn lets you do this quite easily
43 | by telling it the name of a function to use to generate the keys during a save. It works like this:
44 |
45 |
46 | class Tweet
47 | include ActiveColumn
48 |
49 | key :user_id, :values => :generate_user_keys
50 | attr_accessor :user_id, :message
51 |
52 | def generate_user_keys
53 | [ user_id, 'all']
54 | end
55 | end
56 |
57 |
58 | The code to save the tweet is the same as the previous example, but now it saves the tweet in both the "mwynholds" row
59 | and the "all" row. This way, you can pull out the last 20 of all tweets quite easily (assuming you needed to do this
60 | in your app).
61 |
62 | *Compound Keys*
63 |
64 | In some cases you may want to have your rows keyed by multiple values. ActiveColumn supports compound keys,
65 | and looks like this:
66 |
67 |
68 | class TweetDM
69 | include ActiveColumn
70 |
71 | column_family :tweet_dms
72 | key :user_id, :values => :generate_user_keys
73 | key :recipient_id, :values => :recipient_ids
74 | attr_accessor :user_id, :recipient_ids, :message
75 |
76 | def generate_user_keys
77 | [ user_id, 'all ]
78 | end
79 | end
80 |
81 |
82 | Now, when you create a new TweetDM, it might look like this:
83 |
84 |
85 | dm = TweetDM.new( :user_id => 'mwynholds', :recipient_ids => [ 'fsinatra', 'dmartin' ], :message => "Let's go to Vegas" )
86 |
87 |
88 | This tweet direct message will saved to four different rows in the "tweet_dms" column family, under these keys:
89 |
90 | - mwynholds:fsinatra
91 | - mwynholds:dmartin
92 | - all:fsinatra
93 | - all:dmartin
94 |
95 | Now my app can pretty easily figure find all DMs I sent to Old Blue Eyes, or to Dino, and it can also easily find all
96 | DMs sent from *anyone* to Frank or Dino.
97 |
98 | One thing to note about the TweetDM class above is that the "keys" configuration at the top looks a little uglier than
99 | before. If you have a compound key and any of the keys have custom key generators, you need to pass in an array of
100 | single-element hashes. This is in place to support Ruby 1.8, which does not have ordered hashes. Making sure the keys
101 | are ordered is necessary to keep the compounds keys canonical (ie: deterministic).
--------------------------------------------------------------------------------
/docs/Migrate.md:
--------------------------------------------------------------------------------
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 |
23 | rake -T ks
24 |
25 |
26 | 3. Create your keyspaces with the **ks:create:all** rake task:
27 |
28 |
29 | rake ks:create:all
30 |
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 |
40 | rails g active_column:migration NameOfYourMigration
41 |
42 |
43 | If you are using Rails 2, run this command instead:
44 |
45 | ./script/generate ks_migration NameOfYourMigration
46 |
47 |
48 | The name of the migration might be something like "CreateUsersColumnFamily". After you run this command, you should see
49 | a new file that is located here:
50 |
51 |
52 | ks/migrate/20101229183849_create_users_column_family.rb
53 |
54 |
55 | Note that the date stamp on the file will be different depending on when you create the migration. The migration file
56 | will look like this:
57 |
58 |
59 | class CreateUsersColumnFamily < ActiveColumn::Migration
60 |
61 | def self.up
62 |
63 | end
64 |
65 | def self.down
66 |
67 | end
68 |
69 | end
70 |
71 |
72 | 5. Edit your new migration file to do what you want it to. For this migration, it would probably wind up looking like
73 | this:
74 |
75 |
76 | class CreateUsersColumnFamily < ActiveColumn::Migration
77 |
78 | def self.up
79 | create_column_family :users do |cf|
80 | cf.comment = 'Users column family'
81 | cf.comparator_type = :string
82 | end
83 | end
84 |
85 | def self.down
86 | drop_column_family :users
87 | end
88 |
89 | end
90 |
91 |
92 | 6. Run the migrate rake task (for development):
93 |
94 |
95 | rake ks:migrate
96 |
97 |
98 | This will create the column family for your development environment. But you also need it in your test environment.
99 |
100 | 7. Prepare the test environment keyspace:
101 |
102 |
103 | rake ks:test:prepare
104 |
105 |
106 | And BAM! You have your development and test keyspaces set up correctly.
107 |
108 | ### Inside your migrations
109 |
110 | ActiveColumn::Migration, which all migrations extend by default, offers some useful functions. They are documented
111 | via rdoc in the code itself.
112 |
113 | ### But I'm using Sinatra!
114 |
115 | If you are using Rails, you don't need to do anything beyond including the active\_column gem in your Gemfile.
116 | However, if you are using Sinatra (or some other framework), you can get these rake tasks to work merely by adding
117 | the following line to your Rakefile:
118 |
119 | require 'active_column'
120 |
121 |
122 | Please note, however, that the Rails generator is only available if you are using Rails. If you are not using Rails,
123 | you will have to create your migrations by hand.
124 |
--------------------------------------------------------------------------------
/docs/Query.md:
--------------------------------------------------------------------------------
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 |
9 | tweets = Tweet.find( 'mwynholds', :reversed => true, :count => 3 )
10 |
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 |
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 |
22 |
23 | Here are some other examples and their return values:
24 |
25 |
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 |
35 |
36 |
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 |
--------------------------------------------------------------------------------
/generators/ks_migration/USAGE:
--------------------------------------------------------------------------------
1 | Description:
2 | Create an empty Cassandra migration file in 'ks/migrate'. Very similar to Rails database migrations.
3 |
4 | Example:
5 | ./script/generate ks_migration CreateFooColumnFamily
6 |
7 |
--------------------------------------------------------------------------------
/generators/ks_migration/ks_migration_generator.rb:
--------------------------------------------------------------------------------
1 | require 'rails_generator/base'
2 |
3 | class KsMigrationGenerator < Rails::Generator::NamedBase
4 |
5 | def manifest
6 | record do |m|
7 | m.directory 'ks/migrate'
8 | timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
9 | m.template 'migration.rb.erb', "ks/migrate/#{timestamp}_#{file_name.underscore}.rb"
10 | end
11 | end
12 |
13 | def banner
14 | "Usage: ./script/generate ks_migration NAME [options]"
15 | end
16 |
17 | end
18 |
--------------------------------------------------------------------------------
/generators/ks_migration/templates/migration.rb.erb:
--------------------------------------------------------------------------------
1 | class <%= name %> < ActiveColumn::Migration
2 |
3 | def self.up
4 |
5 | end
6 |
7 | def self.down
8 |
9 | end
10 |
11 | end
--------------------------------------------------------------------------------
/lib/active_column.rb:
--------------------------------------------------------------------------------
1 | require 'cassandra/0.8' unless defined? ::Cassandra
2 | require 'benchmark'
3 | require 'yaml'
4 |
5 | module ActiveColumn
6 |
7 | autoload :Base, 'active_column/base'
8 | autoload :Configuration, 'active_column/configuration'
9 | autoload :KeyConfig, 'active_column/key_config'
10 | autoload :Version, 'active_column/version'
11 | autoload :Helpers, 'active_column/helpers'
12 |
13 | require 'active_column/errors'
14 | require 'active_column/migrator'
15 | require 'active_column/migration'
16 |
17 | module Tasks
18 | autoload :Keyspace, 'active_column/tasks/keyspace'
19 | autoload :ColumnFamily, 'active_column/tasks/column_family'
20 | end
21 |
22 | load 'active_column/tasks/ks.rake'
23 |
24 |
25 | if defined? ::Rails
26 | module Generators
27 | if ::Rails::VERSION::MAJOR >= 3
28 | require 'active_column/generators/migration_generator'
29 | end
30 | end
31 | end
32 |
33 | extend Configuration
34 |
35 | end
36 |
--------------------------------------------------------------------------------
/lib/active_column/base.rb:
--------------------------------------------------------------------------------
1 | module ActiveColumn
2 |
3 | def self.included(base)
4 | base.extend ClassMethods
5 | end
6 |
7 | module ClassMethods
8 |
9 | def column_family(column_family = nil)
10 | return @column_family || self.name.tableize.to_sym if column_family.nil?
11 | @column_family = column_family
12 | end
13 |
14 | def key(key, options = {})
15 | @keys ||= []
16 | @keys << KeyConfig.new(key, options)
17 | end
18 |
19 | def keys
20 | @keys
21 | end
22 |
23 | def find(key_parts, options = {})
24 | keys = generate_keys key_parts
25 | ActiveColumn.connection.multi_get(column_family, keys, options).each_with_object( {} ) do |(user, row), results|
26 | results[user] = row.to_a.collect { |(_uuid, col)| new(JSON.parse(col)) }
27 | end
28 | end
29 |
30 | def generate_keys(key_parts)
31 | if keys.size == 1
32 | key_config = keys.first
33 | value = key_parts.is_a?(Hash) ? key_parts[key_config.key] : key_parts
34 | return value if value.is_a? Array
35 | return [value]
36 | end
37 |
38 | values = keys.collect { |kc| key_parts[kc.key] }
39 | product = values.reduce do |memo, key_part|
40 | memo = [memo] unless memo.is_a? Array
41 | key_part = [key_part] unless key_part.is_a? Array
42 | memo.product key_part
43 | end
44 |
45 | product.collect { |p| p.join(':') }
46 | end
47 |
48 | end
49 |
50 | def initialize(attrs = {})
51 | attrs.each do |attr, value|
52 | send("#{attr}=", value) if respond_to?("#{attr}=")
53 | end
54 | end
55 |
56 | def save()
57 | value = { SimpleUUID::UUID.new.to_s => self.to_json }
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)
60 | end
61 | keys = self.class.generate_keys(key_parts)
62 |
63 | keys.each do |key|
64 | ActiveColumn.connection.insert(self.class.column_family, key, value)
65 | end
66 |
67 | self
68 | end
69 |
70 | class Base
71 | include ActiveColumn
72 | end
73 |
74 | end
75 |
76 |
--------------------------------------------------------------------------------
/lib/active_column/configuration.rb:
--------------------------------------------------------------------------------
1 | module ActiveColumn
2 |
3 | module Configuration
4 |
5 | def connect(config)
6 | default_thrift_options = { :timeout => 3, :retries => 2, :server_retry_period => nil }
7 | override_thrift_options = (config['thrift'] || {}).inject({}){|h, (k, v)| h[k.to_sym] = v; h} # symbolize keys
8 | thrift_options = default_thrift_options.merge(override_thrift_options)
9 | self.connection = Cassandra.new(config['keyspace'], config['servers'], thrift_options)
10 | self.connection.disable_node_auto_discovery! if config['disable_node_auto_discovery'] == true
11 | self.connection
12 | end
13 |
14 | def connected?
15 | defined? @@connection
16 | end
17 |
18 | def connection
19 | @@connection
20 | end
21 |
22 | def connection=(connection)
23 | @@connection = connection
24 | @@keyspace_tasks = ActiveColumn::Tasks::Keyspace.new
25 | @@keyspace = connection.keyspace
26 | end
27 |
28 | def keyspace_tasks
29 | @@keyspace_tasks
30 | end
31 |
32 | def column_family_tasks
33 | ActiveColumn::Tasks::ColumnFamily.new(@@keyspace)
34 | end
35 |
36 | end
37 |
38 | end
39 |
--------------------------------------------------------------------------------
/lib/active_column/errors.rb:
--------------------------------------------------------------------------------
1 | module ActiveColumn
2 |
3 | class ActiveColumnError < StandardError
4 | end
5 |
6 | end
7 |
--------------------------------------------------------------------------------
/lib/active_column/generators/migration_generator.rb:
--------------------------------------------------------------------------------
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 | < < ActiveColumn::Migration
2 |
3 | def self.up
4 |
5 | end
6 |
7 | def self.down
8 |
9 | end
10 |
11 | end
--------------------------------------------------------------------------------
/lib/active_column/helpers.rb:
--------------------------------------------------------------------------------
1 | module ActiveColumn
2 | module Helpers
3 |
4 | def self.current_env
5 | ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
6 | end
7 |
8 | def current_env
9 | ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
10 | end
11 |
12 | def testing?
13 | self.current_env == 'test'
14 | end
15 |
16 | def log(msg, e = nil)
17 | puts msg if e || !testing?
18 | p e if e
19 | nil
20 | end
21 |
22 | end
23 | end
24 |
25 | class Object
26 | def to_json(*a)
27 | result = {
28 | JSON.create_id => self.class.name
29 | }
30 | instance_variables.inject(result) do |r, name|
31 | r[name[1..-1]] = instance_variable_get name
32 | r
33 | end
34 | result.to_json(*a)
35 | end
36 | end
37 |
38 | if ! defined? String.tableize
39 | class String
40 | def tableize
41 | t = self.dup.to_s
42 | t += ( t =~ /s$/ ? 'es' : 's' )
43 | t.gsub!(/::/, '/')
44 | t.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
45 | t.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
46 | t.tr!("-", "_")
47 | t.downcase!
48 | t
49 | end
50 | end
51 | end
--------------------------------------------------------------------------------
/lib/active_column/key_config.rb:
--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------
/lib/active_column/migration.rb:
--------------------------------------------------------------------------------
1 | module ActiveColumn
2 |
3 | class Migration
4 |
5 | @@verbose = true
6 |
7 | def self.verbose=(verbose)
8 | @@verbose = verbose
9 | end
10 |
11 | def self.verbose
12 | @@verbose
13 | end
14 |
15 | # Returns the raw connection to Cassandra
16 | def self.connection
17 | ActiveColumn.connection
18 | end
19 |
20 | def self.migrate(direction)
21 | return unless respond_to?(direction)
22 |
23 | case direction
24 | when :up then announce "migrating"
25 | when :down then announce "reverting"
26 | end
27 |
28 | result = nil
29 | time = Benchmark.measure { result = send("#{direction}") }
30 |
31 | case direction
32 | when :up then announce "migrated (%.4fs)" % time.real; write
33 | when :down then announce "reverted (%.4fs)" % time.real; write
34 | end
35 |
36 | result
37 | end
38 |
39 | # Creates a new column family with the given name. Column family configurations can be set within
40 | # a block like this:
41 | #
42 | # create_column_family(:users) do |cf|
43 | # cf.comment = 'Users column family'
44 | # cf.comparator_type = 'TimeUUIDType'
45 | # end
46 | #
47 | # A complete list of available configuration settings is here:
48 | #
49 | # http://github.com/fauna/cassandra/blob/master/vendor/0.7/gen-rb/cassandra_types.rb
50 | #
51 | # Scroll down to the CfDef definition.
52 | def self.create_column_family(name, &block)
53 | ActiveColumn.column_family_tasks.create(name, &block)
54 | end
55 |
56 | def self.update_column_family(name, &block)
57 | ActiveColumn.column_family_tasks.update(name, &block)
58 | end
59 |
60 | # Drops the given column family
61 | def self.drop_column_family(name)
62 | ActiveColumn.column_family_tasks.drop(name)
63 | end
64 |
65 | # Renames the column family from the old name to the new name
66 | def self.rename_column_family(old_name, new_name)
67 | ActiveColumn.column_family_tasks.rename(old_name, new_name)
68 | end
69 |
70 | def self.write(text="")
71 | puts(text) if verbose
72 | end
73 |
74 | def self.announce(message)
75 | version = defined?(@version) ? @version : nil
76 |
77 | text = "#{version} #{name}: #{message}"
78 | length = [0, 75 - text.length].max
79 | write "== %s %s" % [text, "=" * length]
80 | end
81 |
82 | def self.say(message, subitem=false)
83 | write "#{subitem ? " ->" : "--"} #{message}"
84 | end
85 |
86 | def self.say_with_time(message)
87 | say(message)
88 | result = nil
89 | time = Benchmark.measure { result = yield }
90 | say "%.4fs" % time.real, :subitem
91 | say("#{result} rows", :subitem) if result.is_a?(Integer)
92 | result
93 | end
94 |
95 | def self.suppress_messages
96 | save, self.verbose = verbose, false
97 | yield
98 | ensure
99 | self.verbose = save
100 | end
101 |
102 | end
103 |
104 | # MigrationProxy is used to defer loading of the actual migration classes
105 | # until they are needed
106 | class MigrationProxy
107 |
108 | attr_accessor :name, :version, :filename
109 |
110 | def migrate(*args)
111 | migration.migrate *args
112 | end
113 |
114 | def announce(*args)
115 | migration.announce *args
116 | end
117 |
118 | def write(*args)
119 | migration.write *args
120 | end
121 |
122 | private
123 |
124 | def migration
125 | @migration ||= load_migration
126 | end
127 |
128 | def load_migration
129 | require(File.expand_path(filename))
130 | eval(name)
131 | end
132 |
133 | end
134 |
135 | end
136 |
--------------------------------------------------------------------------------
/lib/active_column/migrator.rb:
--------------------------------------------------------------------------------
1 | module ActiveColumn
2 | class IrreversibleMigration < ActiveColumnError
3 | end
4 |
5 | class DuplicateMigrationVersionError < ActiveColumnError#:nodoc:
6 | def initialize(version)
7 | super("Multiple migrations have the version number #{version}")
8 | end
9 | end
10 |
11 | class DuplicateMigrationNameError < ActiveColumnError#:nodoc:
12 | def initialize(name)
13 | super("Multiple migrations have the name #{name}")
14 | end
15 | end
16 |
17 | class UnknownMigrationVersionError < ActiveColumnError #:nodoc:
18 | def initialize(version)
19 | super("No migration with version number #{version}")
20 | end
21 | end
22 |
23 | class IllegalMigrationNameError < ActiveColumnError#: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 | self.new(:up, migrations_path, target_version).migrate
53 | end
54 |
55 | def self.down(migrations_path, target_version = nil)
56 | self.new(:down, migrations_path, target_version).migrate
57 | end
58 |
59 | def self.run(direction, migrations_path, target_version)
60 | self.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.get_all_versions
72 | cas = ActiveColumn.connection
73 | cas.get(schema_migrations_column_family, 'all').map {|(name, _value)| name.to_i}.sort
74 | end
75 |
76 | def self.current_version
77 | sm_cf = schema_migrations_column_family
78 | if ActiveColumn.column_family_tasks.exists?(sm_cf)
79 | get_all_versions.max || 0
80 | else
81 | 0
82 | end
83 | end
84 |
85 | private
86 |
87 | def self.move(direction, migrations_path, steps)
88 | migrator = self.new(direction, migrations_path)
89 | start_index = migrator.migrations.index(migrator.current_migration)
90 |
91 | if start_index
92 | finish = migrator.migrations[start_index + steps]
93 | version = finish ? finish.version : 0
94 | send(direction, migrations_path, version)
95 | end
96 | end
97 |
98 | public
99 |
100 | def initialize(direction, migrations_path, target_version = nil)
101 | cf_tasks = ActiveColumn.column_family_tasks
102 | sm_cf = self.class.schema_migrations_column_family
103 |
104 | unless cf_tasks.exists?(sm_cf)
105 | cf_tasks.create(sm_cf) do |cf|
106 | cf.comparator_type = 'LongType'
107 | end
108 | end
109 |
110 | @direction, @migrations_path, @target_version = direction, migrations_path, target_version
111 | end
112 |
113 | def current_version
114 | migrated.last || 0
115 | end
116 |
117 | def current_migration
118 | migrations.detect { |m| m.version == current_version }
119 | end
120 |
121 | def run
122 | target = migrations.detect { |m| m.version == @target_version }
123 | raise UnknownMigrationVersionError.new(@target_version) if target.nil?
124 | unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
125 | target.migrate(@direction)
126 | record_version_state_after_migrating(target)
127 | end
128 | end
129 |
130 | def migrate
131 | current = migrations.detect { |m| m.version == current_version }
132 | target = migrations.detect { |m| m.version == @target_version }
133 |
134 | if target.nil? && !@target_version.nil? && @target_version > 0
135 | raise UnknownMigrationVersionError.new(@target_version)
136 | end
137 |
138 | start = up? ? 0 : (migrations.index(current) || 0)
139 | finish = migrations.index(target) || migrations.size - 1
140 | runnable = migrations[start..finish]
141 |
142 | # skip the last migration if we're headed down, but not ALL the way down
143 | runnable.pop if down? && !target.nil?
144 |
145 | runnable.each do |migration|
146 | #puts "Migrating to #{migration.name} (#{migration.version})"
147 |
148 | # On our way up, we skip migrating the ones we've already migrated
149 | next if up? && migrated.include?(migration.version.to_i)
150 |
151 | # On our way down, we skip reverting the ones we've never migrated
152 | if down? && !migrated.include?(migration.version.to_i)
153 | migration.announce 'never migrated, skipping'; migration.write
154 | next
155 | end
156 |
157 | migration.migrate(@direction)
158 | record_version_state_after_migrating(migration)
159 | end
160 | end
161 |
162 | def migrations
163 | @migrations ||= begin
164 | files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
165 |
166 | migrations = files.inject([]) do |klasses, file|
167 | version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
168 |
169 | raise IllegalMigrationNameError.new(file) unless version
170 | version = version.to_i
171 |
172 | if klasses.detect { |m| m.version == version }
173 | raise DuplicateMigrationVersionError.new(version)
174 | end
175 |
176 | if klasses.detect { |m| m.name == name }
177 | raise DuplicateMigrationNameError.new(name)
178 | end
179 |
180 | migration = MigrationProxy.new
181 | migration.name = to_camel name
182 | migration.version = version
183 | migration.filename = file
184 | klasses << migration
185 | end
186 |
187 | migrations = migrations.sort_by { |m| m.version }
188 | down? ? migrations.reverse : migrations
189 | end
190 | end
191 |
192 | def pending_migrations
193 | already_migrated = migrated
194 | migrations.reject { |m| already_migrated.include?(m.version.to_i) }
195 | end
196 |
197 | def migrated
198 | @migrated_versions ||= self.class.get_all_versions
199 | end
200 |
201 | private
202 |
203 | def record_version_state_after_migrating(migration)
204 | cas = ActiveColumn.connection
205 | sm_cf = self.class.schema_migrations_column_family
206 |
207 | @migrated_versions ||= []
208 | if down?
209 | @migrated_versions.delete(migration.version)
210 | cas.remove sm_cf, 'all', migration.version
211 | else
212 | @migrated_versions.push(migration.version).sort!
213 | cas.insert sm_cf, 'all', { migration.version => migration.name }
214 | end
215 | end
216 |
217 | def up?
218 | @direction == :up
219 | end
220 |
221 | def down?
222 | @direction == :down
223 | end
224 |
225 | def to_camel(lower_case_and_underscored_word)
226 | lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
227 | end
228 |
229 | end
230 |
231 | end
--------------------------------------------------------------------------------
/lib/active_column/tasks/column_family.rb:
--------------------------------------------------------------------------------
1 | module ActiveColumn
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 | :lexical_uuid => 'LexicalUUIDType'}
13 |
14 | COLUMN_TYPES = { :super => 'Super',
15 | :standard => 'Standard' }
16 |
17 | def initialize(keyspace)
18 | raise 'Cannot operate on system keyspace' if keyspace == 'system'
19 | @keyspace = keyspace
20 | end
21 |
22 | def exists?(name)
23 | ! find_by_name(name).nil?
24 | end
25 |
26 | def create(name, &block)
27 | cf = Cassandra::ColumnFamily.new
28 | cf.name = name.to_s
29 | cf.keyspace = @keyspace.to_s
30 | cf.comparator_type = 'TimeUUIDType'
31 |
32 | block.call cf if block
33 |
34 | post_process_column_family(cf)
35 | connection.add_column_family(cf)
36 | end
37 |
38 | def update(name, &block)
39 | cf = find_by_name(name)
40 | raise "Can not find column family #{name}" if cf.nil?
41 |
42 | block.call cf if block
43 |
44 | post_process_column_family(cf)
45 | connection.update_column_family(cf)
46 | end
47 |
48 | def drop(name)
49 | connection.drop_column_family(name.to_s)
50 | end
51 |
52 | def rename(old_name, new_name)
53 | connection.rename_column_family(old_name.to_s, new_name.to_s)
54 | end
55 |
56 | def clear(name)
57 | connection.truncate!(name.to_s)
58 | end
59 |
60 | private
61 |
62 | def connection
63 | ActiveColumn.connection
64 | end
65 |
66 | def find_by_name(name)
67 | connection.schema.cf_defs.find { |cf_def| cf_def.name == name.to_s }
68 | end
69 |
70 | def post_process_column_family(cf)
71 | type = cf.comparator_type
72 | if type && COMPARATOR_TYPES.has_key?(type)
73 | cf.comparator_type = COMPARATOR_TYPES[type]
74 | end
75 |
76 | subtype = cf.subcomparator_type
77 | if subtype && COMPARATOR_TYPES.has_key?(subtype)
78 | cf.subcomparator_type = COMPARATOR_TYPES[subtype]
79 | end
80 |
81 | column_type = cf.column_type.to_s.downcase.to_sym
82 | if COLUMN_TYPES.has_key?(column_type)
83 | cf.column_type = COLUMN_TYPES[column_type]
84 | else
85 | raise ArgumentError, "Unrecognized column_type #{column_type}"
86 | end
87 |
88 | cf
89 | end
90 |
91 | end
92 |
93 | end
94 |
95 | end
96 |
97 | class Cassandra
98 | class ColumnFamily
99 | def with_fields(options)
100 | struct_fields.collect { |f| f[1][:name] }.each do |f|
101 | send("#{f}=", options[f.to_sym] || options[f.to_s])
102 | end
103 | self
104 | end
105 | end
106 | end
107 |
108 |
--------------------------------------------------------------------------------
/lib/active_column/tasks/keyspace.rb:
--------------------------------------------------------------------------------
1 | module ActiveColumn
2 |
3 | module Tasks
4 |
5 | class Keyspace
6 | include ActiveColumn::Helpers
7 |
8 | def self.parse(hash)
9 | ks = Cassandra::Keyspace.new.with_fields hash
10 | ks.cf_defs = []
11 | hash['cf_defs'].each do |cf|
12 | ks.cf_defs << Cassandra::ColumnFamily.new.with_fields(cf)
13 | end
14 | ks
15 | end
16 |
17 | def initialize
18 | c = ActiveColumn.connection
19 | @cassandra = Cassandra.new('system', c.servers, c.thrift_client_options)
20 | end
21 |
22 | def exists?(name)
23 | @cassandra.keyspaces.include? name.to_s
24 | end
25 |
26 | def create(name, options = {})
27 | if exists? name
28 | log "Keyspace '#{name}' already exists - cannot create"
29 | return nil
30 | end
31 |
32 | opts = { :name => name.to_s,
33 | :strategy_class => 'org.apache.cassandra.locator.SimpleStrategy',
34 | :replication_factor => 1,
35 | :cf_defs => [] }.merge(options)
36 |
37 | ks = Cassandra::Keyspace.new.with_fields(opts)
38 | @cassandra.add_keyspace ks
39 | ks
40 | end
41 |
42 | def drop(name)
43 | return log 'Cannot drop system keyspace' if name == 'system'
44 | return log "Keyspace '#{name}' does not exist - cannot drop" if !exists? name
45 | @cassandra.drop_keyspace name.to_s
46 | true
47 | end
48 |
49 | def set(name)
50 | return log "Keyspace '#{name}' does not exist - cannot set" if !exists? name
51 | @cassandra.keyspace = name.to_s
52 | end
53 |
54 | def get
55 | @cassandra.keyspace
56 | end
57 |
58 | def clear
59 | return log 'Cannot clear system keyspace' if @cassandra.keyspace == 'system'
60 | @cassandra.clear_keyspace!
61 | end
62 |
63 | def schema_dump
64 | @cassandra.schema
65 | end
66 |
67 | def schema_load(schema)
68 | @cassandra.schema.cf_defs.each do |cf|
69 | @cassandra.drop_column_family cf.name
70 | end
71 |
72 | keyspace = get
73 | schema.cf_defs.each do |cf|
74 | cf.keyspace = keyspace
75 | @cassandra.add_column_family cf
76 | end
77 | end
78 |
79 | end
80 |
81 | end
82 |
83 | end
84 |
85 | class Cassandra
86 | class Keyspace
87 | def with_fields(options)
88 | struct_fields.collect { |f| f[1][:name] }.each do |f|
89 | send("#{f}=", options[f.to_sym] || options[f.to_s])
90 | end
91 | self
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/lib/active_column/tasks/ks.rake:
--------------------------------------------------------------------------------
1 | require 'rake'
2 | require 'erb'
3 | require 'active_column/tasks/keyspace'
4 | require 'active_column/tasks/column_family'
5 |
6 | namespace :ks do
7 |
8 | if defined? ::Rails
9 | task :configure do
10 | configure
11 | end
12 | else
13 | task :configure do
14 | configure
15 | end
16 | end
17 |
18 | task :set_keyspace => :configure do
19 | set_keyspace
20 | end
21 |
22 | desc 'Create the keyspace in config/cassandra.yml for the current environment'
23 | task :create => :configure do
24 | ks = ActiveColumn::Tasks::Keyspace.new.create @config['keyspace'], @config
25 | puts "Created keyspace: #{@config['keyspace']}" if ks
26 | end
27 |
28 | namespace :create do
29 | desc 'Create keyspaces in config/cassandra.yml for all environments'
30 | task :all => :configure do
31 | @configs.values.each do |config|
32 | ks = ActiveColumn::Tasks::Keyspace.new.create config['keyspace'], config
33 | puts "Created keyspace: #{config['keyspace']}" if ks
34 | end
35 | end
36 | end
37 |
38 | desc 'Drop keyspace in config/cassandra.yml for the current environment'
39 | task :drop => :configure do
40 | dropped = ActiveColumn::Tasks::Keyspace.new.drop @config['keyspace']
41 | puts "Dropped keyspace: #{@config['keyspace']}" if dropped
42 | end
43 |
44 | namespace :drop do
45 | desc 'Drop keyspaces in config/cassandra.yml for all environments'
46 | task :all => :configure do
47 | @configs.values.each do |config|
48 | dropped = ActiveColumn::Tasks::Keyspace.new.drop config['keyspace']
49 | puts "Dropped keyspace: #{config['keyspace']}" if dropped
50 | end
51 | end
52 | end
53 |
54 | desc 'Migrate the keyspace (options: VERSION=x)'
55 | task :migrate => :set_keyspace do
56 | version = ( ENV['VERSION'] ? ENV['VERSION'].to_i : nil )
57 | ActiveColumn::Migrator.migrate ActiveColumn::Migrator.migrations_path, version
58 | schema_dump
59 | end
60 |
61 | desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n)'
62 | task :rollback => :set_keyspace do
63 | step = ENV['STEP'] ? ENV['STEP'].to_i : 1
64 | ActiveColumn::Migrator.rollback ActiveColumn::Migrator.migrations_path, step
65 | schema_dump
66 | end
67 |
68 | desc 'Pushes the schema to the next version (specify steps w/ STEP=n)'
69 | task :forward => :set_keyspace do
70 | step = ENV['STEP'] ? ENV['STEP'].to_i : 1
71 | ActiveColumn::Migrator.forward ActiveColumn::Migrator.migrations_path, step
72 | schema_dump
73 | end
74 |
75 | namespace :schema do
76 | desc 'Create ks/schema.json file that can be portably used against any Cassandra instance supported by ActiveColumn'
77 | task :dump => :configure do
78 | schema_dump
79 | end
80 |
81 | desc 'Load ks/schema.json file into Cassandra'
82 | task :load => :configure do
83 | schema_load
84 | end
85 | end
86 |
87 | namespace :test do
88 | desc 'Load the development schema in to the test keyspace'
89 | task :prepare => :configure do
90 | schema_dump :development
91 | schema_load :test
92 | end
93 | end
94 |
95 | desc 'Retrieves the current schema version number'
96 | task :version => :set_keyspace do
97 | version = ActiveColumn::Migrator.current_version
98 | puts "Current version: #{version}"
99 | end
100 |
101 | private
102 |
103 | def current_root
104 | return Rails.root.to_s if defined? ::Rails
105 | '.'
106 | end
107 |
108 | def configure
109 | file = "#{current_root}/config/cassandra.yml"
110 | @configs = YAML::load(ERB.new(IO.read(file)).result)
111 | @config = @configs[ActiveColumn::Helpers.current_env]
112 | ActiveColumn.connect @config
113 | end
114 |
115 | def schema_dump(env = ActiveColumn::Helpers.current_env)
116 | ks = set_keyspace env
117 | File.open "#{current_root}/ks/schema.json", 'w' do |file|
118 | basic_json = ks.schema_dump.to_json
119 | formatted_json = JSON.pretty_generate(JSON.parse(basic_json))
120 | file.puts formatted_json
121 | end
122 | end
123 |
124 | def schema_load(env = ActiveColumn::Helpers.current_env)
125 | ks = set_keyspace env
126 | File.open "#{current_root}/ks/schema.json", 'r' do |file|
127 | hash = JSON.parse(file.read(nil))
128 | ks.schema_load ActiveColumn::Tasks::Keyspace.parse(hash)
129 | end
130 | end
131 |
132 | def set_keyspace(env = ActiveColumn::Helpers.current_env)
133 | config = @configs[env.to_s || 'development']
134 | ks = ActiveColumn::Tasks::Keyspace.new
135 | keyspace = config['keyspace']
136 | unless ks.exists? keyspace
137 | puts "Keyspace '#{keyspace}' does not exist - Try 'rake ks:create'"
138 | exit 1
139 | end
140 | ks.set keyspace
141 | ks
142 | end
143 |
144 | end
145 |
--------------------------------------------------------------------------------
/lib/active_column/version.rb:
--------------------------------------------------------------------------------
1 | module ActiveColumn
2 | VERSION = "0.2"
3 | end
4 |
--------------------------------------------------------------------------------
/spec/active_column/base_crud_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveColumn::Base do
4 |
5 | describe '#save' do
6 |
7 | context 'given a model with a single key' do
8 | before do
9 | @counter = Counter.new(:tweets, 'user1', 'user2', 'all')
10 | end
11 |
12 | context 'and an attribute key function' do
13 | before do
14 | Tweet.new( user_id: 'user1', message: 'just woke up' ).save
15 | Tweet.new( user_id: 'user2', message: 'kinda hungry' ).save
16 | end
17 |
18 | it 'saves the model for the key' do
19 | @counter.diff.should == [1, 1, 0]
20 | end
21 | end
22 |
23 | context 'and a custom key function' do
24 | before do
25 | AggregatingTweet.new( user_id: 'user1', message: 'just woke up' ).save
26 | AggregatingTweet.new( user_id: 'user2', message: 'kinda hungry' ).save
27 | end
28 |
29 | it 'saves the model for the keys' do
30 | @counter.diff.should == [1, 1, 2]
31 | end
32 | end
33 | end
34 |
35 | context 'given a model with a compound key' do
36 | before do
37 | @counts = Counter.new(:tweet_dms, 'user1:friend1', 'user1:friend2', 'user1:all', 'all:friend1', 'all:friend2')
38 | TweetDM.new( user_id: 'user1', recipient_ids: ['friend1', 'friend2'], message: 'feeling blue' ).save
39 | TweetDM.new( user_id: 'user1', recipient_ids: ['friend2'], message: 'now im better' ).save
40 | end
41 |
42 | it 'saves the model for the combined compounds keys' do
43 | @counts.diff.should == [1, 2, 2, 1, 2]
44 | end
45 | end
46 |
47 | end
48 |
49 | describe '.generate_keys' do
50 |
51 | context 'given a simple key model' do
52 | before do
53 | @model = SimpleKey.new
54 | end
55 |
56 | context 'and a single key' do
57 | it 'returns an array with the single key' do
58 | keys = @model.class.send :generate_keys, '1'
59 | keys.should == ['1']
60 | end
61 | end
62 |
63 | context 'and an array of keys' do
64 | it 'returns an array with the keys' do
65 | keys = @model.class.send :generate_keys, ['1', '2', '3']
66 | keys.should == ['1', '2', '3']
67 | end
68 | end
69 |
70 | context 'and a map with a single key' do
71 | it 'returns an array with the single key' do
72 | keys = @model.class.send :generate_keys, { :one => '1' }
73 | keys.should == ['1']
74 | end
75 | end
76 |
77 | context 'and a map with an array with a single key' do
78 | it 'returns an array with the single key' do
79 | keys = @model.class.send :generate_keys, { :one => ['1'] }
80 | keys.should == ['1']
81 | end
82 | end
83 | end
84 |
85 | context 'given a compound key model' do
86 | before do
87 | @model = CompoundKey.new
88 | end
89 |
90 | context 'and a map of keys' do
91 | it 'returns an array of the keys put together' do
92 | keys = @model.class.send :generate_keys, { :one => ['1', '2'], :two => ['a', 'b'], :three => 'Z' }
93 | keys.should == ['1:a:Z', '1:b:Z', '2:a:Z', '2:b:Z']
94 | end
95 | end
96 |
97 | context 'and a different map of keys' do
98 | it 'returns an array of the keys put together' do
99 | keys = @model.class.send :generate_keys, { :one => '1', :two => ['a', 'b'], :three => 'Z' }
100 | keys.should == ['1:a:Z', '1:b:Z']
101 | end
102 | end
103 | end
104 |
105 | end
106 |
107 | end
108 |
--------------------------------------------------------------------------------
/spec/active_column/base_finders_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveColumn::Base do
4 |
5 | describe '.find' do
6 |
7 | context 'given a model with a simple key' do
8 | before do
9 | Tweet.new( :user_id => 'user1', :message => 'Going running' ).save
10 | Tweet.new( :user_id => 'user2', :message => 'Watching TV' ).save
11 | Tweet.new( :user_id => 'user1', :message => 'Now im hungry' ).save
12 | Tweet.new( :user_id => 'user1', :message => 'Now im full' ).save
13 | end
14 |
15 | context 'and finding some for a single key' do
16 | before do
17 | @found = Tweet.find( 'user1', :count => 3, :reversed => true )
18 | end
19 |
20 | it 'find all of the models' do
21 | @found.size.should == 1
22 | @found['user1'].size.should == 3
23 | @found['user1'].collect { |t| t.message }.should == [ 'Now im full', 'Now im hungry', 'Going running' ]
24 | end
25 | end
26 |
27 | context 'and finding some for multiple keys' do
28 | before do
29 | @found = Tweet.find( ['user1', 'user2'], :count => 1, :reversed => true )
30 | end
31 |
32 | it 'finds all of the models' do
33 | @found.size.should == 2
34 | @found['user1'].collect { |t| t.message }.should == [ 'Now im full' ]
35 | @found['user2'].collect { |t| t.message }.should == [ 'Watching TV' ]
36 | end
37 | end
38 | end
39 |
40 | context 'given a model with a compound key' do
41 | before do
42 | TweetDM.new( :user_id => 'user1', :recipient_ids => [ 'friend1', 'friend2' ], :message => 'Need to do laundry' ).save
43 | TweetDM.new( :user_id => 'user1', :recipient_ids => [ 'friend2', 'friend3' ], :message => 'My leg itches' ).save
44 | end
45 |
46 | context 'and finding some for both keys' do
47 | before do
48 | @found = TweetDM.find( { :user_id => ['user1', 'user2'], :recipient_id => ['friend1', 'friend2', 'all'] }, :count => 1, :reversed => true )
49 | end
50 |
51 | it 'finds all of the models' do
52 | @found.size.should == 6
53 | @found['user1:friend1'].collect { |t| t.message }.should == [ 'Need to do laundry' ]
54 | @found['user1:friend2'].collect { |t| t.message }.should == [ 'My leg itches' ]
55 | @found['user1:all'].collect { |t| t.message }.should == [ 'My leg itches' ]
56 | @found['user2:friend1'].should == []
57 | @found['user2:friend2'].should == []
58 | @found['user2:all'].should == []
59 | end
60 | end
61 | end
62 |
63 | end
64 |
65 | end
--------------------------------------------------------------------------------
/spec/active_column/migration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'mocha'
3 |
4 | describe ActiveColumn::Migration do
5 | describe '.create_column_family' do
6 |
7 | context 'given a block' do
8 | before do
9 | ActiveColumn.connection.expects(:add_column_family).with() do |cf|
10 | cf.name == 'foo' && cf.comment = 'some comment'
11 | end
12 | end
13 |
14 | it 'sends the settings to cassandra' do
15 | ActiveColumn::Migration.create_column_family :foo do |cf|
16 | cf.comment = 'some comment'
17 | end
18 | end
19 | end
20 |
21 | context 'given no block' do
22 | before do
23 | ActiveColumn.connection.expects(:add_column_family).with() do |cf|
24 | cf.name == 'foo' && cf.comment.nil?
25 | end
26 | end
27 |
28 | it 'sends the default settings to cassandra' do
29 | ActiveColumn::Migration.create_column_family :foo
30 | end
31 | end
32 |
33 | end
34 |
35 | describe '.drop_column_family' do
36 | context 'given a column family' do
37 | before do
38 | ActiveColumn.connection.expects(:drop_column_family).with('foo')
39 | end
40 |
41 | it 'drops it' do
42 | ActiveColumn::Migration.drop_column_family :foo
43 | end
44 | end
45 | end
46 |
47 | describe '.rename_column_family' do
48 | context 'given a column family and a new name' do
49 | before do
50 | ActiveColumn.connection.expects(:rename_column_family).with('old_foo', 'new_foo')
51 | end
52 |
53 | it 'renames it' do
54 | ActiveColumn::Migration.rename_column_family :old_foo, :new_foo
55 | end
56 | end
57 | end
58 |
59 | describe '.update_column_family' do
60 |
61 | context 'given a block' do
62 | before do
63 | ActiveColumn.connection.expects(:update_column_family).with() do |cf|
64 | cf.name == 'tweets' && cf.comment = 'some comment'
65 | end
66 | end
67 |
68 | it 'sends the settings to cassandra' do
69 | ActiveColumn::Migration.update_column_family :tweets do |cf|
70 | cf.comment = 'some comment'
71 | end
72 | end
73 | end
74 |
75 | end
76 |
77 |
78 |
79 |
80 | end
--------------------------------------------------------------------------------
/spec/active_column/migrator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | $migrator_spec_data = {}
4 |
5 | def get_migrations
6 | $cassandra.get(:schema_migrations, 'all').map {|name, _value| name.to_i}
7 | end
8 |
9 | def get_data
10 | $migrator_spec_data.keys.sort
11 | end
12 |
13 | migrations_path = File.expand_path("../../support/migrate/migrator_spec", __FILE__)
14 |
15 | describe ActiveColumn::Migrator do
16 |
17 | after do
18 | $cassandra.truncate!("schema_migrations")
19 | $migrator_spec_data.clear
20 | end
21 |
22 | describe '.migrate' do
23 | context 'given no previous migrations and some pending migrations and no target version' do
24 | before do
25 | ActiveColumn::Migrator.migrate(migrations_path)
26 | end
27 |
28 | it 'runs the migrations' do
29 | assert { get_migrations == [1, 2, 3, 4] }
30 | assert { get_data == [1, 2, 3, 4] }
31 | end
32 | end
33 |
34 | context 'given no previous migrations and some pending migrations and a target version' do
35 | before do
36 | ActiveColumn::Migrator.migrate(migrations_path, 2)
37 | end
38 |
39 | it 'runs the migrations' do
40 | assert { get_migrations == [1, 2] }
41 | assert { get_data == [1, 2] }
42 | end
43 | end
44 |
45 | context 'given no previous migrations and no pending migrations' do
46 | before do
47 | ActiveColumn::Migrator.migrate(File.expand_path("./fake", migrations_path))
48 | end
49 |
50 | it 'runs no migrations' do
51 | assert { get_migrations == [] }
52 | assert { get_data == [] }
53 | end
54 | end
55 |
56 | context 'given some previous migrations and no target version' do
57 | before do
58 | ActiveColumn::Migrator.migrate(migrations_path, 2)
59 | ActiveColumn::Migrator.migrate(migrations_path)
60 | end
61 |
62 | it 'runs the migrations' do
63 | assert { get_migrations == [1, 2, 3, 4]}
64 | assert { get_data == [1, 2, 3, 4]}
65 | end
66 | end
67 |
68 | context 'given some previous migrations and a target version up' do
69 |
70 | before do
71 | ActiveColumn::Migrator.migrate(migrations_path, 2)
72 | ActiveColumn::Migrator.migrate(migrations_path, 3)
73 | end
74 |
75 | it 'runs the migrations' do
76 | assert { get_migrations == [1, 2, 3] }
77 | assert { get_data == [1, 2, 3] }
78 | end
79 | end
80 |
81 | context 'given some previous migrations and a target version down' do
82 | before do
83 | ActiveColumn::Migrator.migrate(migrations_path, 2)
84 | ActiveColumn::Migrator.migrate(migrations_path, 1)
85 | end
86 |
87 | it 'rolls back the migrations' do
88 | assert { get_migrations == [1] }
89 | assert { get_data == [1] }
90 | end
91 | end
92 | end
93 |
94 | describe '.rollback' do
95 | before do
96 | ActiveColumn::Migrator.migrate migrations_path, 3
97 | end
98 |
99 | context 'given no steps' do
100 | before do
101 | ActiveColumn::Migrator.rollback migrations_path
102 | end
103 |
104 | it 'rolls back one step' do
105 | assert { get_migrations == [1, 2] }
106 | assert { get_data == [1, 2] }
107 | end
108 | end
109 |
110 | context 'given steps = 2' do
111 | before do
112 | ActiveColumn::Migrator.rollback migrations_path, 2
113 | end
114 |
115 | it 'rolls back two steps' do
116 | assert { get_migrations == [1] }
117 | assert { get_data == [1] }
118 | end
119 | end
120 | end
121 |
122 | describe '.forward' do
123 | before do
124 | ActiveColumn::Migrator.migrate migrations_path, 1
125 | end
126 |
127 | context 'given no steps' do
128 | before do
129 | ActiveColumn::Migrator.forward migrations_path
130 | end
131 |
132 | it 'migrates one step' do
133 | assert { get_migrations == [1, 2] }
134 | assert { get_data == [1, 2] }
135 | end
136 | end
137 |
138 | context 'given steps = 2' do
139 | before do
140 | ActiveColumn::Migrator.forward migrations_path, 2
141 | end
142 |
143 | it 'migrates two steps' do
144 | assert { get_migrations == [1, 2, 3] }
145 | assert { get_data == [1, 2, 3] }
146 | end
147 | end
148 | end
149 |
150 | end
--------------------------------------------------------------------------------
/spec/active_column/tasks/column_family_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveColumn::Tasks::ColumnFamily do
4 |
5 | describe '.post_process_options' do
6 | context 'given a time-based comparator_type' do
7 | it 'sets TimeUUIDType' do
8 | assert { translated_comparator(:time) == 'TimeUUIDType' }
9 | assert { translated_subcomparator(:time) == 'TimeUUIDType' }
10 | assert { translated_comparator(:timestamp) == 'TimeUUIDType' }
11 | assert { translated_subcomparator(:timestamp) == 'TimeUUIDType' }
12 | assert { translated_comparator('TimeUUIDType') == 'TimeUUIDType' }
13 | assert { translated_subcomparator('TimeUUIDType') == 'TimeUUIDType' }
14 | end
15 | end
16 |
17 | context 'given a long-based comparator_type' do
18 | it 'sets LongType' do
19 | assert { translated_comparator(:long) == 'LongType' }
20 | assert { translated_subcomparator(:long) == 'LongType' }
21 | assert { translated_comparator('LongType') == 'LongType' }
22 | assert { translated_subcomparator('LongType') == 'LongType' }
23 | end
24 | end
25 |
26 | context 'given a string-based comparator_type' do
27 | it 'sets BytesType' do
28 | assert { translated_comparator(:string) == 'BytesType' }
29 | assert { translated_subcomparator(:string) == 'BytesType' }
30 | assert { translated_comparator('BytesType') == 'BytesType' }
31 | assert { translated_subcomparator('BytesType') == 'BytesType' }
32 | end
33 | end
34 |
35 | context 'given a utf8-based comparator_type' do
36 | it 'sets UTF8Type' do
37 | assert { translated_comparator(:utf8) == 'UTF8Type' }
38 | assert { translated_subcomparator(:utf8) == 'UTF8Type' }
39 | assert { translated_comparator('UTF8Type') == 'UTF8Type' }
40 | assert { translated_subcomparator('UTF8Type') == 'UTF8Type' }
41 | end
42 | end
43 |
44 | context 'given a lexicaluuid-based comparator_type' do
45 | it 'sets LexicalUUIDType' do
46 | assert { translated_comparator(:lexical_uuid) == 'LexicalUUIDType' }
47 | assert { translated_subcomparator(:lexical_uuid) == 'LexicalUUIDType' }
48 | assert { translated_comparator('LexicalUUIDType') == 'LexicalUUIDType' }
49 | assert { translated_subcomparator('LexicalUUIDType') == 'LexicalUUIDType' }
50 | end
51 | end
52 |
53 | context 'given a standard column_type' do
54 | it 'sets Standard' do
55 | assert { translated_column_type(:standard) == 'Standard' }
56 | assert { translated_column_type('Standard') == 'Standard' }
57 | assert { translated_column_type('standard') == 'Standard' }
58 | end
59 | end
60 |
61 | context 'given a super column_type' do
62 | it 'sets Super' do
63 | assert { translated_column_type(:super) == 'Super' }
64 | assert { translated_column_type('Super') == 'Super' }
65 | assert { translated_column_type('super') == 'Super' }
66 | end
67 | end
68 |
69 | context 'given an invalid column type' do
70 | it 'raises an ArgumentError' do
71 | expect do
72 | translated_column_type(:foo)
73 | end.to raise_error(ArgumentError)
74 | end
75 | end
76 | end
77 |
78 | describe '.updating_column_family' do
79 | before do
80 | @cf_tasks = ActiveColumn.column_family_tasks
81 |
82 | if @cf_tasks.exists?(:test_cf)
83 | @cf_tasks.drop(:test_cf)
84 | end
85 |
86 | @cf_tasks.create(:test_cf) do |cf|
87 | cf.comment = "foo"
88 | cf.comparator_type = :long
89 | end
90 | end
91 |
92 | context "given a block of column family updates" do
93 | it "post process given column definitions" do
94 | cf_comparator_type(:test_cf).should == 'org.apache.cassandra.db.marshal.LongType'
95 |
96 | @cf_tasks.update(:test_cf) do |cf|
97 | cf.comparator_type = :long
98 | end
99 | cf_comparator_type(:test_cf).should == 'org.apache.cassandra.db.marshal.LongType'
100 |
101 | end
102 |
103 | it 'updates column family definitions' do
104 | cf_comment(:test_cf).should == "foo"
105 |
106 | @cf_tasks.update(:test_cf) do |cf|
107 | cf.comment = "some new comment"
108 | end
109 |
110 | cf_comment(:test_cf).should == "some new comment"
111 | end
112 | end
113 | end
114 |
115 | end
116 |
117 |
118 | def cf_attr(name, attribute)
119 | cassandra = @cf_tasks.send(:connection)
120 | cf_def = cassandra.schema.cf_defs.select{|cf| cf.name == name.to_s}.first
121 | cf_def.send(attribute)
122 | end
123 |
124 | def cf_comment(name)
125 | cf_attr(name, :comment)
126 | end
127 |
128 | def cf_comparator_type(name)
129 | cf_attr(name, :comparator_type)
130 | end
131 |
132 |
133 | def translated(c, sc, ct)
134 | cf_tasks = ActiveColumn.column_family_tasks
135 | cf = Cassandra::ColumnFamily.new
136 | cf.comparator_type = c
137 | cf.subcomparator_type = sc
138 | cf.column_type = ct
139 | cf_tasks.send(:post_process_column_family, cf)
140 | end
141 |
142 | def translated_comparator(given)
143 | translated(given, nil, :standard).comparator_type
144 | end
145 |
146 | def translated_subcomparator(given)
147 | translated(nil, given, :standard).subcomparator_type
148 | end
149 |
150 | def translated_column_type(given)
151 | translated(nil, nil, given).column_type
152 | end
--------------------------------------------------------------------------------
/spec/active_column/tasks/keyspace_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveColumn::Tasks::Keyspace do
4 |
5 | before do
6 | @ks = ActiveColumn.keyspace_tasks
7 | end
8 |
9 | describe "#create" do
10 | context "given a keyspace" do
11 | before do
12 | @ks.drop :ks_create_test if @ks.exists?(:ks_create_test)
13 | @keyspace = @ks.create :ks_create_test
14 | @no_keyspace = @ks.create :ks_create_test
15 | end
16 |
17 | it "creates the keyspace" do
18 | @ks.exists?(:ks_create_test).should be
19 | end
20 |
21 | it 'returns the keyspace' do
22 | @keyspace.should be
23 | end
24 |
25 | it 'does not create duplicate keyspaces' do
26 | @no_keyspace.should_not be
27 | end
28 |
29 | after do
30 | @ks.drop :ks_create_test
31 | end
32 | end
33 | end
34 |
35 | describe '#drop' do
36 | context 'given a keyspace' do
37 | before do
38 | @ks.create :ks_drop_test unless @ks.exists?(:ks_drop_test)
39 | @ks.drop :ks_drop_test
40 | end
41 |
42 | it 'drops the keyspace' do
43 | @ks.exists?(:ks_drop_test).should_not be
44 | end
45 |
46 | it 'gracefully does not drop the keyspace again' do
47 | @ks.drop :ks_drop_test
48 | end
49 | end
50 | end
51 |
52 | describe '.parse' do
53 | context 'given a keyspace schema as a hash' do
54 | before do
55 | @hash = { 'name' => 'ks1',
56 | 'cf_defs' => [ { 'name' => 'cf1', 'comment' => 'foo' },
57 | { 'name' => 'cf2', 'comment' => 'bar' } ] }
58 | @schema = ActiveColumn::Tasks::Keyspace.parse @hash
59 | @cfdefs = @schema.cf_defs.sort { |a,b| a.name <=> b.name }
60 | end
61 |
62 | it 'returns a keyspace schema' do
63 | @schema.should be_a(Cassandra::Keyspace)
64 | @schema.name.should == 'ks1'
65 | end
66 |
67 | it 'returns all column families' do
68 | @cfdefs.collect(&:name).should == [ 'cf1', 'cf2' ]
69 | @cfdefs.collect(&:comment).should == [ 'foo', 'bar' ]
70 | end
71 | end
72 | end
73 |
74 | describe '#schema_dump' do
75 | context 'given a keyspace' do
76 | before do
77 | @ks.drop :ks_schema_dump_test if @ks.exists?(:ks_schema_dump_test)
78 | @ks.create :ks_schema_dump_test
79 | @ks.set :ks_schema_dump_test
80 | cf_tasks = ActiveColumn::Tasks::ColumnFamily.new :ks_schema_dump_test
81 | cf_tasks.create(:cf1) { |cf| cf.comment = 'foo' }
82 | cf_tasks.create(:cf2) { |cf| cf.comment = 'bar' }
83 | @schema = @ks.schema_dump
84 | @cfdefs = @schema.cf_defs.sort { |a,b| a.name <=> b.name }
85 | end
86 |
87 | it 'dumps the keyspace schema' do
88 | @schema.should be
89 | @schema.name.should == 'ks_schema_dump_test'
90 | end
91 |
92 | it 'dumps all column families' do
93 | @cfdefs.collect(&:name).should == [ 'cf1', 'cf2' ]
94 | @cfdefs.collect(&:comment).should == [ 'foo', 'bar' ]
95 | end
96 |
97 | after do
98 | @ks.drop :ks_schema_dump_test
99 | end
100 | end
101 | end
102 |
103 | describe '#schema_load' do
104 | context 'given a keyspace schema' do
105 | before do
106 | @ks.drop :ks_schema_load_test if @ks.exists?(:ks_schema_load_test)
107 | @ks.create :ks_schema_load_test
108 | @ks.set :ks_schema_load_test
109 | cf_tasks = ActiveColumn::Tasks::ColumnFamily.new :ks_schema_load_test
110 | cf_tasks.create(:cf1) { |cf| cf.comment = 'foo' }
111 | cf_tasks.create(:cf2) { |cf| cf.comment = 'bar' }
112 | schema = @ks.schema_dump
113 |
114 | @ks.drop :ks_schema_load_test2 if @ks.exists?(:ks_schema_load_test2)
115 | @ks.create :ks_schema_load_test2
116 | @ks.set :ks_schema_load_test2
117 | @ks.schema_load schema
118 | @schema2 = @ks.schema_dump
119 | @cfdefs2 = @schema2.cf_defs.sort { |a,b| a.name <=> b.name }
120 | end
121 |
122 | it 'loads the keyspace' do
123 | @schema2.should be
124 | @schema2.name.should == 'ks_schema_load_test2'
125 | end
126 |
127 | it 'loads all column families' do
128 | @cfdefs2.collect(&:name).should == [ 'cf1', 'cf2' ]
129 | @cfdefs2.collect(&:comment).should == [ 'foo', 'bar' ]
130 | end
131 |
132 | after do
133 | @ks.drop :ks_schema_load_test
134 | @ks.drop :ks_schema_load_test2
135 | end
136 | end
137 | end
138 | end
139 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'active_column'
2 | require 'rails'
3 | require 'rspec-rails'
4 | require 'rspec/rails/adapters'
5 | require 'wrong/adapters/rspec'
6 |
7 | ENV['RACK_ENV'] = ENV['RAILS_ENV'] = 'test'
8 | Wrong.config.alias_assert :expect
9 | ActiveColumn::Migration.verbose = false
10 |
11 | Dir[ File.expand_path("../support/**/*.rb", __FILE__) ].each {|f| require f}
12 |
13 | thrift = { :retries => 3, :timeout => 2, :server_retry_period => nil }
14 | $cassandra = ActiveColumn.connection = Cassandra.new('active_column', '127.0.0.1:9160', thrift)
15 |
16 | keyspace = 'active_column'
17 | ks_tasks = ActiveColumn.keyspace_tasks
18 | unless ks_tasks.exists?(keyspace)
19 | ks_tasks.create keyspace
20 |
21 | cf_tasks = ActiveColumn.column_family_tasks
22 | [:tweets, :tweet_dms].each do |cf|
23 | cf_tasks.create cf
24 | end
25 | end
26 |
27 | ks_tasks.set keyspace
28 | ks_tasks.clear
29 |
30 | RSpec.configure do |config|
31 | config.mock_with :mocha
32 | end
33 |
34 | class Counter
35 | def initialize(cf, *keys)
36 | @cf = cf
37 | @keys = keys
38 | @counts = get_counts
39 | end
40 |
41 | def diff()
42 | new_counts = get_counts
43 | @keys.each_with_object( [] ) do |key, counts|
44 | counts << new_counts[key] - @counts[key]
45 | end
46 | end
47 |
48 | private
49 |
50 | def get_counts
51 | @keys.each_with_object( {} ) do |key, counts|
52 | counts[key] = $cassandra.get(@cf, key).length
53 | end
54 | end
55 | end
56 |
57 | require 'rspec/core/formatters/base_formatter'
58 | module RSpec
59 | module Core
60 | module Formatters
61 | class BaseTextFormatter < BaseFormatter
62 | def dump_failure(example, index)
63 | exception = example.execution_result[:exception]
64 | output.puts "#{short_padding}#{index.next}) #{example.full_description}"
65 | output.puts "#{long_padding}#{red("Failure/Error:")} #{red(read_failed_line(exception, example).strip)}"
66 | exception.message.split("\n").each { |line| output.puts "#{long_padding}#{red(line)}" } if exception.message
67 |
68 | example.example_group.ancestors.push(example.example_group).each do |group|
69 | if group.metadata[:shared_group_name]
70 | output.puts "#{long_padding}Shared Example Group: \"#{group.metadata[:shared_group_name]}\" called from " +
71 | "#{backtrace_line(group.metadata[:example_group][:location])}"
72 | break
73 | end
74 | end
75 | end
76 | end
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/spec/support/aggregating_tweet.rb:
--------------------------------------------------------------------------------
1 | class AggregatingTweet < ActiveColumn::Base
2 |
3 | column_family :tweets
4 | key :user_id, :values => :user_keys
5 |
6 | attr_accessor :user_id, :message
7 |
8 | def user_keys
9 | [ user_id, 'all' ]
10 | end
11 |
12 | end
--------------------------------------------------------------------------------
/spec/support/compound_key.rb:
--------------------------------------------------------------------------------
1 | class CompoundKey < ActiveColumn::Base
2 | column_family :time
3 | key :one
4 | key :two
5 | key :three
6 | end
--------------------------------------------------------------------------------
/spec/support/config/storage-conf.xml:
--------------------------------------------------------------------------------
1 |
2 | ActiveColumn
3 | false
4 | true
5 |
6 |
7 |
8 |
9 |
10 |
11 | org.apache.cassandra.locator.RackUnawareStrategy
12 | 1
13 | org.apache.cassandra.locator.EndPointSnitch
14 |
15 |
16 |
17 | org.apache.cassandra.auth.AllowAllAuthenticator
18 | org.apache.cassandra.dht.RandomPartitioner
19 |
20 |
21 | /var/lib/cassandra/saved_caches
22 | /var/lib/cassandra/commitlog
23 |
24 | /var/lib/cassandra/data
25 |
26 |
27 |
28 | 127.0.0.1
29 |
30 |
31 | 10000
32 | 128
33 | localhost
34 | 7000
35 | localhost
36 | 9160
37 | false
38 |
39 | auto
40 | 512
41 | 64
42 | 32
43 | 8
44 | 64
45 | 64
46 | 256
47 | 0.3
48 | 60
49 | 8
50 | 32
51 | periodic
52 | 10000
53 | 864000
54 |
55 |
--------------------------------------------------------------------------------
/spec/support/migrate/migrator_spec/1_migration1.rb:
--------------------------------------------------------------------------------
1 | class Migration1 < ActiveColumn::Migration
2 |
3 | def self.up
4 | $migrator_spec_data[1] = true
5 | end
6 |
7 | def self.down
8 | $migrator_spec_data.delete 1
9 | end
10 |
11 | end
--------------------------------------------------------------------------------
/spec/support/migrate/migrator_spec/2_migration2.rb:
--------------------------------------------------------------------------------
1 | class Migration2 < ActiveColumn::Migration
2 |
3 | def self.up
4 | $migrator_spec_data[2] = true
5 | end
6 |
7 | def self.down
8 | $migrator_spec_data.delete 2
9 | end
10 |
11 | end
--------------------------------------------------------------------------------
/spec/support/migrate/migrator_spec/3_migration3.rb:
--------------------------------------------------------------------------------
1 | class Migration3 < ActiveColumn::Migration
2 |
3 | def self.up
4 | $migrator_spec_data[3] = true
5 | end
6 |
7 | def self.down
8 | $migrator_spec_data.delete 3
9 | end
10 |
11 | end
--------------------------------------------------------------------------------
/spec/support/migrate/migrator_spec/4_migration4.rb:
--------------------------------------------------------------------------------
1 | class Migration4 < ActiveColumn::Migration
2 |
3 | def self.up
4 | $migrator_spec_data[4] = true
5 | end
6 |
7 | def self.down
8 | $migrator_spec_data.delete 4
9 | end
10 |
11 | end
--------------------------------------------------------------------------------
/spec/support/simple_key.rb:
--------------------------------------------------------------------------------
1 | class SimpleKey < ActiveColumn::Base
2 | column_family :time
3 | key :one
4 | end
--------------------------------------------------------------------------------
/spec/support/tweet.rb:
--------------------------------------------------------------------------------
1 | class Tweet
2 | include ActiveColumn
3 |
4 | key :user_id
5 |
6 | attr_accessor :user_id, :message
7 |
8 | end
--------------------------------------------------------------------------------
/spec/support/tweet_dm.rb:
--------------------------------------------------------------------------------
1 | class TweetDM
2 | include ActiveColumn
3 |
4 | key :user_id, :values => :user_keys
5 | key :recipient_id, :values => :recipient_keys
6 |
7 | attr_accessor :user_id, :recipient_ids, :message
8 |
9 | def user_keys
10 | [ user_id, 'all' ]
11 | end
12 |
13 | def recipient_keys
14 | recipient_ids + ['all']
15 | end
16 |
17 | end
--------------------------------------------------------------------------------