Skip to content

Repository files navigation

Cequel

Cequel is a Ruby ORM for Cassandra using CQL3.

Code Climate

Cequel::Record is an ActiveRecord-like domain model layer that exposes the robust data modeling capabilities of CQL3, including parent-child relationships via compound primary keys and collection columns.

The lower-level Cequel::Metal layer provides a CQL query builder interface inspired by the excellent Sequel library.

Installation

Add it to your Gemfile:

gem'cequel',github: 'cequel/cequel'

Rails integration

Cequel does not require Rails, but if you are using Rails, you will need version 3.2+. Cequel::Record will read from the configuration file config/cequel.yml if it is present. You can generate a default configuarion file with:

rails g cequel:configuration

Once you've got things configured (or decided to accept the defaults), run this to create your keyspace (database):

rake cequel:keyspace:create

Setting up Models

Unlike in ActiveRecord, models declare their properties inline. We'll start with a simple Blog model:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:textend

Unlike a relational database, Cassandra does not have auto-incrementing primary keys, so you must explicitly set the primary key when you create a new model. For blogs, we use a natural key, which is the subdomain. Another option is to use a UUID.

Compound keys and parent-child relationships

While Cassandra is not a relational database, compound keys do naturally map to parent-child relationships. Cequel supports this explicitly with the has_many and belongs_to relations. Let's create a model for posts that acts as the child of the blog model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:timeuuid,auto: truecolumn:title,:textcolumn:body,:textend

The auto option for the key declaration means Cequel will initialize new records with a UUID already generated. This option is only valid for :uuid and :timeuuid key columns.

Note that the belongs_to declaration must come before the key declaration. This is because belongs_to defines the partition key; the id column is the clustering column.

Practically speaking, this means that posts are accessed using both the blog_subdomain (automatically defined by the belongs_to association) and the id. The most natural way to represent this type of lookup is using a has_many association. Let's add one to Blog:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:texthas_many:postsend

Now we might do something like this:

classPostsController < ActionController::BasedefshowBlog.find(current_subdomain).posts.find(params[:id])endend

Schema synchronization

Cequel will automatically synchronize the schema stored in Cassandra to match the schema you have defined in your models. If you're using Rails, you can synchronize your schemas for everything in app/models by invoking:

rake cequel:migrate

Record sets

Record sets are lazy-loaded collections of records that correspond to a particular CQL query. They behave similarly to ActiveRecord scopes:

Post.select(:id,:title).reverse.limit(10)

To scope a record set to a primary key value, use the [] operator. This will define a scoped value for the first unscoped primary key in the record set:

Post['bigdata']# scopes posts with blog_subdomain="bigdata"

You can pass multiple arguments to the [] operator, which will generate an IN query:

Post['bigdata','nosql']# scopes posts with blog_subdomain IN ("bigdata", "nosql")

To select ranges of data, use before, after, from, upto, and in. Like the [] operator, these methods operate on the first unscoped primary key:

Post['bigdata'].after(last_id)# scopes posts with blog_subdomain="bigdata" and id > last_id

Note that record sets always load records in batches; Cassandra does not support result sets of unbounded size. This process is transparent to you but you'll see multiple queries in your logs if you're iterating over a huge result set.

Time UUID Queries

CQL has special handling for the timeuuid type, which allows you to return a rows whose UUID keys correspond to a range of timestamps.

Cequel automatically constructs timeuuid range queries if you pass a Time value for a range over a timeuuid column. So, if you want to get the posts from the last day, you can run:

Blog['myblog'].posts.from(1.day.ago)

Updating records

When you update an existing record, Cequel will only write statements to the database that correspond to explicit modifications you've made to the record in memory. So, in this situation:

@post=Blog.find(current_subdomain).posts.find(params[:id])@post.update_attributes!(title: "Announcing Cequel 1.0")

Cequel will only update the title column. Note that this is not full dirty tracking; simply setting the title on the record will signal to Cequel that you want to write that attribute to the database, regardless of its previous value.

Unloaded models

In the above example, we call the familiar find method to load a blog and then one of its posts, but we didn't actually do anything with the data in the Blog model; it was simply a convenient object-oriented way to get a handle to the blog's posts. Cequel supports unloaded models via the [] operator; this will return an unloaded blog instance, which knows the value of its primary key, but does not read the row from the database. So, we can refactor the example to be a bit more efficient:

classPostsController < ActionController::Basedefshow@post=Blog[current_subdomain].posts.find(params[:id])endend

If you attempt to access a data attribute on an unloaded class, it will lazy-load the row from the database and become a normal loaded instance.

You can generate a collection of unloaded instances by passing multiple arguments to []:

classBlogsController < ActionController::Basedefrecommended@blogs=Blog['cassandra','nosql']endend

The above will not generate a CQL query, but when you access a property on any of the unloaded Blog instances, Cequel will load data for all of them with a single query. Note that CQL does not allow selecting collection columns when loading multiple records by primary key; only scalar columns will be loaded.

There is another use for unloaded instances: you may set attributes on an unloaded instance and call save without ever actually reading the row from Cassandra. Because Cassandra is optimized for writing data, this "write without reading" pattern gives you maximum efficiency, particularly if you are updating a large number of records.

Collection columns

Cassandra supports three types of collection columns: lists, sets, and maps. Collection columns can be manipulated using atomic collection mutation; e.g., you can add an element to a set without knowing the existing elements. Cequel supports this by exposing collection objects that keep track of their modifications, and which then persist those modifications to Cassandra on save.

Let's add a category set to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textset:categories,:textend

If we were to then update a post like so:

@post=Blog[current_subdomain].posts[params[:id]]@post.categories << 'Kittens'@post.save!

Cequel would send the CQL equivalent of "Add the category 'Kittens' to the post at the given (blog_subdomain, id)", without ever reading the saved value of the categories set.

Secondary indexes

Cassandra supports secondary indexes, although with notable restrictions:

  • Only scalar data columns can be indexed; key columns and collection columns cannot.
  • A secondary index consists of exactly one column.
  • Though you can have more than one secondary index on a table, you can only use one in any given query.

Cequel supports the :index option to add secondary indexes to column definitions:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textcolumn:author_id,:uuid,:index=>trueset:categories,:textend

Defining a column with a secondary index adds several "magic methods" for using the index:

Post.with_author_id(id)# returns a record set scoped to that author_idPost.find_by_author_id(id)# returns the first post with that author_idPost.find_all_by_author_id(id)# returns an array of all posts with that author_id

You can also call the where method directly on record sets:

Post.where(:author_id,id)

Note that where is only for secondary indexed columns; use [] to scope record sets by primary keys.

ActiveModel Support

Cequel supports ActiveModel functionality, such as callbacks, validations, dirty attribute tracking, naming, and serialization. If you're using Rails 3, mass-assignment protection works as usual, and in Rails 4, strong parameters are treated correctly. So we can add some extra ActiveModel goodness to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textvalidates:body,presence: trueafter_save:notify_followersend

Note that validations or callbacks that need to read data attributes will cause unloaded models to load their row during the course of the save operation, so if you are following a write-without-reading pattern, you will need to be careful.

Dirty attribute tracking is only enabled on loaded models.

Upgrading from Cequel 0.x

Cequel 0.x targeted CQL2, which has a substantially different data representation from CQL3. Accordingly, upgrading from Cequel 0.x to Cequel 1.0 requires some changes to your data models.

Upgrading a Cequel::Model

Upgrading from a Cequel::Model class is fairly straightforward; simply add the compact_storage directive to your class definition:

# Model definition in Cequel 0.xclassPostincludeCequel::Modelkey:id,:uuidcolumn:title,:textcolumn:body,:textend# Model definition in Cequel 1.0classPostincludeCequel::Recordkey:id,:uuidcolumn:title,:textcolumn:body,:textcompact_storageend

Note that the semantics of belongs_to and has_many are completely different between Cequel 0.x and Cequel 1.0; if you have data columns that reference keys in other tables, you will need to hand-roll those associations for now.

Upgrading a Cequel::Model::Dictionary

CQL3 does not have a direct "wide row" representation like CQL2, so the Dictionary class does not have a direct analog in Cequel 1.0. Instead, each row key-map key-value tuple in a Dictionary corresponds to a single row in CQL3. Upgrading a Dictionary to Cequel 1.0 involves defining two primary keys and a single data column, again using the compact_storage directive:

# Dictionary definition in Cequel 0.xclassBlogPosts < Cequel::Model::Dictionarykey:blog_id,:uuidmaps:uuid=>:textprivatedefserialize_value(column,value)value.to_jsonenddefdeserialize_value(column,value)JSON.parse(value)endend# Equivalent model in Cequel 1.0classBlogPostincludeCequel::Recordkey:blog_id,:uuidkey:id,:uuidcolumn:data,:textdefdataJSON.parse(read_attribute(:data))enddefdata=(new_data)write_attribute(:data,new_data.to_json)endend

Note that you will want to run ::synchronize_schema on your models when upgrading; this will not change the underlying data structure, but will add some CQL3-specific metadata to the table definition which will allow you to query it.

CQL Gotchas

CQL is designed to be immediately familiar to those of us who are used to working with SQL, which is all of us. Cequel advances this spirit by providing an ActiveRecord-like mapping for CQL. However, Cassandra is very much not a relational database, so some behaviors can come as a surprise. Here's an overview.

Upserts

Perhaps the most surprising fact about CQL is that INSERT and UPDATE are essentially the same thing: both simply persist the given column data at the given key(s). So, you may think you are creating a new record, but in fact you're overwriting data at an existing record:

# I'm just creating a blog here.blog1=Blog.create!(subdomain: 'big-data',name: 'Big Data',description: 'A blog about all things big data')# And another new blog.blog2=Blog.create!(subdomain: 'big-data',name: 'The Big Data Blog')

Living in a relational world, we'd expect the second statement to throw an error because the row with key 'big-data' already exists. But not Cassandra: the above code will just overwrite the name in that row. Note that the description will not be touched by the second statement; upserts only work on the columns that are given.

Compatibility

Rails

  • 4.0
  • 3.2
  • 3.1

Ruby

  • 2.0
  • 1.9.3
  • Rubinius 1.0 in 1.9 mode

Cassandra

  • Cassandra 1.2

Support & Bugs

If you find a bug, feel free to open an issue on GitHub. Pull requests are most welcome.

For questions or feedback, hit up our mailing list at cequel@groups.google.com or find outoftime in the #cassandra IRC channel on Freenode.

Credits

Cequel was written by:

  • Mat Brown
  • Aubrey Holland
  • Keenan Brock
  • Insoo Buzz Jung
  • Louis Simoneau
  • Randy Meech

Special thanks to Brewster, which supported the 0.x releases of Cequel.

License

Cequel is distributed under the MIT license. See the attached LICENSE for all the sordid details.

About

Ruby ORM for Cassandra with CQL3

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - PascalTurbo/cequel: Ruby ORM for Cassandra with CQL3 · GitHub
Skip to content

