├── .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 --------------------------------------------------------------------------------