Repository files navigation

Cequel

Cequel is a Ruby ORM for Cassandra using CQL3.

Code Climate

Cequel::Record is an ActiveRecord-like domain model layer that exposes the robust data modeling capabilities of CQL3, including parent-child relationships via compound primary keys and collection columns.

The lower-level Cequel::Metal layer provides a CQL query builder interface inspired by the excellent Sequel library.

Installation

Add it to your Gemfile:

gem'cequel',github: 'cequel/cequel'

Rails integration

Cequel does not require Rails, but if you are using Rails, you will need version 3.2+. Cequel::Record will read from the configuration file config/cequel.yml if it is present. You can generate a default configuarion file with:

rails g cequel:configuration

Once you've got things configured (or decided to accept the defaults), run this to create your keyspace (database):

rake cequel:keyspace:create

Setting up Models

Unlike in ActiveRecord, models declare their properties inline. We'll start with a simple Blog model:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:textend

Unlike a relational database, Cassandra does not have auto-incrementing primary keys, so you must explicitly set the primary key when you create a new model. For blogs, we use a natural key, which is the subdomain. Another option is to use a UUID.

Compound keys and parent-child relationships

While Cassandra is not a relational database, compound keys do naturally map to parent-child relationships. Cequel supports this explicitly with the has_many and belongs_to relations. Let's create a model for posts that acts as the child of the blog model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:timeuuid,auto: truecolumn:title,:textcolumn:body,:textend

The auto option for the key declaration means Cequel will initialize new records with a UUID already generated. This option is only valid for :uuid and :timeuuid key columns.

Note that the belongs_to declaration must come before the key declaration. This is because belongs_to defines the partition key; the id column is the clustering column.

Practically speaking, this means that posts are accessed using both the blog_subdomain (automatically defined by the belongs_to association) and the id. The most natural way to represent this type of lookup is using a has_many association. Let's add one to Blog:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:texthas_many:postsend

Now we might do something like this:

classPostsController < ActionController::BasedefshowBlog.find(current_subdomain).posts.find(params[:id])endend

Schema synchronization

Cequel will automatically synchronize the schema stored in Cassandra to match the schema you have defined in your models. If you're using Rails, you can synchronize your schemas for everything in app/models by invoking:

rake cequel:migrate

Record sets

Record sets are lazy-loaded collections of records that correspond to a particular CQL query. They behave similarly to ActiveRecord scopes:

Post.select(:id,:title).reverse.limit(10)

To scope a record set to a primary key value, use the [] operator. This will define a scoped value for the first unscoped primary key in the record set:

Post['bigdata']# scopes posts with blog_subdomain="bigdata"

You can pass multiple arguments to the [] operator, which will generate an IN query:

Post['bigdata','nosql']# scopes posts with blog_subdomain IN ("bigdata", "nosql")

To select ranges of data, use before, after, from, upto, and in. Like the [] operator, these methods operate on the first unscoped primary key:

Post['bigdata'].after(last_id)# scopes posts with blog_subdomain="bigdata" and id > last_id

Note that record sets always load records in batches; Cassandra does not support result sets of unbounded size. This process is transparent to you but you'll see multiple queries in your logs if you're iterating over a huge result set.

Time UUID Queries

CQL has special handling for the timeuuid type, which allows you to return a rows whose UUID keys correspond to a range of timestamps.

Cequel automatically constructs timeuuid range queries if you pass a Time value for a range over a timeuuid column. So, if you want to get the posts from the last day, you can run:

Blog['myblog'].posts.from(1.day.ago)

Updating records

When you update an existing record, Cequel will only write statements to the database that correspond to explicit modifications you've made to the record in memory. So, in this situation:

@post=Blog.find(current_subdomain).posts.find(params[:id])@post.update_attributes!(title: "Announcing Cequel 1.0")

Cequel will only update the title column. Note that this is not full dirty tracking; simply setting the title on the record will signal to Cequel that you want to write that attribute to the database, regardless of its previous value.

Unloaded models

In the above example, we call the familiar find method to load a blog and then one of its posts, but we didn't actually do anything with the data in the Blog model; it was simply a convenient object-oriented way to get a handle to the blog's posts. Cequel supports unloaded models via the [] operator; this will return an unloaded blog instance, which knows the value of its primary key, but does not read the row from the database. So, we can refactor the example to be a bit more efficient:

classPostsController < ActionController::Basedefshow@post=Blog[current_subdomain].posts.find(params[:id])endend

If you attempt to access a data attribute on an unloaded class, it will lazy-load the row from the database and become a normal loaded instance.

You can generate a collection of unloaded instances by passing multiple arguments to []:

classBlogsController < ActionController::Basedefrecommended@blogs=Blog['cassandra','nosql']endend

The above will not generate a CQL query, but when you access a property on any of the unloaded Blog instances, Cequel will load data for all of them with a single query. Note that CQL does not allow selecting collection columns when loading multiple records by primary key; only scalar columns will be loaded.

There is another use for unloaded instances: you may set attributes on an unloaded instance and call save without ever actually reading the row from Cassandra. Because Cassandra is optimized for writing data, this "write without reading" pattern gives you maximum efficiency, particularly if you are updating a large number of records.

Collection columns

Cassandra supports three types of collection columns: lists, sets, and maps. Collection columns can be manipulated using atomic collection mutation; e.g., you can add an element to a set without knowing the existing elements. Cequel supports this by exposing collection objects that keep track of their modifications, and which then persist those modifications to Cassandra on save.

Let's add a category set to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textset:categories,:textend

If we were to then update a post like so:

@post=Blog[current_subdomain].posts[params[:id]]@post.categories << 'Kittens'@post.save!

Cequel would send the CQL equivalent of "Add the category 'Kittens' to the post at the given (blog_subdomain, id)", without ever reading the saved value of the categories set.

Secondary indexes

Cassandra supports secondary indexes, although with notable restrictions:

  • Only scalar data columns can be indexed; key columns and collection columns cannot.
  • A secondary index consists of exactly one column.
  • Though you can have more than one secondary index on a table, you can only use one in any given query.

Cequel supports the :index option to add secondary indexes to column definitions:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textcolumn:author_id,:uuid,:index=>trueset:categories,:textend

Defining a column with a secondary index adds several "magic methods" for using the index:

Post.with_author_id(id)# returns a record set scoped to that author_idPost.find_by_author_id(id)# returns the first post with that author_idPost.find_all_by_author_id(id)# returns an array of all posts with that author_id

You can also call the where method directly on record sets:

Post.where(:author_id,id)

Note that where is only for secondary indexed columns; use [] to scope record sets by primary keys.

ActiveModel Support

Cequel supports ActiveModel functionality, such as callbacks, validations, dirty attribute tracking, naming, and serialization. If you're using Rails 3, mass-assignment protection works as usual, and in Rails 4, strong parameters are treated correctly. So we can add some extra ActiveModel goodness to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textvalidates:body,presence: trueafter_save:notify_followersend

Note that validations or callbacks that need to read data attributes will cause unloaded models to load their row during the course of the save operation, so if you are following a write-without-reading pattern, you will need to be careful.

Dirty attribute tracking is only enabled on loaded models.

Upgrading from Cequel 0.x

Cequel 0.x targeted CQL2, which has a substantially different data representation from CQL3. Accordingly, upgrading from Cequel 0.x to Cequel 1.0 requires some changes to your data models.

Upgrading a Cequel::Model

Upgrading from a Cequel::Model class is fairly straightforward; simply add the compact_storage directive to your class definition:

# Model definition in Cequel 0.xclassPostincludeCequel::Modelkey:id,:uuidcolumn:title,:textcolumn:body,:textend# Model definition in Cequel 1.0classPostincludeCequel::Recordkey:id,:uuidcolumn:title,:textcolumn:body,:textcompact_storageend

Note that the semantics of belongs_to and has_many are completely different between Cequel 0.x and Cequel 1.0; if you have data columns that reference keys in other tables, you will need to hand-roll those associations for now.

Upgrading a Cequel::Model::Dictionary

CQL3 does not have a direct "wide row" representation like CQL2, so the Dictionary class does not have a direct analog in Cequel 1.0. Instead, each row key-map key-value tuple in a Dictionary corresponds to a single row in CQL3. Upgrading a Dictionary to Cequel 1.0 involves defining two primary keys and a single data column, again using the compact_storage directive:

# Dictionary definition in Cequel 0.xclassBlogPosts < Cequel::Model::Dictionarykey:blog_id,:uuidmaps:uuid=>:textprivatedefserialize_value(column,value)value.to_jsonenddefdeserialize_value(column,value)JSON.parse(value)endend# Equivalent model in Cequel 1.0classBlogPostincludeCequel::Recordkey:blog_id,:uuidkey:id,:uuidcolumn:data,:textdefdataJSON.parse(read_attribute(:data))enddefdata=(new_data)write_attribute(:data,new_data.to_json)endend

Note that you will want to run ::synchronize_schema on your models when upgrading; this will not change the underlying data structure, but will add some CQL3-specific metadata to the table definition which will allow you to query it.

CQL Gotchas

CQL is designed to be immediately familiar to those of us who are used to working with SQL, which is all of us. Cequel advances this spirit by providing an ActiveRecord-like mapping for CQL. However, Cassandra is very much not a relational database, so some behaviors can come as a surprise. Here's an overview.

Upserts

Perhaps the most surprising fact about CQL is that INSERT and UPDATE are essentially the same thing: both simply persist the given column data at the given key(s). So, you may think you are creating a new record, but in fact you're overwriting data at an existing record:

# I'm just creating a blog here.blog1=Blog.create!(subdomain: 'big-data',name: 'Big Data',description: 'A blog about all things big data')# And another new blog.blog2=Blog.create!(subdomain: 'big-data',name: 'The Big Data Blog')

Living in a relational world, we'd expect the second statement to throw an error because the row with key 'big-data' already exists. But not Cassandra: the above code will just overwrite the name in that row. Note that the description will not be touched by the second statement; upserts only work on the columns that are given.

Compatibility

Rails

  • 4.0
  • 3.2
  • 3.1

Ruby

  • 2.0
  • 1.9.3
  • Rubinius 1.0 in 1.9 mode

Cassandra

  • Cassandra 1.2

Support & Bugs

If you find a bug, feel free to open an issue on GitHub. Pull requests are most welcome.

For questions or feedback, hit up our mailing list at cequel@groups.google.com or find outoftime in the #cassandra IRC channel on Freenode.

Credits

Cequel was written by:

  • Mat Brown
  • Aubrey Holland
  • Keenan Brock
  • Insoo Buzz Jung
  • Louis Simoneau
  • Randy Meech

Special thanks to Brewster, which supported the 0.x releases of Cequel.

License

Cequel is distributed under the MIT license. See the attached LICENSE for all the sordid details.

About

Ruby ORM for Cassandra with CQL3

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - PascalTurbo/cequel: Ruby ORM for Cassandra with CQL3 · GitHub
Skip to content

Repository files navigation

Cequel

Cequel is a Ruby ORM for Cassandra using CQL3.

Code Climate

Cequel::Record is an ActiveRecord-like domain model layer that exposes the robust data modeling capabilities of CQL3, including parent-child relationships via compound primary keys and collection columns.

The lower-level Cequel::Metal layer provides a CQL query builder interface inspired by the excellent Sequel library.

Installation

Add it to your Gemfile:

gem'cequel',github: 'cequel/cequel'

Rails integration

Cequel does not require Rails, but if you are using Rails, you will need version 3.2+. Cequel::Record will read from the configuration file config/cequel.yml if it is present. You can generate a default configuarion file with:

rails g cequel:configuration

Once you've got things configured (or decided to accept the defaults), run this to create your keyspace (database):

rake cequel:keyspace:create

Setting up Models

Unlike in ActiveRecord, models declare their properties inline. We'll start with a simple Blog model:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:textend

Unlike a relational database, Cassandra does not have auto-incrementing primary keys, so you must explicitly set the primary key when you create a new model. For blogs, we use a natural key, which is the subdomain. Another option is to use a UUID.

Compound keys and parent-child relationships

While Cassandra is not a relational database, compound keys do naturally map to parent-child relationships. Cequel supports this explicitly with the has_many and belongs_to relations. Let's create a model for posts that acts as the child of the blog model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:timeuuid,auto: truecolumn:title,:textcolumn:body,:textend

The auto option for the key declaration means Cequel will initialize new records with a UUID already generated. This option is only valid for :uuid and :timeuuid key columns.

Note that the belongs_to declaration must come before the key declaration. This is because belongs_to defines the partition key; the id column is the clustering column.

Practically speaking, this means that posts are accessed using both the blog_subdomain (automatically defined by the belongs_to association) and the id. The most natural way to represent this type of lookup is using a has_many association. Let's add one to Blog:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:texthas_many:postsend

Now we might do something like this:

classPostsController < ActionController::BasedefshowBlog.find(current_subdomain).posts.find(params[:id])endend

Schema synchronization

Cequel will automatically synchronize the schema stored in Cassandra to match the schema you have defined in your models. If you're using Rails, you can synchronize your schemas for everything in app/models by invoking:

rake cequel:migrate

Record sets

Record sets are lazy-loaded collections of records that correspond to a particular CQL query. They behave similarly to ActiveRecord scopes:

Post.select(:id,:title).reverse.limit(10)

To scope a record set to a primary key value, use the [] operator. This will define a scoped value for the first unscoped primary key in the record set:

Post['bigdata']# scopes posts with blog_subdomain="bigdata"

You can pass multiple arguments to the [] operator, which will generate an IN query:

Post['bigdata','nosql']# scopes posts with blog_subdomain IN ("bigdata", "nosql")

To select ranges of data, use before, after, from, upto, and in. Like the [] operator, these methods operate on the first unscoped primary key:

Post['bigdata'].after(last_id)# scopes posts with blog_subdomain="bigdata" and id > last_id

Note that record sets always load records in batches; Cassandra does not support result sets of unbounded size. This process is transparent to you but you'll see multiple queries in your logs if you're iterating over a huge result set.

Time UUID Queries

CQL has special handling for the timeuuid type, which allows you to return a rows whose UUID keys correspond to a range of timestamps.

Cequel automatically constructs timeuuid range queries if you pass a Time value for a range over a timeuuid column. So, if you want to get the posts from the last day, you can run:

Blog['myblog'].posts.from(1.day.ago)

Updating records

When you update an existing record, Cequel will only write statements to the database that correspond to explicit modifications you've made to the record in memory. So, in this situation:

@post=Blog.find(current_subdomain).posts.find(params[:id])@post.update_attributes!(title: "Announcing Cequel 1.0")

Cequel will only update the title column. Note that this is not full dirty tracking; simply setting the title on the record will signal to Cequel that you want to write that attribute to the database, regardless of its previous value.

Unloaded models

In the above example, we call the familiar find method to load a blog and then one of its posts, but we didn't actually do anything with the data in the Blog model; it was simply a convenient object-oriented way to get a handle to the blog's posts. Cequel supports unloaded models via the [] operator; this will return an unloaded blog instance, which knows the value of its primary key, but does not read the row from the database. So, we can refactor the example to be a bit more efficient:

classPostsController < ActionController::Basedefshow@post=Blog[current_subdomain].posts.find(params[:id])endend

If you attempt to access a data attribute on an unloaded class, it will lazy-load the row from the database and become a normal loaded instance.

You can generate a collection of unloaded instances by passing multiple arguments to []:

classBlogsController < ActionController::Basedefrecommended@blogs=Blog['cassandra','nosql']endend

The above will not generate a CQL query, but when you access a property on any of the unloaded Blog instances, Cequel will load data for all of them with a single query. Note that CQL does not allow selecting collection columns when loading multiple records by primary key; only scalar columns will be loaded.

There is another use for unloaded instances: you may set attributes on an unloaded instance and call save without ever actually reading the row from Cassandra. Because Cassandra is optimized for writing data, this "write without reading" pattern gives you maximum efficiency, particularly if you are updating a large number of records.

Collection columns

Cassandra supports three types of collection columns: lists, sets, and maps. Collection columns can be manipulated using atomic collection mutation; e.g., you can add an element to a set without knowing the existing elements. Cequel supports this by exposing collection objects that keep track of their modifications, and which then persist those modifications to Cassandra on save.

Let's add a category set to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textset:categories,:textend

If we were to then update a post like so:

@post=Blog[current_subdomain].posts[params[:id]]@post.categories << 'Kittens'@post.save!

Cequel would send the CQL equivalent of "Add the category 'Kittens' to the post at the given (blog_subdomain, id)", without ever reading the saved value of the categories set.

Secondary indexes

Cassandra supports secondary indexes, although with notable restrictions:

  • Only scalar data columns can be indexed; key columns and collection columns cannot.
  • A secondary index consists of exactly one column.
  • Though you can have more than one secondary index on a table, you can only use one in any given query.

Cequel supports the :index option to add secondary indexes to column definitions:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textcolumn:author_id,:uuid,:index=>trueset:categories,:textend

Defining a column with a secondary index adds several "magic methods" for using the index:

Post.with_author_id(id)# returns a record set scoped to that author_idPost.find_by_author_id(id)# returns the first post with that author_idPost.find_all_by_author_id(id)# returns an array of all posts with that author_id

You can also call the where method directly on record sets:

Post.where(:author_id,id)

Note that where is only for secondary indexed columns; use [] to scope record sets by primary keys.

ActiveModel Support

Cequel supports ActiveModel functionality, such as callbacks, validations, dirty attribute tracking, naming, and serialization. If you're using Rails 3, mass-assignment protection works as usual, and in Rails 4, strong parameters are treated correctly. So we can add some extra ActiveModel goodness to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textvalidates:body,presence: trueafter_save:notify_followersend

Note that validations or callbacks that need to read data attributes will cause unloaded models to load their row during the course of the save operation, so if you are following a write-without-reading pattern, you will need to be careful.

Dirty attribute tracking is only enabled on loaded models.

Upgrading from Cequel 0.x

Cequel 0.x targeted CQL2, which has a substantially different data representation from CQL3. Accordingly, upgrading from Cequel 0.x to Cequel 1.0 requires some changes to your data models.

Upgrading a Cequel::Model

Upgrading from a Cequel::Model class is fairly straightforward; simply add the compact_storage directive to your class definition:

# Model definition in Cequel 0.xclassPostincludeCequel::Modelkey:id,:uuidcolumn:title,:textcolumn:body,:textend# Model definition in Cequel 1.0classPostincludeCequel::Recordkey:id,:uuidcolumn:title,:textcolumn:body,:textcompact_storageend

Note that the semantics of belongs_to and has_many are completely different between Cequel 0.x and Cequel 1.0; if you have data columns that reference keys in other tables, you will need to hand-roll those associations for now.

Upgrading a Cequel::Model::Dictionary

CQL3 does not have a direct "wide row" representation like CQL2, so the Dictionary class does not have a direct analog in Cequel 1.0. Instead, each row key-map key-value tuple in a Dictionary corresponds to a single row in CQL3. Upgrading a Dictionary to Cequel 1.0 involves defining two primary keys and a single data column, again using the compact_storage directive:

# Dictionary definition in Cequel 0.xclassBlogPosts < Cequel::Model::Dictionarykey:blog_id,:uuidmaps:uuid=>:textprivatedefserialize_value(column,value)value.to_jsonenddefdeserialize_value(column,value)JSON.parse(value)endend# Equivalent model in Cequel 1.0classBlogPostincludeCequel::Recordkey:blog_id,:uuidkey:id,:uuidcolumn:data,:textdefdataJSON.parse(read_attribute(:data))enddefdata=(new_data)write_attribute(:data,new_data.to_json)endend

Note that you will want to run ::synchronize_schema on your models when upgrading; this will not change the underlying data structure, but will add some CQL3-specific metadata to the table definition which will allow you to query it.

CQL Gotchas

CQL is designed to be immediately familiar to those of us who are used to working with SQL, which is all of us. Cequel advances this spirit by providing an ActiveRecord-like mapping for CQL. However, Cassandra is very much not a relational database, so some behaviors can come as a surprise. Here's an overview.

Upserts

Perhaps the most surprising fact about CQL is that INSERT and UPDATE are essentially the same thing: both simply persist the given column data at the given key(s). So, you may think you are creating a new record, but in fact you're overwriting data at an existing record:

# I'm just creating a blog here.blog1=Blog.create!(subdomain: 'big-data',name: 'Big Data',description: 'A blog about all things big data')# And another new blog.blog2=Blog.create!(subdomain: 'big-data',name: 'The Big Data Blog')

Living in a relational world, we'd expect the second statement to throw an error because the row with key 'big-data' already exists. But not Cassandra: the above code will just overwrite the name in that row. Note that the description will not be touched by the second statement; upserts only work on the columns that are given.

Compatibility

Rails

  • 4.0
  • 3.2
  • 3.1

Ruby

  • 2.0
  • 1.9.3
  • Rubinius 1.0 in 1.9 mode

Cassandra

  • Cassandra 1.2

Support & Bugs

If you find a bug, feel free to open an issue on GitHub. Pull requests are most welcome.

For questions or feedback, hit up our mailing list at cequel@groups.google.com or find outoftime in the #cassandra IRC channel on Freenode.

Credits

Cequel was written by:

  • Mat Brown
  • Aubrey Holland
  • Keenan Brock
  • Insoo Buzz Jung
  • Louis Simoneau
  • Randy Meech

Special thanks to Brewster, which supported the 0.x releases of Cequel.

License

Cequel is distributed under the MIT license. See the attached LICENSE for all the sordid details.

About

Ruby ORM for Cassandra with CQL3

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - PascalTurbo/cequel: Ruby ORM for Cassandra with CQL3 · GitHub
Skip to content

Repository files navigation

Cequel

Cequel is a Ruby ORM for Cassandra using CQL3.

Code Climate

Cequel::Record is an ActiveRecord-like domain model layer that exposes the robust data modeling capabilities of CQL3, including parent-child relationships via compound primary keys and collection columns.

The lower-level Cequel::Metal layer provides a CQL query builder interface inspired by the excellent Sequel library.

Installation

Add it to your Gemfile:

gem'cequel',github: 'cequel/cequel'

Rails integration

Cequel does not require Rails, but if you are using Rails, you will need version 3.2+. Cequel::Record will read from the configuration file config/cequel.yml if it is present. You can generate a default configuarion file with:

rails g cequel:configuration

Once you've got things configured (or decided to accept the defaults), run this to create your keyspace (database):

rake cequel:keyspace:create

Setting up Models

Unlike in ActiveRecord, models declare their properties inline. We'll start with a simple Blog model:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:textend

Unlike a relational database, Cassandra does not have auto-incrementing primary keys, so you must explicitly set the primary key when you create a new model. For blogs, we use a natural key, which is the subdomain. Another option is to use a UUID.

Compound keys and parent-child relationships

While Cassandra is not a relational database, compound keys do naturally map to parent-child relationships. Cequel supports this explicitly with the has_many and belongs_to relations. Let's create a model for posts that acts as the child of the blog model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:timeuuid,auto: truecolumn:title,:textcolumn:body,:textend

The auto option for the key declaration means Cequel will initialize new records with a UUID already generated. This option is only valid for :uuid and :timeuuid key columns.

Note that the belongs_to declaration must come before the key declaration. This is because belongs_to defines the partition key; the id column is the clustering column.

Practically speaking, this means that posts are accessed using both the blog_subdomain (automatically defined by the belongs_to association) and the id. The most natural way to represent this type of lookup is using a has_many association. Let's add one to Blog:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:texthas_many:postsend

Now we might do something like this:

classPostsController < ActionController::BasedefshowBlog.find(current_subdomain).posts.find(params[:id])endend

Schema synchronization

Cequel will automatically synchronize the schema stored in Cassandra to match the schema you have defined in your models. If you're using Rails, you can synchronize your schemas for everything in app/models by invoking:

rake cequel:migrate

Record sets

Record sets are lazy-loaded collections of records that correspond to a particular CQL query. They behave similarly to ActiveRecord scopes:

Post.select(:id,:title).reverse.limit(10)

To scope a record set to a primary key value, use the [] operator. This will define a scoped value for the first unscoped primary key in the record set:

Post['bigdata']# scopes posts with blog_subdomain="bigdata"

You can pass multiple arguments to the [] operator, which will generate an IN query:

Post['bigdata','nosql']# scopes posts with blog_subdomain IN ("bigdata", "nosql")

To select ranges of data, use before, after, from, upto, and in. Like the [] operator, these methods operate on the first unscoped primary key:

Post['bigdata'].after(last_id)# scopes posts with blog_subdomain="bigdata" and id > last_id

Note that record sets always load records in batches; Cassandra does not support result sets of unbounded size. This process is transparent to you but you'll see multiple queries in your logs if you're iterating over a huge result set.

Time UUID Queries

CQL has special handling for the timeuuid type, which allows you to return a rows whose UUID keys correspond to a range of timestamps.

Cequel automatically constructs timeuuid range queries if you pass a Time value for a range over a timeuuid column. So, if you want to get the posts from the last day, you can run:

Blog['myblog'].posts.from(1.day.ago)

Updating records

When you update an existing record, Cequel will only write statements to the database that correspond to explicit modifications you've made to the record in memory. So, in this situation:

@post=Blog.find(current_subdomain).posts.find(params[:id])@post.update_attributes!(title: "Announcing Cequel 1.0")

Cequel will only update the title column. Note that this is not full dirty tracking; simply setting the title on the record will signal to Cequel that you want to write that attribute to the database, regardless of its previous value.

Unloaded models

In the above example, we call the familiar find method to load a blog and then one of its posts, but we didn't actually do anything with the data in the Blog model; it was simply a convenient object-oriented way to get a handle to the blog's posts. Cequel supports unloaded models via the [] operator; this will return an unloaded blog instance, which knows the value of its primary key, but does not read the row from the database. So, we can refactor the example to be a bit more efficient:

classPostsController < ActionController::Basedefshow@post=Blog[current_subdomain].posts.find(params[:id])endend

If you attempt to access a data attribute on an unloaded class, it will lazy-load the row from the database and become a normal loaded instance.

You can generate a collection of unloaded instances by passing multiple arguments to []:

classBlogsController < ActionController::Basedefrecommended@blogs=Blog['cassandra','nosql']endend

The above will not generate a CQL query, but when you access a property on any of the unloaded Blog instances, Cequel will load data for all of them with a single query. Note that CQL does not allow selecting collection columns when loading multiple records by primary key; only scalar columns will be loaded.

There is another use for unloaded instances: you may set attributes on an unloaded instance and call save without ever actually reading the row from Cassandra. Because Cassandra is optimized for writing data, this "write without reading" pattern gives you maximum efficiency, particularly if you are updating a large number of records.

Collection columns

Cassandra supports three types of collection columns: lists, sets, and maps. Collection columns can be manipulated using atomic collection mutation; e.g., you can add an element to a set without knowing the existing elements. Cequel supports this by exposing collection objects that keep track of their modifications, and which then persist those modifications to Cassandra on save.

Let's add a category set to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textset:categories,:textend

If we were to then update a post like so:

@post=Blog[current_subdomain].posts[params[:id]]@post.categories << 'Kittens'@post.save!

Cequel would send the CQL equivalent of "Add the category 'Kittens' to the post at the given (blog_subdomain, id)", without ever reading the saved value of the categories set.

Secondary indexes

Cassandra supports secondary indexes, although with notable restrictions:

  • Only scalar data columns can be indexed; key columns and collection columns cannot.
  • A secondary index consists of exactly one column.
  • Though you can have more than one secondary index on a table, you can only use one in any given query.

Cequel supports the :index option to add secondary indexes to column definitions:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textcolumn:author_id,:uuid,:index=>trueset:categories,:textend

Defining a column with a secondary index adds several "magic methods" for using the index:

Post.with_author_id(id)# returns a record set scoped to that author_idPost.find_by_author_id(id)# returns the first post with that author_idPost.find_all_by_author_id(id)# returns an array of all posts with that author_id

You can also call the where method directly on record sets:

Post.where(:author_id,id)

Note that where is only for secondary indexed columns; use [] to scope record sets by primary keys.

ActiveModel Support

Cequel supports ActiveModel functionality, such as callbacks, validations, dirty attribute tracking, naming, and serialization. If you're using Rails 3, mass-assignment protection works as usual, and in Rails 4, strong parameters are treated correctly. So we can add some extra ActiveModel goodness to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textvalidates:body,presence: trueafter_save:notify_followersend

Note that validations or callbacks that need to read data attributes will cause unloaded models to load their row during the course of the save operation, so if you are following a write-without-reading pattern, you will need to be careful.

Dirty attribute tracking is only enabled on loaded models.

Upgrading from Cequel 0.x

Cequel 0.x targeted CQL2, which has a substantially different data representation from CQL3. Accordingly, upgrading from Cequel 0.x to Cequel 1.0 requires some changes to your data models.

Upgrading a Cequel::Model

Upgrading from a Cequel::Model class is fairly straightforward; simply add the compact_storage directive to your class definition:

# Model definition in Cequel 0.xclassPostincludeCequel::Modelkey:id,:uuidcolumn:title,:textcolumn:body,:textend# Model definition in Cequel 1.0classPostincludeCequel::Recordkey:id,:uuidcolumn:title,:textcolumn:body,:textcompact_storageend

Note that the semantics of belongs_to and has_many are completely different between Cequel 0.x and Cequel 1.0; if you have data columns that reference keys in other tables, you will need to hand-roll those associations for now.

Upgrading a Cequel::Model::Dictionary

CQL3 does not have a direct "wide row" representation like CQL2, so the Dictionary class does not have a direct analog in Cequel 1.0. Instead, each row key-map key-value tuple in a Dictionary corresponds to a single row in CQL3. Upgrading a Dictionary to Cequel 1.0 involves defining two primary keys and a single data column, again using the compact_storage directive:

# Dictionary definition in Cequel 0.xclassBlogPosts < Cequel::Model::Dictionarykey:blog_id,:uuidmaps:uuid=>:textprivatedefserialize_value(column,value)value.to_jsonenddefdeserialize_value(column,value)JSON.parse(value)endend# Equivalent model in Cequel 1.0classBlogPostincludeCequel::Recordkey:blog_id,:uuidkey:id,:uuidcolumn:data,:textdefdataJSON.parse(read_attribute(:data))enddefdata=(new_data)write_attribute(:data,new_data.to_json)endend

Note that you will want to run ::synchronize_schema on your models when upgrading; this will not change the underlying data structure, but will add some CQL3-specific metadata to the table definition which will allow you to query it.

CQL Gotchas

CQL is designed to be immediately familiar to those of us who are used to working with SQL, which is all of us. Cequel advances this spirit by providing an ActiveRecord-like mapping for CQL. However, Cassandra is very much not a relational database, so some behaviors can come as a surprise. Here's an overview.

Upserts

Perhaps the most surprising fact about CQL is that INSERT and UPDATE are essentially the same thing: both simply persist the given column data at the given key(s). So, you may think you are creating a new record, but in fact you're overwriting data at an existing record:

# I'm just creating a blog here.blog1=Blog.create!(subdomain: 'big-data',name: 'Big Data',description: 'A blog about all things big data')# And another new blog.blog2=Blog.create!(subdomain: 'big-data',name: 'The Big Data Blog')

Living in a relational world, we'd expect the second statement to throw an error because the row with key 'big-data' already exists. But not Cassandra: the above code will just overwrite the name in that row. Note that the description will not be touched by the second statement; upserts only work on the columns that are given.

Compatibility

Rails

  • 4.0
  • 3.2
  • 3.1

Ruby

  • 2.0
  • 1.9.3
  • Rubinius 1.0 in 1.9 mode

Cassandra

  • Cassandra 1.2

Support & Bugs

If you find a bug, feel free to open an issue on GitHub. Pull requests are most welcome.

For questions or feedback, hit up our mailing list at cequel@groups.google.com or find outoftime in the #cassandra IRC channel on Freenode.

Credits

Cequel was written by:

  • Mat Brown
  • Aubrey Holland
  • Keenan Brock
  • Insoo Buzz Jung
  • Louis Simoneau
  • Randy Meech

Special thanks to Brewster, which supported the 0.x releases of Cequel.

License

Cequel is distributed under the MIT license. See the attached LICENSE for all the sordid details.

About

Ruby ORM for Cassandra with CQL3

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - PascalTurbo/cequel: Ruby ORM for Cassandra with CQL3 · GitHub
Skip to content

Repository files navigation

Cequel

Cequel is a Ruby ORM for Cassandra using CQL3.

Code Climate

Cequel::Record is an ActiveRecord-like domain model layer that exposes the robust data modeling capabilities of CQL3, including parent-child relationships via compound primary keys and collection columns.

The lower-level Cequel::Metal layer provides a CQL query builder interface inspired by the excellent Sequel library.

Installation

Add it to your Gemfile:

gem'cequel',github: 'cequel/cequel'

Rails integration

Cequel does not require Rails, but if you are using Rails, you will need version 3.2+. Cequel::Record will read from the configuration file config/cequel.yml if it is present. You can generate a default configuarion file with:

rails g cequel:configuration

Once you've got things configured (or decided to accept the defaults), run this to create your keyspace (database):

rake cequel:keyspace:create

Setting up Models

Unlike in ActiveRecord, models declare their properties inline. We'll start with a simple Blog model:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:textend

Unlike a relational database, Cassandra does not have auto-incrementing primary keys, so you must explicitly set the primary key when you create a new model. For blogs, we use a natural key, which is the subdomain. Another option is to use a UUID.

Compound keys and parent-child relationships

While Cassandra is not a relational database, compound keys do naturally map to parent-child relationships. Cequel supports this explicitly with the has_many and belongs_to relations. Let's create a model for posts that acts as the child of the blog model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:timeuuid,auto: truecolumn:title,:textcolumn:body,:textend

The auto option for the key declaration means Cequel will initialize new records with a UUID already generated. This option is only valid for :uuid and :timeuuid key columns.

Note that the belongs_to declaration must come before the key declaration. This is because belongs_to defines the partition key; the id column is the clustering column.

Practically speaking, this means that posts are accessed using both the blog_subdomain (automatically defined by the belongs_to association) and the id. The most natural way to represent this type of lookup is using a has_many association. Let's add one to Blog:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:texthas_many:postsend

Now we might do something like this:

classPostsController < ActionController::BasedefshowBlog.find(current_subdomain).posts.find(params[:id])endend

Schema synchronization

Cequel will automatically synchronize the schema stored in Cassandra to match the schema you have defined in your models. If you're using Rails, you can synchronize your schemas for everything in app/models by invoking:

rake cequel:migrate

Record sets

Record sets are lazy-loaded collections of records that correspond to a particular CQL query. They behave similarly to ActiveRecord scopes:

Post.select(:id,:title).reverse.limit(10)

To scope a record set to a primary key value, use the [] operator. This will define a scoped value for the first unscoped primary key in the record set:

Post['bigdata']# scopes posts with blog_subdomain="bigdata"

You can pass multiple arguments to the [] operator, which will generate an IN query:

Post['bigdata','nosql']# scopes posts with blog_subdomain IN ("bigdata", "nosql")

To select ranges of data, use before, after, from, upto, and in. Like the [] operator, these methods operate on the first unscoped primary key:

Post['bigdata'].after(last_id)# scopes posts with blog_subdomain="bigdata" and id > last_id

Note that record sets always load records in batches; Cassandra does not support result sets of unbounded size. This process is transparent to you but you'll see multiple queries in your logs if you're iterating over a huge result set.

Time UUID Queries

CQL has special handling for the timeuuid type, which allows you to return a rows whose UUID keys correspond to a range of timestamps.

Cequel automatically constructs timeuuid range queries if you pass a Time value for a range over a timeuuid column. So, if you want to get the posts from the last day, you can run:

Blog['myblog'].posts.from(1.day.ago)

Updating records

When you update an existing record, Cequel will only write statements to the database that correspond to explicit modifications you've made to the record in memory. So, in this situation:

@post=Blog.find(current_subdomain).posts.find(params[:id])@post.update_attributes!(title: "Announcing Cequel 1.0")

Cequel will only update the title column. Note that this is not full dirty tracking; simply setting the title on the record will signal to Cequel that you want to write that attribute to the database, regardless of its previous value.

Unloaded models

In the above example, we call the familiar find method to load a blog and then one of its posts, but we didn't actually do anything with the data in the Blog model; it was simply a convenient object-oriented way to get a handle to the blog's posts. Cequel supports unloaded models via the [] operator; this will return an unloaded blog instance, which knows the value of its primary key, but does not read the row from the database. So, we can refactor the example to be a bit more efficient:

classPostsController < ActionController::Basedefshow@post=Blog[current_subdomain].posts.find(params[:id])endend

If you attempt to access a data attribute on an unloaded class, it will lazy-load the row from the database and become a normal loaded instance.

You can generate a collection of unloaded instances by passing multiple arguments to []:

classBlogsController < ActionController::Basedefrecommended@blogs=Blog['cassandra','nosql']endend

The above will not generate a CQL query, but when you access a property on any of the unloaded Blog instances, Cequel will load data for all of them with a single query. Note that CQL does not allow selecting collection columns when loading multiple records by primary key; only scalar columns will be loaded.

There is another use for unloaded instances: you may set attributes on an unloaded instance and call save without ever actually reading the row from Cassandra. Because Cassandra is optimized for writing data, this "write without reading" pattern gives you maximum efficiency, particularly if you are updating a large number of records.

Collection columns

Cassandra supports three types of collection columns: lists, sets, and maps. Collection columns can be manipulated using atomic collection mutation; e.g., you can add an element to a set without knowing the existing elements. Cequel supports this by exposing collection objects that keep track of their modifications, and which then persist those modifications to Cassandra on save.

Let's add a category set to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textset:categories,:textend

If we were to then update a post like so:

@post=Blog[current_subdomain].posts[params[:id]]@post.categories << 'Kittens'@post.save!

Cequel would send the CQL equivalent of "Add the category 'Kittens' to the post at the given (blog_subdomain, id)", without ever reading the saved value of the categories set.

Secondary indexes

Cassandra supports secondary indexes, although with notable restrictions:

  • Only scalar data columns can be indexed; key columns and collection columns cannot.
  • A secondary index consists of exactly one column.
  • Though you can have more than one secondary index on a table, you can only use one in any given query.

Cequel supports the :index option to add secondary indexes to column definitions:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textcolumn:author_id,:uuid,:index=>trueset:categories,:textend

Defining a column with a secondary index adds several "magic methods" for using the index:

Post.with_author_id(id)# returns a record set scoped to that author_idPost.find_by_author_id(id)# returns the first post with that author_idPost.find_all_by_author_id(id)# returns an array of all posts with that author_id

You can also call the where method directly on record sets:

Post.where(:author_id,id)

Note that where is only for secondary indexed columns; use [] to scope record sets by primary keys.

ActiveModel Support

Cequel supports ActiveModel functionality, such as callbacks, validations, dirty attribute tracking, naming, and serialization. If you're using Rails 3, mass-assignment protection works as usual, and in Rails 4, strong parameters are treated correctly. So we can add some extra ActiveModel goodness to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textvalidates:body,presence: trueafter_save:notify_followersend

Note that validations or callbacks that need to read data attributes will cause unloaded models to load their row during the course of the save operation, so if you are following a write-without-reading pattern, you will need to be careful.

Dirty attribute tracking is only enabled on loaded models.

Upgrading from Cequel 0.x

Cequel 0.x targeted CQL2, which has a substantially different data representation from CQL3. Accordingly, upgrading from Cequel 0.x to Cequel 1.0 requires some changes to your data models.

Upgrading a Cequel::Model

Upgrading from a Cequel::Model class is fairly straightforward; simply add the compact_storage directive to your class definition:

# Model definition in Cequel 0.xclassPostincludeCequel::Modelkey:id,:uuidcolumn:title,:textcolumn:body,:textend# Model definition in Cequel 1.0classPostincludeCequel::Recordkey:id,:uuidcolumn:title,:textcolumn:body,:textcompact_storageend

Note that the semantics of belongs_to and has_many are completely different between Cequel 0.x and Cequel 1.0; if you have data columns that reference keys in other tables, you will need to hand-roll those associations for now.

Upgrading a Cequel::Model::Dictionary

CQL3 does not have a direct "wide row" representation like CQL2, so the Dictionary class does not have a direct analog in Cequel 1.0. Instead, each row key-map key-value tuple in a Dictionary corresponds to a single row in CQL3. Upgrading a Dictionary to Cequel 1.0 involves defining two primary keys and a single data column, again using the compact_storage directive:

# Dictionary definition in Cequel 0.xclassBlogPosts < Cequel::Model::Dictionarykey:blog_id,:uuidmaps:uuid=>:textprivatedefserialize_value(column,value)value.to_jsonenddefdeserialize_value(column,value)JSON.parse(value)endend# Equivalent model in Cequel 1.0classBlogPostincludeCequel::Recordkey:blog_id,:uuidkey:id,:uuidcolumn:data,:textdefdataJSON.parse(read_attribute(:data))enddefdata=(new_data)write_attribute(:data,new_data.to_json)endend

Note that you will want to run ::synchronize_schema on your models when upgrading; this will not change the underlying data structure, but will add some CQL3-specific metadata to the table definition which will allow you to query it.

CQL Gotchas

CQL is designed to be immediately familiar to those of us who are used to working with SQL, which is all of us. Cequel advances this spirit by providing an ActiveRecord-like mapping for CQL. However, Cassandra is very much not a relational database, so some behaviors can come as a surprise. Here's an overview.

Upserts

Perhaps the most surprising fact about CQL is that INSERT and UPDATE are essentially the same thing: both simply persist the given column data at the given key(s). So, you may think you are creating a new record, but in fact you're overwriting data at an existing record:

# I'm just creating a blog here.blog1=Blog.create!(subdomain: 'big-data',name: 'Big Data',description: 'A blog about all things big data')# And another new blog.blog2=Blog.create!(subdomain: 'big-data',name: 'The Big Data Blog')

Living in a relational world, we'd expect the second statement to throw an error because the row with key 'big-data' already exists. But not Cassandra: the above code will just overwrite the name in that row. Note that the description will not be touched by the second statement; upserts only work on the columns that are given.

Compatibility

Rails

  • 4.0
  • 3.2
  • 3.1

Ruby

  • 2.0
  • 1.9.3
  • Rubinius 1.0 in 1.9 mode

Cassandra

  • Cassandra 1.2

Support & Bugs

If you find a bug, feel free to open an issue on GitHub. Pull requests are most welcome.

For questions or feedback, hit up our mailing list at cequel@groups.google.com or find outoftime in the #cassandra IRC channel on Freenode.

Credits

Cequel was written by:

  • Mat Brown
  • Aubrey Holland
  • Keenan Brock
  • Insoo Buzz Jung
  • Louis Simoneau
  • Randy Meech

Special thanks to Brewster, which supported the 0.x releases of Cequel.

License

Cequel is distributed under the MIT license. See the attached LICENSE for all the sordid details.

About

Ruby ORM for Cassandra with CQL3

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - PascalTurbo/cequel: Ruby ORM for Cassandra with CQL3 · GitHub
Skip to content

Repository files navigation

Cequel

Cequel is a Ruby ORM for Cassandra using CQL3.

Code Climate

Cequel::Record is an ActiveRecord-like domain model layer that exposes the robust data modeling capabilities of CQL3, including parent-child relationships via compound primary keys and collection columns.

The lower-level Cequel::Metal layer provides a CQL query builder interface inspired by the excellent Sequel library.

Installation

Add it to your Gemfile:

gem'cequel',github: 'cequel/cequel'

Rails integration

Cequel does not require Rails, but if you are using Rails, you will need version 3.2+. Cequel::Record will read from the configuration file config/cequel.yml if it is present. You can generate a default configuarion file with:

rails g cequel:configuration

Once you've got things configured (or decided to accept the defaults), run this to create your keyspace (database):

rake cequel:keyspace:create

Setting up Models

Unlike in ActiveRecord, models declare their properties inline. We'll start with a simple Blog model:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:textend

Unlike a relational database, Cassandra does not have auto-incrementing primary keys, so you must explicitly set the primary key when you create a new model. For blogs, we use a natural key, which is the subdomain. Another option is to use a UUID.

Compound keys and parent-child relationships

While Cassandra is not a relational database, compound keys do naturally map to parent-child relationships. Cequel supports this explicitly with the has_many and belongs_to relations. Let's create a model for posts that acts as the child of the blog model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:timeuuid,auto: truecolumn:title,:textcolumn:body,:textend

The auto option for the key declaration means Cequel will initialize new records with a UUID already generated. This option is only valid for :uuid and :timeuuid key columns.

Note that the belongs_to declaration must come before the key declaration. This is because belongs_to defines the partition key; the id column is the clustering column.

Practically speaking, this means that posts are accessed using both the blog_subdomain (automatically defined by the belongs_to association) and the id. The most natural way to represent this type of lookup is using a has_many association. Let's add one to Blog:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:texthas_many:postsend

Now we might do something like this:

classPostsController < ActionController::BasedefshowBlog.find(current_subdomain).posts.find(params[:id])endend

Schema synchronization

Cequel will automatically synchronize the schema stored in Cassandra to match the schema you have defined in your models. If you're using Rails, you can synchronize your schemas for everything in app/models by invoking:

rake cequel:migrate

Record sets

Record sets are lazy-loaded collections of records that correspond to a particular CQL query. They behave similarly to ActiveRecord scopes:

Post.select(:id,:title).reverse.limit(10)

To scope a record set to a primary key value, use the [] operator. This will define a scoped value for the first unscoped primary key in the record set:

Post['bigdata']# scopes posts with blog_subdomain="bigdata"

You can pass multiple arguments to the [] operator, which will generate an IN query:

Post['bigdata','nosql']# scopes posts with blog_subdomain IN ("bigdata", "nosql")

To select ranges of data, use before, after, from, upto, and in. Like the [] operator, these methods operate on the first unscoped primary key:

Post['bigdata'].after(last_id)# scopes posts with blog_subdomain="bigdata" and id > last_id

Note that record sets always load records in batches; Cassandra does not support result sets of unbounded size. This process is transparent to you but you'll see multiple queries in your logs if you're iterating over a huge result set.

Time UUID Queries

CQL has special handling for the timeuuid type, which allows you to return a rows whose UUID keys correspond to a range of timestamps.

Cequel automatically constructs timeuuid range queries if you pass a Time value for a range over a timeuuid column. So, if you want to get the posts from the last day, you can run:

Blog['myblog'].posts.from(1.day.ago)

Updating records

When you update an existing record, Cequel will only write statements to the database that correspond to explicit modifications you've made to the record in memory. So, in this situation:

@post=Blog.find(current_subdomain).posts.find(params[:id])@post.update_attributes!(title: "Announcing Cequel 1.0")

Cequel will only update the title column. Note that this is not full dirty tracking; simply setting the title on the record will signal to Cequel that you want to write that attribute to the database, regardless of its previous value.

Unloaded models

In the above example, we call the familiar find method to load a blog and then one of its posts, but we didn't actually do anything with the data in the Blog model; it was simply a convenient object-oriented way to get a handle to the blog's posts. Cequel supports unloaded models via the [] operator; this will return an unloaded blog instance, which knows the value of its primary key, but does not read the row from the database. So, we can refactor the example to be a bit more efficient:

classPostsController < ActionController::Basedefshow@post=Blog[current_subdomain].posts.find(params[:id])endend

If you attempt to access a data attribute on an unloaded class, it will lazy-load the row from the database and become a normal loaded instance.

You can generate a collection of unloaded instances by passing multiple arguments to []:

classBlogsController < ActionController::Basedefrecommended@blogs=Blog['cassandra','nosql']endend

The above will not generate a CQL query, but when you access a property on any of the unloaded Blog instances, Cequel will load data for all of them with a single query. Note that CQL does not allow selecting collection columns when loading multiple records by primary key; only scalar columns will be loaded.

There is another use for unloaded instances: you may set attributes on an unloaded instance and call save without ever actually reading the row from Cassandra. Because Cassandra is optimized for writing data, this "write without reading" pattern gives you maximum efficiency, particularly if you are updating a large number of records.

Collection columns

Cassandra supports three types of collection columns: lists, sets, and maps. Collection columns can be manipulated using atomic collection mutation; e.g., you can add an element to a set without knowing the existing elements. Cequel supports this by exposing collection objects that keep track of their modifications, and which then persist those modifications to Cassandra on save.

Let's add a category set to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textset:categories,:textend

If we were to then update a post like so:

@post=Blog[current_subdomain].posts[params[:id]]@post.categories << 'Kittens'@post.save!

Cequel would send the CQL equivalent of "Add the category 'Kittens' to the post at the given (blog_subdomain, id)", without ever reading the saved value of the categories set.

Secondary indexes

Cassandra supports secondary indexes, although with notable restrictions:

  • Only scalar data columns can be indexed; key columns and collection columns cannot.
  • A secondary index consists of exactly one column.
  • Though you can have more than one secondary index on a table, you can only use one in any given query.

Cequel supports the :index option to add secondary indexes to column definitions:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textcolumn:author_id,:uuid,:index=>trueset:categories,:textend

Defining a column with a secondary index adds several "magic methods" for using the index:

Post.with_author_id(id)# returns a record set scoped to that author_idPost.find_by_author_id(id)# returns the first post with that author_idPost.find_all_by_author_id(id)# returns an array of all posts with that author_id

You can also call the where method directly on record sets:

Post.where(:author_id,id)

Note that where is only for secondary indexed columns; use [] to scope record sets by primary keys.

ActiveModel Support

Cequel supports ActiveModel functionality, such as callbacks, validations, dirty attribute tracking, naming, and serialization. If you're using Rails 3, mass-assignment protection works as usual, and in Rails 4, strong parameters are treated correctly. So we can add some extra ActiveModel goodness to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textvalidates:body,presence: trueafter_save:notify_followersend

Note that validations or callbacks that need to read data attributes will cause unloaded models to load their row during the course of the save operation, so if you are following a write-without-reading pattern, you will need to be careful.

Dirty attribute tracking is only enabled on loaded models.

Upgrading from Cequel 0.x

Cequel 0.x targeted CQL2, which has a substantially different data representation from CQL3. Accordingly, upgrading from Cequel 0.x to Cequel 1.0 requires some changes to your data models.

Upgrading a Cequel::Model

Upgrading from a Cequel::Model class is fairly straightforward; simply add the compact_storage directive to your class definition:

# Model definition in Cequel 0.xclassPostincludeCequel::Modelkey:id,:uuidcolumn:title,:textcolumn:body,:textend# Model definition in Cequel 1.0classPostincludeCequel::Recordkey:id,:uuidcolumn:title,:textcolumn:body,:textcompact_storageend

Note that the semantics of belongs_to and has_many are completely different between Cequel 0.x and Cequel 1.0; if you have data columns that reference keys in other tables, you will need to hand-roll those associations for now.

Upgrading a Cequel::Model::Dictionary

CQL3 does not have a direct "wide row" representation like CQL2, so the Dictionary class does not have a direct analog in Cequel 1.0. Instead, each row key-map key-value tuple in a Dictionary corresponds to a single row in CQL3. Upgrading a Dictionary to Cequel 1.0 involves defining two primary keys and a single data column, again using the compact_storage directive:

# Dictionary definition in Cequel 0.xclassBlogPosts < Cequel::Model::Dictionarykey:blog_id,:uuidmaps:uuid=>:textprivatedefserialize_value(column,value)value.to_jsonenddefdeserialize_value(column,value)JSON.parse(value)endend# Equivalent model in Cequel 1.0classBlogPostincludeCequel::Recordkey:blog_id,:uuidkey:id,:uuidcolumn:data,:textdefdataJSON.parse(read_attribute(:data))enddefdata=(new_data)write_attribute(:data,new_data.to_json)endend

Note that you will want to run ::synchronize_schema on your models when upgrading; this will not change the underlying data structure, but will add some CQL3-specific metadata to the table definition which will allow you to query it.

CQL Gotchas

CQL is designed to be immediately familiar to those of us who are used to working with SQL, which is all of us. Cequel advances this spirit by providing an ActiveRecord-like mapping for CQL. However, Cassandra is very much not a relational database, so some behaviors can come as a surprise. Here's an overview.

Upserts

Perhaps the most surprising fact about CQL is that INSERT and UPDATE are essentially the same thing: both simply persist the given column data at the given key(s). So, you may think you are creating a new record, but in fact you're overwriting data at an existing record:

# I'm just creating a blog here.blog1=Blog.create!(subdomain: 'big-data',name: 'Big Data',description: 'A blog about all things big data')# And another new blog.blog2=Blog.create!(subdomain: 'big-data',name: 'The Big Data Blog')

Living in a relational world, we'd expect the second statement to throw an error because the row with key 'big-data' already exists. But not Cassandra: the above code will just overwrite the name in that row. Note that the description will not be touched by the second statement; upserts only work on the columns that are given.

Compatibility

Rails

  • 4.0
  • 3.2
  • 3.1

Ruby

  • 2.0
  • 1.9.3
  • Rubinius 1.0 in 1.9 mode

Cassandra

  • Cassandra 1.2

Support & Bugs

If you find a bug, feel free to open an issue on GitHub. Pull requests are most welcome.

For questions or feedback, hit up our mailing list at cequel@groups.google.com or find outoftime in the #cassandra IRC channel on Freenode.

Credits

Cequel was written by:

  • Mat Brown
  • Aubrey Holland
  • Keenan Brock
  • Insoo Buzz Jung
  • Louis Simoneau
  • Randy Meech

Special thanks to Brewster, which supported the 0.x releases of Cequel.

License

Cequel is distributed under the MIT license. See the attached LICENSE for all the sordid details.

About

Ruby ORM for Cassandra with CQL3

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - PascalTurbo/cequel: Ruby ORM for Cassandra with CQL3 · GitHub
Skip to content

Repository files navigation

Cequel

Cequel is a Ruby ORM for Cassandra using CQL3.

Code Climate

Cequel::Record is an ActiveRecord-like domain model layer that exposes the robust data modeling capabilities of CQL3, including parent-child relationships via compound primary keys and collection columns.

The lower-level Cequel::Metal layer provides a CQL query builder interface inspired by the excellent Sequel library.

Installation

Add it to your Gemfile:

gem'cequel',github: 'cequel/cequel'

Rails integration

Cequel does not require Rails, but if you are using Rails, you will need version 3.2+. Cequel::Record will read from the configuration file config/cequel.yml if it is present. You can generate a default configuarion file with:

rails g cequel:configuration

Once you've got things configured (or decided to accept the defaults), run this to create your keyspace (database):

rake cequel:keyspace:create

Setting up Models

Unlike in ActiveRecord, models declare their properties inline. We'll start with a simple Blog model:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:textend

Unlike a relational database, Cassandra does not have auto-incrementing primary keys, so you must explicitly set the primary key when you create a new model. For blogs, we use a natural key, which is the subdomain. Another option is to use a UUID.

Compound keys and parent-child relationships

While Cassandra is not a relational database, compound keys do naturally map to parent-child relationships. Cequel supports this explicitly with the has_many and belongs_to relations. Let's create a model for posts that acts as the child of the blog model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:timeuuid,auto: truecolumn:title,:textcolumn:body,:textend

The auto option for the key declaration means Cequel will initialize new records with a UUID already generated. This option is only valid for :uuid and :timeuuid key columns.

Note that the belongs_to declaration must come before the key declaration. This is because belongs_to defines the partition key; the id column is the clustering column.

Practically speaking, this means that posts are accessed using both the blog_subdomain (automatically defined by the belongs_to association) and the id. The most natural way to represent this type of lookup is using a has_many association. Let's add one to Blog:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:texthas_many:postsend

Now we might do something like this:

classPostsController < ActionController::BasedefshowBlog.find(current_subdomain).posts.find(params[:id])endend

Schema synchronization

Cequel will automatically synchronize the schema stored in Cassandra to match the schema you have defined in your models. If you're using Rails, you can synchronize your schemas for everything in app/models by invoking:

rake cequel:migrate

Record sets

Record sets are lazy-loaded collections of records that correspond to a particular CQL query. They behave similarly to ActiveRecord scopes:

Post.select(:id,:title).reverse.limit(10)

To scope a record set to a primary key value, use the [] operator. This will define a scoped value for the first unscoped primary key in the record set:

Post['bigdata']# scopes posts with blog_subdomain="bigdata"

You can pass multiple arguments to the [] operator, which will generate an IN query:

Post['bigdata','nosql']# scopes posts with blog_subdomain IN ("bigdata", "nosql")

To select ranges of data, use before, after, from, upto, and in. Like the [] operator, these methods operate on the first unscoped primary key:

Post['bigdata'].after(last_id)# scopes posts with blog_subdomain="bigdata" and id > last_id

Note that record sets always load records in batches; Cassandra does not support result sets of unbounded size. This process is transparent to you but you'll see multiple queries in your logs if you're iterating over a huge result set.

Time UUID Queries

CQL has special handling for the timeuuid type, which allows you to return a rows whose UUID keys correspond to a range of timestamps.

Cequel automatically constructs timeuuid range queries if you pass a Time value for a range over a timeuuid column. So, if you want to get the posts from the last day, you can run:

Blog['myblog'].posts.from(1.day.ago)

Updating records

When you update an existing record, Cequel will only write statements to the database that correspond to explicit modifications you've made to the record in memory. So, in this situation:

@post=Blog.find(current_subdomain).posts.find(params[:id])@post.update_attributes!(title: "Announcing Cequel 1.0")

Cequel will only update the title column. Note that this is not full dirty tracking; simply setting the title on the record will signal to Cequel that you want to write that attribute to the database, regardless of its previous value.

Unloaded models

In the above example, we call the familiar find method to load a blog and then one of its posts, but we didn't actually do anything with the data in the Blog model; it was simply a convenient object-oriented way to get a handle to the blog's posts. Cequel supports unloaded models via the [] operator; this will return an unloaded blog instance, which knows the value of its primary key, but does not read the row from the database. So, we can refactor the example to be a bit more efficient:

classPostsController < ActionController::Basedefshow@post=Blog[current_subdomain].posts.find(params[:id])endend

If you attempt to access a data attribute on an unloaded class, it will lazy-load the row from the database and become a normal loaded instance.

You can generate a collection of unloaded instances by passing multiple arguments to []:

classBlogsController < ActionController::Basedefrecommended@blogs=Blog['cassandra','nosql']endend

The above will not generate a CQL query, but when you access a property on any of the unloaded Blog instances, Cequel will load data for all of them with a single query. Note that CQL does not allow selecting collection columns when loading multiple records by primary key; only scalar columns will be loaded.

There is another use for unloaded instances: you may set attributes on an unloaded instance and call save without ever actually reading the row from Cassandra. Because Cassandra is optimized for writing data, this "write without reading" pattern gives you maximum efficiency, particularly if you are updating a large number of records.

Collection columns

Cassandra supports three types of collection columns: lists, sets, and maps. Collection columns can be manipulated using atomic collection mutation; e.g., you can add an element to a set without knowing the existing elements. Cequel supports this by exposing collection objects that keep track of their modifications, and which then persist those modifications to Cassandra on save.

Let's add a category set to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textset:categories,:textend

If we were to then update a post like so:

@post=Blog[current_subdomain].posts[params[:id]]@post.categories << 'Kittens'@post.save!

Cequel would send the CQL equivalent of "Add the category 'Kittens' to the post at the given (blog_subdomain, id)", without ever reading the saved value of the categories set.

Secondary indexes

Cassandra supports secondary indexes, although with notable restrictions:

  • Only scalar data columns can be indexed; key columns and collection columns cannot.
  • A secondary index consists of exactly one column.
  • Though you can have more than one secondary index on a table, you can only use one in any given query.

Cequel supports the :index option to add secondary indexes to column definitions:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textcolumn:author_id,:uuid,:index=>trueset:categories,:textend

Defining a column with a secondary index adds several "magic methods" for using the index:

Post.with_author_id(id)# returns a record set scoped to that author_idPost.find_by_author_id(id)# returns the first post with that author_idPost.find_all_by_author_id(id)# returns an array of all posts with that author_id

You can also call the where method directly on record sets:

Post.where(:author_id,id)

Note that where is only for secondary indexed columns; use [] to scope record sets by primary keys.

ActiveModel Support

Cequel supports ActiveModel functionality, such as callbacks, validations, dirty attribute tracking, naming, and serialization. If you're using Rails 3, mass-assignment protection works as usual, and in Rails 4, strong parameters are treated correctly. So we can add some extra ActiveModel goodness to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textvalidates:body,presence: trueafter_save:notify_followersend

Note that validations or callbacks that need to read data attributes will cause unloaded models to load their row during the course of the save operation, so if you are following a write-without-reading pattern, you will need to be careful.

Dirty attribute tracking is only enabled on loaded models.

Upgrading from Cequel 0.x

Cequel 0.x targeted CQL2, which has a substantially different data representation from CQL3. Accordingly, upgrading from Cequel 0.x to Cequel 1.0 requires some changes to your data models.

Upgrading a Cequel::Model

Upgrading from a Cequel::Model class is fairly straightforward; simply add the compact_storage directive to your class definition:

# Model definition in Cequel 0.xclassPostincludeCequel::Modelkey:id,:uuidcolumn:title,:textcolumn:body,:textend# Model definition in Cequel 1.0classPostincludeCequel::Recordkey:id,:uuidcolumn:title,:textcolumn:body,:textcompact_storageend

Note that the semantics of belongs_to and has_many are completely different between Cequel 0.x and Cequel 1.0; if you have data columns that reference keys in other tables, you will need to hand-roll those associations for now.

Upgrading a Cequel::Model::Dictionary

CQL3 does not have a direct "wide row" representation like CQL2, so the Dictionary class does not have a direct analog in Cequel 1.0. Instead, each row key-map key-value tuple in a Dictionary corresponds to a single row in CQL3. Upgrading a Dictionary to Cequel 1.0 involves defining two primary keys and a single data column, again using the compact_storage directive:

# Dictionary definition in Cequel 0.xclassBlogPosts < Cequel::Model::Dictionarykey:blog_id,:uuidmaps:uuid=>:textprivatedefserialize_value(column,value)value.to_jsonenddefdeserialize_value(column,value)JSON.parse(value)endend# Equivalent model in Cequel 1.0classBlogPostincludeCequel::Recordkey:blog_id,:uuidkey:id,:uuidcolumn:data,:textdefdataJSON.parse(read_attribute(:data))enddefdata=(new_data)write_attribute(:data,new_data.to_json)endend

Note that you will want to run ::synchronize_schema on your models when upgrading; this will not change the underlying data structure, but will add some CQL3-specific metadata to the table definition which will allow you to query it.

CQL Gotchas

CQL is designed to be immediately familiar to those of us who are used to working with SQL, which is all of us. Cequel advances this spirit by providing an ActiveRecord-like mapping for CQL. However, Cassandra is very much not a relational database, so some behaviors can come as a surprise. Here's an overview.

Upserts

Perhaps the most surprising fact about CQL is that INSERT and UPDATE are essentially the same thing: both simply persist the given column data at the given key(s). So, you may think you are creating a new record, but in fact you're overwriting data at an existing record:

# I'm just creating a blog here.blog1=Blog.create!(subdomain: 'big-data',name: 'Big Data',description: 'A blog about all things big data')# And another new blog.blog2=Blog.create!(subdomain: 'big-data',name: 'The Big Data Blog')

Living in a relational world, we'd expect the second statement to throw an error because the row with key 'big-data' already exists. But not Cassandra: the above code will just overwrite the name in that row. Note that the description will not be touched by the second statement; upserts only work on the columns that are given.

Compatibility

Rails

  • 4.0
  • 3.2
  • 3.1

Ruby

  • 2.0
  • 1.9.3
  • Rubinius 1.0 in 1.9 mode

Cassandra

  • Cassandra 1.2

Support & Bugs

If you find a bug, feel free to open an issue on GitHub. Pull requests are most welcome.

For questions or feedback, hit up our mailing list at cequel@groups.google.com or find outoftime in the #cassandra IRC channel on Freenode.

Credits

Cequel was written by:

  • Mat Brown
  • Aubrey Holland
  • Keenan Brock
  • Insoo Buzz Jung
  • Louis Simoneau
  • Randy Meech

Special thanks to Brewster, which supported the 0.x releases of Cequel.

License

Cequel is distributed under the MIT license. See the attached LICENSE for all the sordid details.

About

Ruby ORM for Cassandra with CQL3

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - PascalTurbo/cequel: Ruby ORM for Cassandra with CQL3 · GitHub
Skip to content

Repository files navigation

Cequel

Cequel is a Ruby ORM for Cassandra using CQL3.

Code Climate

Cequel::Record is an ActiveRecord-like domain model layer that exposes the robust data modeling capabilities of CQL3, including parent-child relationships via compound primary keys and collection columns.

The lower-level Cequel::Metal layer provides a CQL query builder interface inspired by the excellent Sequel library.

Installation

Add it to your Gemfile:

gem'cequel',github: 'cequel/cequel'

Rails integration

Cequel does not require Rails, but if you are using Rails, you will need version 3.2+. Cequel::Record will read from the configuration file config/cequel.yml if it is present. You can generate a default configuarion file with:

rails g cequel:configuration

Once you've got things configured (or decided to accept the defaults), run this to create your keyspace (database):

rake cequel:keyspace:create

Setting up Models

Unlike in ActiveRecord, models declare their properties inline. We'll start with a simple Blog model:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:textend

Unlike a relational database, Cassandra does not have auto-incrementing primary keys, so you must explicitly set the primary key when you create a new model. For blogs, we use a natural key, which is the subdomain. Another option is to use a UUID.

Compound keys and parent-child relationships

While Cassandra is not a relational database, compound keys do naturally map to parent-child relationships. Cequel supports this explicitly with the has_many and belongs_to relations. Let's create a model for posts that acts as the child of the blog model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:timeuuid,auto: truecolumn:title,:textcolumn:body,:textend

The auto option for the key declaration means Cequel will initialize new records with a UUID already generated. This option is only valid for :uuid and :timeuuid key columns.

Note that the belongs_to declaration must come before the key declaration. This is because belongs_to defines the partition key; the id column is the clustering column.

Practically speaking, this means that posts are accessed using both the blog_subdomain (automatically defined by the belongs_to association) and the id. The most natural way to represent this type of lookup is using a has_many association. Let's add one to Blog:

classBlogincludeCequel::Recordkey:subdomain,:textcolumn:name,:textcolumn:description,:texthas_many:postsend

Now we might do something like this:

classPostsController < ActionController::BasedefshowBlog.find(current_subdomain).posts.find(params[:id])endend

Schema synchronization

Cequel will automatically synchronize the schema stored in Cassandra to match the schema you have defined in your models. If you're using Rails, you can synchronize your schemas for everything in app/models by invoking:

rake cequel:migrate

Record sets

Record sets are lazy-loaded collections of records that correspond to a particular CQL query. They behave similarly to ActiveRecord scopes:

Post.select(:id,:title).reverse.limit(10)

To scope a record set to a primary key value, use the [] operator. This will define a scoped value for the first unscoped primary key in the record set:

Post['bigdata']# scopes posts with blog_subdomain="bigdata"

You can pass multiple arguments to the [] operator, which will generate an IN query:

Post['bigdata','nosql']# scopes posts with blog_subdomain IN ("bigdata", "nosql")

To select ranges of data, use before, after, from, upto, and in. Like the [] operator, these methods operate on the first unscoped primary key:

Post['bigdata'].after(last_id)# scopes posts with blog_subdomain="bigdata" and id > last_id

Note that record sets always load records in batches; Cassandra does not support result sets of unbounded size. This process is transparent to you but you'll see multiple queries in your logs if you're iterating over a huge result set.

Time UUID Queries

CQL has special handling for the timeuuid type, which allows you to return a rows whose UUID keys correspond to a range of timestamps.

Cequel automatically constructs timeuuid range queries if you pass a Time value for a range over a timeuuid column. So, if you want to get the posts from the last day, you can run:

Blog['myblog'].posts.from(1.day.ago)

Updating records

When you update an existing record, Cequel will only write statements to the database that correspond to explicit modifications you've made to the record in memory. So, in this situation:

@post=Blog.find(current_subdomain).posts.find(params[:id])@post.update_attributes!(title: "Announcing Cequel 1.0")

Cequel will only update the title column. Note that this is not full dirty tracking; simply setting the title on the record will signal to Cequel that you want to write that attribute to the database, regardless of its previous value.

Unloaded models

In the above example, we call the familiar find method to load a blog and then one of its posts, but we didn't actually do anything with the data in the Blog model; it was simply a convenient object-oriented way to get a handle to the blog's posts. Cequel supports unloaded models via the [] operator; this will return an unloaded blog instance, which knows the value of its primary key, but does not read the row from the database. So, we can refactor the example to be a bit more efficient:

classPostsController < ActionController::Basedefshow@post=Blog[current_subdomain].posts.find(params[:id])endend

If you attempt to access a data attribute on an unloaded class, it will lazy-load the row from the database and become a normal loaded instance.

You can generate a collection of unloaded instances by passing multiple arguments to []:

classBlogsController < ActionController::Basedefrecommended@blogs=Blog['cassandra','nosql']endend

The above will not generate a CQL query, but when you access a property on any of the unloaded Blog instances, Cequel will load data for all of them with a single query. Note that CQL does not allow selecting collection columns when loading multiple records by primary key; only scalar columns will be loaded.

There is another use for unloaded instances: you may set attributes on an unloaded instance and call save without ever actually reading the row from Cassandra. Because Cassandra is optimized for writing data, this "write without reading" pattern gives you maximum efficiency, particularly if you are updating a large number of records.

Collection columns

Cassandra supports three types of collection columns: lists, sets, and maps. Collection columns can be manipulated using atomic collection mutation; e.g., you can add an element to a set without knowing the existing elements. Cequel supports this by exposing collection objects that keep track of their modifications, and which then persist those modifications to Cassandra on save.

Let's add a category set to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textset:categories,:textend

If we were to then update a post like so:

@post=Blog[current_subdomain].posts[params[:id]]@post.categories << 'Kittens'@post.save!

Cequel would send the CQL equivalent of "Add the category 'Kittens' to the post at the given (blog_subdomain, id)", without ever reading the saved value of the categories set.

Secondary indexes

Cassandra supports secondary indexes, although with notable restrictions:

  • Only scalar data columns can be indexed; key columns and collection columns cannot.
  • A secondary index consists of exactly one column.
  • Though you can have more than one secondary index on a table, you can only use one in any given query.

Cequel supports the :index option to add secondary indexes to column definitions:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textcolumn:author_id,:uuid,:index=>trueset:categories,:textend

Defining a column with a secondary index adds several "magic methods" for using the index:

Post.with_author_id(id)# returns a record set scoped to that author_idPost.find_by_author_id(id)# returns the first post with that author_idPost.find_all_by_author_id(id)# returns an array of all posts with that author_id

You can also call the where method directly on record sets:

Post.where(:author_id,id)

Note that where is only for secondary indexed columns; use [] to scope record sets by primary keys.

ActiveModel Support

Cequel supports ActiveModel functionality, such as callbacks, validations, dirty attribute tracking, naming, and serialization. If you're using Rails 3, mass-assignment protection works as usual, and in Rails 4, strong parameters are treated correctly. So we can add some extra ActiveModel goodness to our post model:

classPostincludeCequel::Recordbelongs_to:blogkey:id,:uuidcolumn:title,:textcolumn:body,:textvalidates:body,presence: trueafter_save:notify_followersend

Note that validations or callbacks that need to read data attributes will cause unloaded models to load their row during the course of the save operation, so if you are following a write-without-reading pattern, you will need to be careful.

Dirty attribute tracking is only enabled on loaded models.

Upgrading from Cequel 0.x

Cequel 0.x targeted CQL2, which has a substantially different data representation from CQL3. Accordingly, upgrading from Cequel 0.x to Cequel 1.0 requires some changes to your data models.

Upgrading a Cequel::Model

Upgrading from a Cequel::Model class is fairly straightforward; simply add the compact_storage directive to your class definition:

# Model definition in Cequel 0.xclassPostincludeCequel::Modelkey:id,:uuidcolumn:title,:textcolumn:body,:textend# Model definition in Cequel 1.0classPostincludeCequel::Recordkey:id,:uuidcolumn:title,:textcolumn:body,:textcompact_storageend

Note that the semantics of belongs_to and has_many are completely different between Cequel 0.x and Cequel 1.0; if you have data columns that reference keys in other tables, you will need to hand-roll those associations for now.

Upgrading a Cequel::Model::Dictionary

CQL3 does not have a direct "wide row" representation like CQL2, so the Dictionary class does not have a direct analog in Cequel 1.0. Instead, each row key-map key-value tuple in a Dictionary corresponds to a single row in CQL3. Upgrading a Dictionary to Cequel 1.0 involves defining two primary keys and a single data column, again using the compact_storage directive:

# Dictionary definition in Cequel 0.xclassBlogPosts < Cequel::Model::Dictionarykey:blog_id,:uuidmaps:uuid=>:textprivatedefserialize_value(column,value)value.to_jsonenddefdeserialize_value(column,value)JSON.parse(value)endend# Equivalent model in Cequel 1.0classBlogPostincludeCequel::Recordkey:blog_id,:uuidkey:id,:uuidcolumn:data,:textdefdataJSON.parse(read_attribute(:data))enddefdata=(new_data)write_attribute(:data,new_data.to_json)endend

Note that you will want to run ::synchronize_schema on your models when upgrading; this will not change the underlying data structure, but will add some CQL3-specific metadata to the table definition which will allow you to query it.

CQL Gotchas

CQL is designed to be immediately familiar to those of us who are used to working with SQL, which is all of us. Cequel advances this spirit by providing an ActiveRecord-like mapping for CQL. However, Cassandra is very much not a relational database, so some behaviors can come as a surprise. Here's an overview.

Upserts

Perhaps the most surprising fact about CQL is that INSERT and UPDATE are essentially the same thing: both simply persist the given column data at the given key(s). So, you may think you are creating a new record, but in fact you're overwriting data at an existing record:

# I'm just creating a blog here.blog1=Blog.create!(subdomain: 'big-data',name: 'Big Data',description: 'A blog about all things big data')# And another new blog.blog2=Blog.create!(subdomain: 'big-data',name: 'The Big Data Blog')

Living in a relational world, we'd expect the second statement to throw an error because the row with key 'big-data' already exists. But not Cassandra: the above code will just overwrite the name in that row. Note that the description will not be touched by the second statement; upserts only work on the columns that are given.

Compatibility

Rails

  • 4.0
  • 3.2
  • 3.1

Ruby

  • 2.0
  • 1.9.3
  • Rubinius 1.0 in 1.9 mode

Cassandra

  • Cassandra 1.2

Support & Bugs

If you find a bug, feel free to open an issue on GitHub. Pull requests are most welcome.

For questions or feedback, hit up our mailing list at cequel@groups.google.com or find outoftime in the #cassandra IRC channel on Freenode.

Credits

Cequel was written by:

  • Mat Brown
  • Aubrey Holland
  • Keenan Brock
  • Insoo Buzz Jung
  • Louis Simoneau
  • Randy Meech

Special thanks to Brewster, which supported the 0.x releases of Cequel.

License

Cequel is distributed under the MIT license. See the attached LICENSE for all the sordid details.

About

Ruby ORM for Cassandra with CQL3

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages