├── VERSION ├── .ruby-version ├── .gitignore ├── specs └── spec_helper.rb ├── features ├── support │ └── env.rb ├── dsl.feature ├── select.feature ├── step_definitions │ └── query.rb ├── update.feature ├── insert.feature └── select_where.feature ├── lib ├── cql_model.rb └── cql_model │ ├── query │ ├── parse_error.rb │ ├── mutation_statement.rb │ ├── statement.rb │ ├── update_statement.rb │ ├── insert_statement.rb │ ├── expression.rb │ └── select_statement.rb │ ├── query.rb │ ├── model.rb │ └── model │ └── dsl.rb ├── cql_model.rconf ├── README.md ├── Gemfile ├── Gemfile.lock ├── Rakefile └── cql_model.gemspec /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 1.9.3-p392 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | tags -------------------------------------------------------------------------------- /specs/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'debugger' 2 | 3 | $: << File.expand_path('../../lib', __FILE__) 4 | 5 | require 'cql_model' -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'debugger' 2 | 3 | $: << File.expand_path('../../../lib', __FILE__) 4 | 5 | require 'cql_model' 6 | -------------------------------------------------------------------------------- /lib/cql_model.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require 'cql' 3 | 4 | module CQLModel 5 | end 6 | 7 | require 'cql_model/query' 8 | require 'cql_model/model' 9 | require 'cql_model/model/dsl' 10 | -------------------------------------------------------------------------------- /cql_model.rconf: -------------------------------------------------------------------------------- 1 | # Configuration settings for RightSite 2 | ruby do 3 | version 'ruby-1.9.3-p392' 4 | end 5 | bundler do 6 | version '1.2.1' 7 | end 8 | cassandra do 9 | version '1.1.10' 10 | end 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | = TODO 2 | 3 | == Features 4 | 5 | * 6 | * Prepared statement support (e.g. for the result of SelectStatement#build) 7 | 8 | == Usability / Correctness /Performance 9 | 10 | * Better solution for using and switching namespaces -- thread/fiber safety 11 | * Better use of cql-rb connection pooling 12 | -------------------------------------------------------------------------------- /lib/cql_model/query/parse_error.rb: -------------------------------------------------------------------------------- 1 | module CQLModel::Query 2 | class ParseError < Exception 3 | end 4 | 5 | # Raised if an insert statement does not specify all the primary keys 6 | # or if an update statement does not specify any key (part of a composite primary key or a primary key) 7 | class MissingKeysError < Exception 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Runtime dependencies of CQLModel. 4 | gem "cql-rb", "~> 1.0.0.pre4" 5 | 6 | # Gems used during test and development of CQLModel. 7 | group :development do 8 | gem "rake", "~> 0.9" 9 | gem "rspec", "~> 2.0" 10 | gem "cucumber", "~> 1.0" 11 | gem "jeweler", "~> 1.8.3" 12 | gem "debugger" 13 | gem "rdoc", ">= 2.4.2" 14 | gem "flexmock", "~> 0.8" 15 | gem "syntax", "~> 1.0.0" #rspec will syntax-highlight code snippets if this gem is available 16 | gem "nokogiri", "~> 1.5" 17 | end 18 | -------------------------------------------------------------------------------- /features/dsl.feature: -------------------------------------------------------------------------------- 1 | Feature: CQL model DSL 2 | In order to define the CQL schema 3 | Developers call class-level DSL methods 4 | So the framework knows about their models 5 | 6 | Background: 7 | Given a CQL model definition: 8 | """ 9 | class Person 10 | include CQLModel::Model 11 | 12 | property :name, String 13 | property :age, Integer 14 | 15 | scope(:not_joe) { name != 'Joe' } 16 | scope(:older_than) { |x| age > x } 17 | end 18 | """ 19 | 20 | Scenario: declare properties 21 | Given a pending cuke 22 | 23 | Scenario: named scope with fixed where-clause 24 | When I call: not_joe 25 | Then it should generate CQL: WHERE name != 'Joe' 26 | 27 | Scenario: named scope with dynamic where-clause 28 | When I call: older_than(33) 29 | Then it should generate CQL: WHERE age > 33 30 | 31 | Scenario: overridden table name 32 | Given a pending cuke 33 | -------------------------------------------------------------------------------- /features/select.feature: -------------------------------------------------------------------------------- 1 | Feature: SELECT statement 2 | In order to build SELECT statements 3 | Developers call class-level DSL methods 4 | So they can read data from Cassandra 5 | 6 | Background: 7 | Given a CQL model definition: 8 | """ 9 | class Widget 10 | include CQLModel::Model 11 | 12 | property :name, String 13 | property :age, Integer 14 | property :price, Float 15 | property :alive, Boolean 16 | property :dead, Boolean 17 | end 18 | """ 19 | 20 | Scenario: select all columns 21 | Given a pending cuke 22 | 23 | Scenario: select some columns 24 | Given a pending cuke 25 | 26 | Scenario: limit results 27 | Given a pending cuke 28 | 29 | Scenario: sort results ascending 30 | Given a pending cuke 31 | 32 | Scenario: sort results descending 33 | Given a pending cuke 34 | 35 | Scenario: specify read consistency 36 | Given a pending cuke 37 | 38 | Scenario: select each row 39 | Given a pending cuke 40 | 41 | Scenario: select each model 42 | Given a pending cuke 43 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | builder (3.1.4) 5 | columnize (0.3.6) 6 | cql-rb (1.0.0.pre4) 7 | cucumber (1.2.1) 8 | builder (>= 2.1.2) 9 | diff-lcs (>= 1.1.3) 10 | gherkin (~> 2.11.0) 11 | json (>= 1.4.6) 12 | debugger (1.5.0) 13 | columnize (>= 0.3.1) 14 | debugger-linecache (~> 1.2.0) 15 | debugger-ruby_core_source (~> 1.2.0) 16 | debugger-linecache (1.2.0) 17 | debugger-ruby_core_source (1.2.0) 18 | diff-lcs (1.1.3) 19 | flexmock (0.9.0) 20 | gherkin (2.11.6) 21 | json (>= 1.7.6) 22 | git (1.2.5) 23 | jeweler (1.8.4) 24 | bundler (~> 1.0) 25 | git (>= 1.2.5) 26 | rake 27 | rdoc 28 | json (1.7.7) 29 | nokogiri (1.5.9) 30 | rake (0.9.6) 31 | rdoc (4.0.1) 32 | json (~> 1.4) 33 | rspec (2.12.0) 34 | rspec-core (~> 2.12.0) 35 | rspec-expectations (~> 2.12.0) 36 | rspec-mocks (~> 2.12.0) 37 | rspec-core (2.12.2) 38 | rspec-expectations (2.12.1) 39 | diff-lcs (~> 1.1.3) 40 | rspec-mocks (2.12.2) 41 | syntax (1.0.0) 42 | 43 | PLATFORMS 44 | ruby 45 | 46 | DEPENDENCIES 47 | cql-rb (~> 1.0.0.pre4) 48 | cucumber (~> 1.0) 49 | debugger 50 | flexmock (~> 0.8) 51 | jeweler (~> 1.8.3) 52 | nokogiri (~> 1.5) 53 | rake (~> 0.9) 54 | rdoc (>= 2.4.2) 55 | rspec (~> 2.0) 56 | syntax (~> 1.0.0) 57 | -------------------------------------------------------------------------------- /features/step_definitions/query.rb: -------------------------------------------------------------------------------- 1 | # Set ENV['CASS_KEYSPACE'], ENV['CASS_HOST'] and ENV['CASS_PORT'] to configure CQL client 2 | # 3 | Given /a CQL model definition:/ do |defn| 4 | # Define the new class in the context of the Cucumber world so its constant will 5 | # be swept away when the test case complete. 6 | @cql_model = instance_eval(defn) 7 | options = {} 8 | options[:port] = ENV['CASS_PORT'] if ENV['CASS_PORT'] 9 | options[:host] = ENV['CASS_HOST'] if ENV['CASS_HOST'] 10 | options[:keyspace] = ENV['CASS_KEYSPACE'] if ENV['CASS_KEYSPACE'] 11 | cql_client = Cql::Client.new(options) 12 | @cql_model.cql_client = cql_client 13 | @cql_model 14 | end 15 | 16 | When /I call: (.*)/ do |ruby| 17 | begin 18 | @call_return = @cql_model.instance_eval(ruby).to_s.strip 19 | rescue Exception => e 20 | @call_error = e 21 | end 22 | end 23 | 24 | Then /it should generate CQL( that includes)?: (.*)/ do |partial, cql| 25 | puts "***ERROR #{@call_error.message.inspect}\n#{@call_error.backtrace.join("\n")}" if @call_error 26 | @call_error.should be_nil 27 | cql.gsub!('', @cql_model.table_name) 28 | @call_return.should =~ /#{Regexp.escape(cql) + (partial ? '.*' : '')};$/ 29 | end 30 | 31 | Then /it should error with: (.*), (.*)/ do |klass, msg| 32 | @call_error.should_not be_nil 33 | @call_error.class.name.should == klass 34 | @call_error.message.should =~ Regexp.new(msg) 35 | end 36 | 37 | -------------------------------------------------------------------------------- /lib/cql_model/query/mutation_statement.rb: -------------------------------------------------------------------------------- 1 | module CQLModel::Query 2 | 3 | # Common parent to InsertStatement and UpdateStatment 4 | # provide helpers for managing common DSL settings 5 | class MutationStatement < Statement 6 | 7 | # Instantiate statement 8 | # 9 | # @param [Class] klass 10 | # @param [CQLModel::Client] CQL client used to execute statement 11 | def initialize(klass, client=nil) 12 | super(klass, client) 13 | @values = nil 14 | @ttl = nil 15 | @timestamp = nil 16 | end 17 | 18 | # DSL for setting TTL value 19 | # 20 | # @param [Fixnum] ttl_value TTL value in seconds 21 | def ttl(ttl_value) 22 | raise ArgumentError, "Cannot specify TTL twice" unless @ttl.nil? 23 | @ttl = ttl_value 24 | self 25 | end 26 | 27 | # DSL for setting timestamp value 28 | # 29 | # @param [Fixnum|String] timestamp_value (number of milliseconds since epoch or ISO 8601 date time value) 30 | def timestamp(timestamp_value) 31 | raise ArgumentError, "Cannot specify timestamp twice" unless @timestamp.nil? 32 | @timestamp = timestamp_value 33 | self 34 | end 35 | 36 | # Execute this statement on the CQL client connection 37 | # INSERT statements do not return a result 38 | # 39 | # @return [true] always returns true 40 | def execute 41 | @client.execute(to_s) 42 | true 43 | end 44 | 45 | end 46 | 47 | end 48 | 49 | -------------------------------------------------------------------------------- /lib/cql_model/query/statement.rb: -------------------------------------------------------------------------------- 1 | module CQLModel::Query 2 | 3 | class Statement 4 | 5 | # Initialize instance variables common to all statements 6 | # 7 | # @param [Class] klass Model class 8 | # @param [CQLModel::Client] client used to connect to Cassandra 9 | def initialize(klass, client) 10 | @klass = klass 11 | @client = client || klass.cql_client 12 | @consistency = nil 13 | end 14 | 15 | def to_s 16 | raise NotImplementedError, "Subclass responsibility" 17 | end 18 | 19 | def execute 20 | raise NotImplementedError, "Subclass responsibility" 21 | end 22 | 23 | # Specify consistency level to use when executing statemnt 24 | # See http://www.datastax.com/docs/1.0/dml/data_consistency 25 | # Defaults to :local_quorum 26 | # 27 | # @param [String] consistency One of 'ANY', 'ONE', 'QUORUM', 'LOCAL_QUORUM', 'EACH_QUORUM', 'ALL' as of Cassandra 1.0 28 | # @return [String] consistency value 29 | def consistency(consist) 30 | raise ArgumentError, "Cannot specify USING CONSISTENCY twice" unless @consistency.nil? 31 | @consistency = consist 32 | self 33 | end 34 | 35 | # CQL query consistency level 36 | # Default to 'LOCAL_QUORUM' 37 | # 38 | # @return [String] CQL query consistency 39 | def statement_consistency 40 | @consistency || 'LOCAL_QUORUM' 41 | end 42 | 43 | alias using_consistency consistency 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # -*-ruby-*- 2 | require 'rubygems' 3 | require 'bundler/setup' 4 | 5 | require 'rake' 6 | require 'rdoc/task' 7 | require 'rubygems/package_task' 8 | 9 | require 'rake/clean' 10 | require 'rspec/core/rake_task' 11 | require 'cucumber/rake/task' 12 | 13 | require 'jeweler' 14 | 15 | desc "Run unit tests" 16 | task :default => :spec 17 | 18 | desc "Run unit tests" 19 | RSpec::Core::RakeTask.new do |t| 20 | t.pattern = Dir['**/*_spec.rb'] 21 | end 22 | 23 | desc "Run functional tests" 24 | Cucumber::Rake::Task.new do |t| 25 | t.cucumber_opts = %w{--color --format pretty} 26 | end 27 | 28 | desc 'Generate documentation for the cql_model gem.' 29 | Rake::RDocTask.new(:rdoc) do |rdoc| 30 | rdoc.rdoc_dir = 'doc' 31 | rdoc.title = 'CQLModel' 32 | rdoc.options << '--line-numbers' << '--inline-source' 33 | rdoc.rdoc_files.include('README.md') 34 | rdoc.rdoc_files.include('lib/**/*.rb') 35 | rdoc.rdoc_files.exclude('features/**/*') 36 | rdoc.rdoc_files.exclude('spec/**/*') 37 | end 38 | 39 | Jeweler::Tasks.new do |gem| 40 | # gem is a Gem::Specification; see http://docs.rubygems.org/read/chapter/20 for more options 41 | gem.name = "cql_model" 42 | gem.required_ruby_version = ">= 1.9.0" 43 | gem.homepage = "https://github.com/xeger/cql_model" 44 | gem.license = "MIT" 45 | gem.summary = %Q{Cassandra CQL model.} 46 | gem.description = %Q{A lightweight, performant OOP wrapper for Cassandra tables; inspired by DataMapper.} 47 | gem.email = "gemspec@tracker.xeger.net" 48 | gem.authors = ['Tony Spataro'] 49 | end 50 | 51 | Jeweler::RubygemsDotOrgTasks.new 52 | 53 | CLEAN.include('pkg') 54 | -------------------------------------------------------------------------------- /features/update.feature: -------------------------------------------------------------------------------- 1 | Feature: UPDATE statement 2 | In order to build UPDATE statements 3 | Developers call class-level DSL methods 4 | So they can update data in Cassandra 5 | 6 | Background: 7 | Given a CQL model definition: 8 | """ 9 | class Timeline 10 | include CQLModel::Model 11 | 12 | property :user_id, Integer 13 | property :tweet_id, Integer 14 | property :text, String 15 | property :counter, Integer 16 | 17 | primary_key :user_id, :tweet_id 18 | end 19 | """ 20 | 21 | Scenario: simple update 22 | When I call: update(:user_id => 42, :tweet_id => 13, :name => 'joe') 23 | Then it should generate CQL: UPDATE USING CONSISTENCY LOCAL_QUORUM SET tweet_id = 13, name = 'joe' WHERE user_id = 42 24 | 25 | Scenario: simple update, first component of composite key to appear in values should be used in WHERE clause 26 | When I call: update(:name => 'joe', :tweet_id => 13, :user_id => 42) 27 | Then it should generate CQL: UPDATE USING CONSISTENCY LOCAL_QUORUM SET name = 'joe', user_id = 42 WHERE tweet_id = 13 28 | 29 | Scenario: update counter column 30 | When I call: update(:user_id => 42, :counter => { :value => 'counter + 2' }) 31 | Then it should generate CQL: UPDATE USING CONSISTENCY LOCAL_QUORUM SET counter = counter + 2 WHERE user_id = 42 32 | 33 | Scenario: update with no primary key should fail and error message shoud list primary key(s) 34 | When I call: update(:name => 'joe') 35 | Then it should error with: CQLModel::Query::MissingKeysError, user_id.*tweet_id 36 | 37 | Scenario Outline: update with options 38 | When I call: 39 | Then it should generate CQL that includes: 40 | Examples: 41 | | ruby | cql | 42 | | update(:user_id => 42, :name => 'joe').consistency('ONE') | USING CONSISTENCY ONE SET | 43 | | update(:user_id => 42, :name => 'joe').timestamp(1366057256324) | USING CONSISTENCY LOCAL_QUORUM AND TIMESTAMP 1366057256324 SET | 44 | | update(:user_id => 42, :name => 'joe').ttl(3600) | USING CONSISTENCY LOCAL_QUORUM AND TTL 3600 SET | 45 | | update(:user_id => 42, :name => 'joe').consistency('ONE').timestamp(1366057256324).ttl(3600) | USING CONSISTENCY ONE AND TIMESTAMP 1366057256324 AND TTL 3600 SET | 46 | -------------------------------------------------------------------------------- /features/insert.feature: -------------------------------------------------------------------------------- 1 | Feature: INSERT statement 2 | In order to build INSERT statements 3 | Developers call class-level DSL methods 4 | So they can insert data into Cassandra 5 | 6 | Background: 7 | Given a CQL model definition: 8 | """ 9 | class Timeline 10 | include CQLModel::Model 11 | 12 | property :user_id, Integer 13 | property :tweet_id, Integer 14 | property :text, String 15 | 16 | primary_key :user_id, :tweet_id 17 | end 18 | """ 19 | 20 | Scenario: simple create 21 | When I call: create(:user_id => 42, :tweet_id => 13, :name => 'joe') 22 | Then it should generate CQL: INSERT INTO (user_id, tweet_id, name) VALUES (42, 13, 'joe') USING CONSISTENCY LOCAL_QUORUM 23 | 24 | Scenario: simple create, keys should always be first in CQL independendly of Ruby's internal hash ordering 25 | When I call: create(:name => 'joe', :tweet_id => 13, :user_id => 42) 26 | Then it should generate CQL: INSERT INTO (user_id, tweet_id, name) VALUES (42, 13, 'joe') USING CONSISTENCY LOCAL_QUORUM 27 | 28 | Scenario Outline: create with missing primary key(s) should fail and error message shoud list missing key(s) 29 | When I call: 30 | Then it should error with: 31 | Examples: 32 | | ruby | error | 33 | | create(:user_id => 42, :name => 'joe') | CQLModel::Query::MissingKeysError, tweet_id | 34 | | create(:tweet_id => 42, :name => 'joe') | CQLModel::Query::MissingKeysError, user_id | 35 | | create(:name => 'joe') | CQLModel::Query::MissingKeysError, user_id.*tweet_id | 36 | 37 | Scenario Outline: create with options 38 | When I call: 39 | Then it should generate CQL: 40 | Examples: 41 | | ruby | cql | 42 | | create(:user_id => 42, :tweet_id => 13, :name => 'joe').consistency('ONE') | USING CONSISTENCY ONE | 43 | | create(:user_id => 42, :tweet_id => 13, :name => 'joe').timestamp(1366057256324) | USING CONSISTENCY LOCAL_QUORUM AND TIMESTAMP 1366057256324 | 44 | | create(:user_id => 42, :tweet_id => 13, :name => 'joe').ttl(3600) | USING CONSISTENCY LOCAL_QUORUM AND TTL 3600 | 45 | | create(:user_id => 42, :tweet_id => 13, :name => 'joe').consistency('ONE').timestamp(1366057256324).ttl(3600) | USING CONSISTENCY ONE AND TIMESTAMP 1366057256324 AND TTL 3600 | 46 | 47 | -------------------------------------------------------------------------------- /lib/cql_model/query/update_statement.rb: -------------------------------------------------------------------------------- 1 | module CQLModel::Query 2 | 3 | # UPDATE statements DSL 4 | # << An UPDATE writes one or more columns to a record in a Cassandra column family. No results are returned. 5 | # Row/column records are created if they do not exist, or overwritten if they do exist >> 6 | # (from http://www.datastax.com/docs/1.1/references/cql/UPDATE) 7 | # 8 | # Note: user a hash with a single key :value to update counter columns using the existing counter value: 9 | # update(:id => 12, :counter => { :value => 'counter + 1' }) 10 | # 11 | # E.g.: 12 | # Model.update(:id => '123', :col => 'value', :counter => { :value => 'counter + 2' }) 13 | # Model.update(:id => ['123', '456'], :col => 'value') 14 | # Model.update(:id => '123', :col => 'value').ttl(3600) 15 | # Model.update(:id => '123', :col => 'value').timestamp(1366057256324) 16 | # Model.update(:id => '123', :col => 'value').timestamp('2013-04-15 13:21:48') 17 | # Model.update(:id => '123', :col => 'value').consistency('ONE') 18 | # Model.update(:id => ['123', '456'], :col => 'value', :counter => 'counter + 2').ttl(3600).timestamp(1366057256324).consistency('ONE') 19 | # 20 | # Can also be used on Model instances, e.g.: 21 | # @model.update(:col => 'value', :counter => 'counter + 2') 22 | # @model.update_all_by('name', :col => 'value') # 'name' must be part of the table composite key 23 | class UpdateStatement < MutationStatement 24 | 25 | # DSL for setting UPDATE values 26 | # 27 | # @param [Hash] values Hash of column values or column update expression indexed by column name 28 | def update(values) 29 | raise ArgumentError, "Cannot specify UPDATE values twice" unless @values.nil? 30 | @values = values 31 | self 32 | end 33 | 34 | # @return [String] a CQL UPDATE statement with suitable constraints and options 35 | def to_s 36 | key = @values.keys.detect { |k| @klass.primary_key.include?(k) } 37 | if key.nil? 38 | raise MissingKeysError.new("No key in UPDATE statement, please use at least one of: #{@klass.primary_key.map(&:inspect).join(', ')}") 39 | end 40 | key_values = @values.delete(key) 41 | s = "UPDATE #{@klass.table_name}" 42 | options = [] 43 | options << "CONSISTENCY #{statement_consistency}" 44 | options << "TIMESTAMP #{@timestamp}" unless @timestamp.nil? 45 | options << "TTL #{@ttl}" unless @ttl.nil? 46 | s << " USING #{options.join(' AND ')}" 47 | s << " SET #{@values.to_a.map { |n, v| "#{n} = #{::CQLModel::Query.cql_value(v)}" }.join(', ')}" 48 | s << " WHERE #{key} " 49 | s << (key_values.is_a?(Array) ? "IN (#{key_values.map { |v| ::CQLModel::Query.cql_value(v) }.join(', ')})" : "= #{::CQLModel::Query.cql_value(key_values)}") 50 | s << ';' 51 | end 52 | 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/cql_model/query/insert_statement.rb: -------------------------------------------------------------------------------- 1 | module CQLModel::Query 2 | 3 | # INSERT statements DSL 4 | # << An INSERT writes one or more columns to a record in a Cassandra column family. No results are returned. 5 | # The first column name in the INSERT list must be the name of the column family key >> 6 | # (from: http://www.datastax.com/docs/1.1/references/cql/INSERT) 7 | # 8 | # Ex: 9 | # Model.create(:key => 'val', :col1 => 'value', :col2 => 42) # Simple insert 10 | # Model.create(:key => 'val', :key2 => 64, :col1 => 'value', :col2 => 42) # Composite keys 11 | # Model.create(:key => 'val', :col => 'value').ttl(3600) # TTL in seconds 12 | # Model.create(:key => 'val', :col => 'value').timestamp(1366057256324) # Milliseconds since epoch timestamp 13 | # Model.create(:key => 'val', :col => 'value').timestamp('2013-04-15 13:21:48') # ISO 8601 timestamp 14 | # Model.create(:key => 'val', :col => 'value').consistency('ONE') # Custom consistency (default is 'LOCAL_QUORUM') 15 | # Model.create(:key => 'val', :col => 'value').ttl(3600).timestamp(1366057256324).consistency('ONE') # Multiple options 16 | class InsertStatement < MutationStatement 17 | 18 | # DSL for setting INSERT values 19 | # 20 | # @param [Hash] values Hash of column values indexed by column name 21 | def create(values) 22 | raise ArgumentError, "Cannot specify INSERT values twice" unless @values.nil? 23 | @values = values 24 | self 25 | end 26 | 27 | # Build CQL statement 28 | # Note: Do not check whether all keys are specified, let cassandra return an error if there is one 29 | # 30 | # @return [String] a CQL INSERT statement with suitable constraints and options 31 | # 32 | # @raise [CQLModel::MissingKeysError] if a primary key is not set in inserted values 33 | def to_s 34 | keys = @klass.primary_key.inject([]) { |h, k| h << [k, @values.delete(k)]; h } 35 | if keys.any? { |k| k[1].nil? } 36 | raise MissingKeysError.new("Missing primary key(s) in INSERT statement: #{keys.select { |k| k[1].nil? }.map(&:first).map(&:inspect).join(', ')}") 37 | end 38 | s = "INSERT INTO #{@klass.table_name} (#{keys.map { |k| k[0] }.join(', ')}, #{@values.keys.join(', ')})" 39 | s << " VALUES (#{keys.map { |k| ::CQLModel::Query.cql_value(k[1]) }.join(', ')}, #{@values.values.map { |v| ::CQLModel::Query.cql_value(v) }.join(', ')})" 40 | options = [] 41 | options << "CONSISTENCY #{statement_consistency}" 42 | options << "TIMESTAMP #{@timestamp}" unless @timestamp.nil? 43 | options << "TTL #{@ttl}" unless @ttl.nil? 44 | s << " USING #{options.join(' AND ')}" 45 | s << ';' 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /features/select_where.feature: -------------------------------------------------------------------------------- 1 | Feature: WHERE constraint 2 | In order to build CQL WHERE constraints 3 | Developers pass blocks that call Ruby methods and operators 4 | So their queries are more idiomatic 5 | 6 | Background: 7 | Given a CQL model definition: 8 | """ 9 | class Widget 10 | include CQLModel::Model 11 | 12 | property :name, String 13 | property :age, Integer 14 | property :price, Float 15 | property :alive, Boolean 16 | property :dead, Boolean 17 | end 18 | """ 19 | 20 | Scenario Outline: equality constraint 21 | When I call: 22 | Then it should generate CQL: 23 | Examples: 24 | | ruby | cql | 25 | | where { name == 'Joe' } | WHERE name = 'Joe' | 26 | | where { age == 35 } | WHERE age = 35 | 27 | | where { price == 29.95 } | WHERE price = 29.95 | 28 | | where { alive == true } | WHERE alive = true | 29 | | where { dead == false } | WHERE dead = false | 30 | 31 | Scenario Outline: non-string column names 32 | When I call: 33 | Then it should generate CQL: 34 | Examples: 35 | | ruby | cql | 36 | | where { int(123) == 'One Two Three' } | WHERE "123" = 'One Two Three' | 37 | | where { float(42.42) == 'meaning' } | WHERE "42.42" = 'meaning' | 38 | | where { timestamp(8976) == true } | WHERE "8976" = true | 39 | | where { varint(123) == 'One Two Three' } | WHERE "123" = 'One Two Three' | 40 | 41 | Scenario Outline: membership constraint 42 | When I call: 43 | Then it should generate CQL: 44 | Examples: 45 | | ruby | cql | 46 | | where { name.in('Tom', 'Fred') } | WHERE name IN ('Tom', 'Fred') | 47 | | where { age.in(33, 34, 35) } | WHERE age IN (33, 34, 35) | 48 | | where { price.in(29.95) } | WHERE price IN (29.95) | 49 | | where { utf8('你好').in(29.95) } | WHERE "你好" IN (29.95) | 50 | 51 | Scenario Outline: inequality constraint 52 | When I call: 53 | Then it should generate CQL: 54 | Examples: 55 | | ruby | cql | 56 | | where { price > 4.95 } | WHERE price > 4.95 | 57 | | where { price < 4.95 } | WHERE price < 4.95 | 58 | | where { name >= 'D' } | WHERE name >= 'D' | 59 | | where { age <= 30 } | WHERE age <= 30 | 60 | | where { name != 'Joe' } | WHERE name != 'Joe' | 61 | 62 | Scenario Outline: compound expressions 63 | When I call: 64 | Then it should generate CQL: 65 | Examples: 66 | | ruby | cql | 67 | | where { name == 'Joe' }.and { age.in(33,34,35) } | WHERE name = 'Joe' AND age IN (33, 34, 35) | 68 | | where { name.in('Tom', 'Fred') }.and { price > 29.95 } | WHERE name IN ('Tom', 'Fred') AND price > 29.95 | 69 | 70 | -------------------------------------------------------------------------------- /lib/cql_model/query.rb: -------------------------------------------------------------------------------- 1 | module CQLModel::Query 2 | # CQL single quote character. 3 | SQ = "'" 4 | 5 | # CQL single-quote escape sequence. 6 | SQSQ = "''" 7 | 8 | # CQL double-quote character. 9 | DQ = '"' 10 | 11 | # CQL double-quote escape. 12 | DQDQ = '""' 13 | 14 | # Valid CQL identifier (can be used as a column name without double-quoting) 15 | IDENTIFIER = /[a-z][a-z0-9_]*/i 16 | 17 | module_function 18 | 19 | # Transform a list of symbols or strings into CQL column names. Performs no safety checks!! 20 | def cql_column_names(list) 21 | if list.empty? 22 | '*' 23 | else 24 | list.join(', ') 25 | end 26 | end 27 | 28 | # Transform a Ruby object into its CQL identifier representation. 29 | # 30 | # @TODO more docs 31 | # 32 | def cql_identifier(value) 33 | # TODO UUID, Time, ... 34 | case value 35 | when Symbol, String 36 | if value =~ IDENTIFIER 37 | value.to_s 38 | else 39 | "#{DQ}#{value.gsub(DQ, DQDQ)}#{DQ}" 40 | end 41 | when Numeric, TrueClass, FalseClass 42 | "#{DQ}#{cql_value(value)}#{DQ}" 43 | else 44 | raise ParseError, "Cannot convert #{value.class} to a CQL identifier" 45 | end 46 | end 47 | 48 | # Transform a Ruby object into its CQL literal value representation. A literal value is anything 49 | # that can appear in a CQL statement as a key or column value (but not column NAME; see 50 | # #cql_identifier to convert values to column names). 51 | # 52 | # @param value [String,Numeric,Boolean, Array] 53 | # @return [String] the CQL equivalent of value 54 | # 55 | # When used as a key or column value, CQL supports the following kinds of literal value: 56 | # * unquoted identifier (treated as a string value) 57 | # * string literal 58 | # * integer number 59 | # * UUID 60 | # * floating-point number 61 | # * boolean true/false 62 | # 63 | # When used as a column name, any value that is not a valid identifier MUST BE ENCLOSED IN 64 | # DOUBLE QUOTES. This method does not handle the double-quote escaping; see #cql_identifier 65 | # for that. 66 | # 67 | # @see #cql_identifier 68 | # @see http://www.datastax.com/docs/1.1/references/cql/cql_lexicon#keywords-and-identifiers 69 | def cql_value(value) 70 | # TODO UUID, Time, ... 71 | case value 72 | when String 73 | "#{SQ}#{value.gsub(SQ, SQSQ)}#{SQ}" 74 | when Numeric, TrueClass, FalseClass 75 | value.to_s 76 | when Hash 77 | # Used by UPDATE statements to refer to counter columns 78 | value = value[:value] 79 | raise ParseError, "Cannot convert #{value.inspect} to a CQL value" unless value.is_a?(String) 80 | value 81 | else 82 | if value.respond_to?(:map) 83 | '(' + value.map { |v| cql_value(v) }.join(', ') + ')' 84 | else 85 | raise ParseError, "Cannot convert #{value.class} to a CQL value" 86 | end 87 | end 88 | end 89 | end 90 | 91 | require 'cql_model/query/parse_error' 92 | require 'cql_model/query/expression' 93 | require 'cql_model/query/statement' 94 | require 'cql_model/query/mutation_statement' 95 | require 'cql_model/query/select_statement' 96 | require 'cql_model/query/insert_statement' 97 | require 'cql_model/query/update_statement' 98 | -------------------------------------------------------------------------------- /cql_model.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "cql_model" 8 | s.version = "0.1.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Tony Spataro"] 12 | s.date = "2013-04-11" 13 | s.description = "A lightweight, performant OOP wrapper for Cassandra tables; inspired by DataMapper." 14 | s.email = "gemspec@tracker.xeger.net" 15 | s.extra_rdoc_files = [ 16 | "README.md" 17 | ] 18 | s.files = [ 19 | ".ruby-version", 20 | "Gemfile", 21 | "Gemfile.lock", 22 | "README.md", 23 | "Rakefile", 24 | "VERSION", 25 | "features/dsl.feature", 26 | "features/select.feature", 27 | "features/select_where.feature", 28 | "features/step_definitions/query.rb", 29 | "features/support/env.rb", 30 | "lib/cql_model.rb", 31 | "lib/cql_model/model.rb", 32 | "lib/cql_model/model/dsl.rb", 33 | "lib/cql_model/query.rb", 34 | "lib/cql_model/query/expression.rb", 35 | "lib/cql_model/query/parse_error.rb", 36 | "lib/cql_model/query/select_statement.rb", 37 | "specs/spec_helper.rb" 38 | ] 39 | s.homepage = "https://github.com/xeger/cql_model" 40 | s.licenses = ["MIT"] 41 | s.require_paths = ["lib"] 42 | s.required_ruby_version = Gem::Requirement.new(">= 1.9.0") 43 | s.rubygems_version = "1.8.23" 44 | s.summary = "Cassandra CQL model." 45 | 46 | if s.respond_to? :specification_version then 47 | s.specification_version = 3 48 | 49 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 50 | s.add_runtime_dependency(%q, ["~> 1.0.0.pre4"]) 51 | s.add_development_dependency(%q, ["~> 0.9"]) 52 | s.add_development_dependency(%q, ["~> 2.0"]) 53 | s.add_development_dependency(%q, ["~> 1.0"]) 54 | s.add_development_dependency(%q, ["~> 1.8.3"]) 55 | s.add_development_dependency(%q, [">= 0"]) 56 | s.add_development_dependency(%q, [">= 2.4.2"]) 57 | s.add_development_dependency(%q, ["~> 0.8"]) 58 | s.add_development_dependency(%q, ["~> 1.0.0"]) 59 | s.add_development_dependency(%q, ["~> 1.5"]) 60 | else 61 | s.add_dependency(%q, ["~> 1.0.0.pre4"]) 62 | s.add_dependency(%q, ["~> 0.9"]) 63 | s.add_dependency(%q, ["~> 2.0"]) 64 | s.add_dependency(%q, ["~> 1.0"]) 65 | s.add_dependency(%q, ["~> 1.8.3"]) 66 | s.add_dependency(%q, [">= 0"]) 67 | s.add_dependency(%q, [">= 2.4.2"]) 68 | s.add_dependency(%q, ["~> 0.8"]) 69 | s.add_dependency(%q, ["~> 1.0.0"]) 70 | s.add_dependency(%q, ["~> 1.5"]) 71 | end 72 | else 73 | s.add_dependency(%q, ["~> 1.0.0.pre4"]) 74 | s.add_dependency(%q, ["~> 0.9"]) 75 | s.add_dependency(%q, ["~> 2.0"]) 76 | s.add_dependency(%q, ["~> 1.0"]) 77 | s.add_dependency(%q, ["~> 1.8.3"]) 78 | s.add_dependency(%q, [">= 0"]) 79 | s.add_dependency(%q, [">= 2.4.2"]) 80 | s.add_dependency(%q, ["~> 0.8"]) 81 | s.add_dependency(%q, ["~> 1.0.0"]) 82 | s.add_dependency(%q, ["~> 1.5"]) 83 | end 84 | end 85 | 86 | -------------------------------------------------------------------------------- /lib/cql_model/model.rb: -------------------------------------------------------------------------------- 1 | module CQLModel::Model 2 | # Type alias for use with the property-declaration DSL. 3 | UUID = Cql::Uuid 4 | 5 | # Type alias for use with the property-declaration DSL. 6 | Boolean = TrueClass 7 | 8 | def self.included(klass) 9 | klass.__send__(:extend, CQLModel::Model::DSL) 10 | end 11 | 12 | # Master client connection shared by every model that doesn't bother to set its own. 13 | # Defaults to a localhost connection with no default keyspace; every query must be 14 | # wrapped in a using_keyspace. 15 | # 16 | # @return [Cql::Client] the current client 17 | def self.cql_client 18 | @cql_client ||= Cql::Client.new 19 | @cql_client.start! unless @cql_client.connected? 20 | @cql_client 21 | end 22 | 23 | # Change the client connection. Will not affect any in-progress queries. 24 | # 25 | # @param [Cql::Client] client 26 | # @return [Cql::Client] the new client 27 | def self.cql_client=(client) 28 | @cql_client = client 29 | end 30 | 31 | # Instantiate a new instance of this model. Do not validate the contents of 32 | # cql_properties; it may contain properties that aren't declared by this model, that have 33 | # a missing CQL column, or an improper name/value for their column type. 34 | # 35 | # @param [Hash] cql_properties typed hash of all properties associated with this model 36 | def initialize(cql_properties=nil) 37 | @cql_properties = cql_properties || {} 38 | end 39 | 40 | # Read a property. Property names are column names, and can therefore take any data type 41 | # that a column name can take (integer, UUID, etc). 42 | # 43 | # @param [Object] name 44 | # @return [Object] the value of the specified column, or nil 45 | def [](name) 46 | @cql_properties[name] 47 | end 48 | 49 | # Start an INSERT CQL statement to update model 50 | # @see CQLModel::Query::InsertStatement 51 | # 52 | # @param [Hash] values Hash of column values indexed by column name, optional 53 | # @return [CQLModel::Query::InsertStatement] a query object to customize (ttl, timestamp etc) or execute 54 | # 55 | # @example 56 | # joe.update(:age => 35).execute 57 | # joe.update.ttl(3600).execute 58 | # joe.update(:age => 36).ttl(7200).consistency('ONE').execute 59 | def update(values={}) 60 | key_vals = self.class.primary_key.inject({}) { |h, k| h[k] = @cql_properties[k]; h } 61 | CQLModel::Query::UpdateStatement.new(self.class).update(values.merge(key_vals)) 62 | end 63 | 64 | # Start an UPDATE CQL statement to update all models with given key 65 | # This can update multiple models if the key is part of a composite key 66 | # Updating all models with given (different) key values can be done using the '.update' class method 67 | # @see CQLModel::Query::UpdateStatement 68 | # 69 | # @param [Symbol|String] key Name of key used to select models to be updated 70 | # @param [Hash] values Hash of column values indexed by column names, optional 71 | # @return [CQLModel::Query::UpdateStatement] a query object to customize (ttl, timestamp etc) or execute 72 | # 73 | # @example 74 | # joe.update_all_by(:name, :age => 25).execute # Set all joe's age to 25 75 | # joe.update_all_by(:name).ttl(3600).execute # Set all joe's TTL to one hour 76 | def update_all_by(key, values={}) 77 | CQLModel::Query::UpdateStatement.new(self.class).update(values.merge({ key => @cql_properties[key.to_s] })) 78 | end 79 | 80 | end 81 | 82 | require 'cql_model/model/dsl' 83 | -------------------------------------------------------------------------------- /lib/cql_model/query/expression.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module CQLModel::Query 4 | # @TODO docs 5 | class Expression < BasicObject 6 | OPERATORS = {:== => '=', 7 | :'!=' => '!=', 8 | :'>' => '>', 9 | :'<' => '<', 10 | :'>=' => '>=', 11 | :'<=' => '<=', 12 | :'in' => 'IN', 13 | }.freeze 14 | 15 | TYPECASTS = [ 16 | :ascii, 17 | :bigint, 18 | :blob, 19 | :boolean, 20 | :counter, 21 | :decimal, 22 | :double, 23 | :float, 24 | :int, 25 | :text, 26 | :timestamp, 27 | :uuid, 28 | :timeuuid, 29 | :varchar, 30 | :varint 31 | ].freeze 32 | 33 | # @TODO docs 34 | def initialize(*params, &block) 35 | @left = nil 36 | @operator = nil 37 | @right = nil 38 | 39 | instance_exec(*params, &block) if block 40 | end 41 | 42 | # @TODO docs 43 | def to_s 44 | __build__ 45 | end 46 | 47 | # @TODO docs 48 | def inspect 49 | __build__ 50 | end 51 | 52 | # This is where the magic happens. Ensure all of our operators are overloaded so they call 53 | # #apply and contribute to the CQL expression that will be built. 54 | OPERATORS.keys.each do |op| 55 | define_method(op) do |*args| 56 | __apply__(op, args) 57 | end 58 | end 59 | 60 | TYPECASTS.each do |func| 61 | define_method(func) do |*args| 62 | __apply__(func, args) 63 | end 64 | end 65 | 66 | # @TODO docs 67 | def method_missing(token, *args) 68 | __apply__(token, args) 69 | end 70 | 71 | private 72 | 73 | # @TODO docs 74 | def __apply__(token, args) 75 | if @left.nil? 76 | if args.empty? 77 | # A well-behaved CQL identifier (column name that is a valid Ruby method name) 78 | @left = token 79 | elsif args.length == 1 80 | # A CQL typecast (column name is an integer, float, etc and must be wrapped in a decorator) 81 | @left = args.first 82 | else 83 | ::Kernel.raise ParseError.new( 84 | "Unacceptable token '#{token}'; expected a CQL identifier or typecast") 85 | end 86 | elsif @operator.nil? 87 | # Looking for an operator + right operand 88 | if OPERATORS.keys.include?(token) 89 | @operator = token 90 | 91 | if (args.size > 1) || (token == :in) 92 | @right = args 93 | else 94 | @right = args.first 95 | end 96 | else 97 | ::Kernel.raise ParseError.new( 98 | "Unacceptable token '#{token}'; expected a CQL-compatible operator") 99 | end 100 | else 101 | ::Kernel.raise ParseError.new( 102 | "Unacceptable token '#{token}'; the expression is " + 103 | "already complete") 104 | end 105 | 106 | self 107 | end 108 | 109 | # @TODO docs 110 | def __build__ 111 | if @left.nil? || @operator.nil? || @right.nil? 112 | ::Kernel.raise ParseError.new( 113 | "Cannot build a CQL expression; the Ruby expression is incomplete " + 114 | "(#{@left.inspect} #{@operator.inspect} #{@right.inspect})") 115 | else 116 | left = ::CQLModel::Query.cql_identifier(@left) 117 | op = OPERATORS[@operator] 118 | right = ::CQLModel::Query.cql_value(@right) 119 | "#{left} #{op} #{right}" 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/cql_model/query/select_statement.rb: -------------------------------------------------------------------------------- 1 | module CQLModel::Query 2 | 3 | # SELECT statements DSL 4 | # << A SELECT expression reads one or more records from a Cassandra column family and returns a result-set of rows. 5 | # Each row consists of a row key and a collection of columns corresponding to the query. >> 6 | # (from: http://www.datastax.com/docs/1.1/references/cql/SELECT) 7 | # 8 | # Ex: 9 | # Model.select(:col1, :col2) 10 | # Model.select(:col1, :col2).where { name == 'Joe' } 11 | # Model.select(:col1, :col2).where { name == 'Joe' }.and { age.in(33,34,35) } 12 | # Model.select(:col1, :col2).where { name == 'Joe' }.and { age.in(33,34,35) }.order_by(:age).desc 13 | class SelectStatement < Statement 14 | 15 | # Instantiate statement 16 | # 17 | # @param [Class] klass Model class 18 | # @param [CQLModel::Client] CQL client used to execute statement 19 | def initialize(klass, client=nil) 20 | super(klass, client) 21 | @columns = nil 22 | @where = [] 23 | @order = '' 24 | @limit = nil 25 | end 26 | 27 | # @TODO docs 28 | def where(*params, &block) 29 | @where << Expression.new(*params, &block) 30 | self 31 | end 32 | 33 | alias and where 34 | 35 | # @TODO docs 36 | def order(*columns) 37 | raise ArgumentError, "Cannot specify ORDER BY twice" unless @order.empty? 38 | @order = Query.cql_column_names(columns) 39 | self 40 | end 41 | 42 | alias order_by order 43 | 44 | # @TODO docs 45 | def asc 46 | raise ArgumentError, "Cannot specify ASC / DESC twice" if @order =~ /ASC|DESC$/ 47 | raise ArgumentError, "Must specify ORDER BY before ASC" if @order.empty? 48 | @order << " ASC" 49 | end 50 | 51 | # @TODO docs 52 | def desc 53 | raise ArgumentError, "Cannot specify ASC / DESC twice" if @order =~ /ASC|DESC$/ 54 | raise ArgumentError, "Must specify ORDER BY before DESC" if @order.empty? 55 | @order << " DESC" 56 | end 57 | 58 | # @TODO docs 59 | def limit(lim) 60 | raise ArgumentError, "Cannot specify LIMIT twice" unless @limit.nil? 61 | @limit = lim 62 | self 63 | end 64 | 65 | # @TODO docs 66 | def select(*columns) 67 | raise ArgumentError, "Cannot specify SELECT column names twice" unless @columns.nil? 68 | @columns = Query.cql_column_names(columns) 69 | self 70 | end 71 | 72 | # @return [String] a CQL SELECT statement with suitable constraints and options 73 | def to_s 74 | s = "SELECT #{@columns || '*'} FROM #{@klass.table_name}" 75 | s << " USING CONSISTENCY " << statement_consistency 76 | unless @where.empty? 77 | s << " WHERE " << @where.map { |w| w.to_s }.join(' AND ') 78 | end 79 | s << " ORDER BY " << @order unless @order.empty? 80 | s << " LIMIT #{@limit}" unless @limit.nil? 81 | s << ';' 82 | end 83 | 84 | # Execute this SELECT statement on the CQL client connection and yield each row of the 85 | # result set as a raw-data Hash. 86 | # 87 | # @yield each row of the result set 88 | # @yieldparam [Hash] row a Ruby Hash representing the column names and values for a given row 89 | # @return [true] always returns true 90 | def execute(&block) 91 | @client.execute(to_s).each_row(&block).size 92 | 93 | true 94 | end 95 | 96 | alias each_row execute 97 | 98 | # Execute this SELECT statement on the CQL client connection and yield each row of the 99 | # as an instance of CQL::Model. 100 | # 101 | # @yield each row of the result set 102 | # @yieldparam [Hash] row a Ruby Hash representing the column names and values for a given row 103 | # @return [true] always returns true 104 | def each(&block) 105 | each_row do |row| 106 | block.call(@klass.new(row)) 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/cql_model/model/dsl.rb: -------------------------------------------------------------------------------- 1 | module CQLModel::Model::DSL 2 | def self.extended(klass) 3 | klass.instance_eval do 4 | @@cql_model_mutex ||= Mutex.new 5 | @@cql_table_name ||= klass.name 6 | @@cql_model_properties ||= {} 7 | @@cql_model_keys ||= [] 8 | end 9 | end 10 | 11 | # @TODO docs 12 | def cql_client 13 | @cql_client || ::CQLModel::Model.cql_client 14 | end 15 | 16 | # @TODO docs 17 | def cql_client=(client) 18 | @cql_client = client 19 | end 20 | 21 | # @TODO docs 22 | def table_name(new_name=nil) 23 | if new_name 24 | @@cql_model_mutex.synchronize do 25 | # Set the table name 26 | @@cql_table_name = new_name 27 | end 28 | else 29 | # Get the table name 30 | @@cql_table_name 31 | end 32 | end 33 | 34 | # @TODO docs 35 | def property(name, type, opts={}) 36 | definition = {} 37 | 38 | # If the user specified the name as a symbol, then they automatically get 39 | # a reader and writer because the property has a predictable, fixed column 40 | # name. 41 | if name.is_a?(Symbol) 42 | definition[:reader] = opts[:reader] || name 43 | definition[:writer] = opts[:writer] || "#{definition[:reader]}=".to_sym 44 | name = name.to_s 45 | end 46 | 47 | @@cql_model_mutex.synchronize do 48 | definition[:type] = type 49 | 50 | if @@cql_model_properties.key?(name) && (@@cql_model_properties[name] != definition) 51 | raise ArgumentError, "Property #{name} is already defined" 52 | end 53 | 54 | unless @@cql_model_properties.key?(name) 55 | @@cql_model_properties[name] = definition 56 | 57 | __send__(:define_method, definition[:reader]) do 58 | self[name] 59 | end if definition[:reader] 60 | 61 | __send__(:define_method, definition[:writer]) do |value| 62 | self[name] = value 63 | end if definition[:writer] 64 | end 65 | end 66 | 67 | self 68 | end 69 | 70 | # Specify or get a primary key or a composite primary key 71 | # 72 | # @param key_vals [Symbol|Array] single key name or composite key names 73 | # 74 | # @return [CQLModel::Model] self 75 | def primary_key(*keys) 76 | if keys.empty? 77 | @@cql_model_keys 78 | else 79 | @@cql_model_mutex.synchronize do 80 | @@cql_model_keys = keys 81 | end 82 | self 83 | end 84 | end 85 | 86 | # @TODO docs 87 | def scope(name, &block) 88 | @@cql_model_mutex.synchronize do 89 | eigenclass = class < 0 116 | # Dynamic WHERE clause (that contains runtime replacement parameters) 117 | CQLModel::Query::SelectStatement.new(self).where(*params, &block) 118 | else 119 | # Static WHERE clause 120 | CQLModel::Query::SelectStatement.new(self).where(*params, &block) 121 | end 122 | end 123 | 124 | # Begin a CQL INSERT statement. 125 | # @see CQLModel::Query::InsertStatement 126 | # 127 | # @param [Hash] values Hash of column values indexed by column name 128 | # @return [CQLModel::Query::InsertStatement] a query object to customize (timestamp, ttl, etc) or execute 129 | # 130 | # @example 131 | # Person.create(:name => 'Joe', :age => 25).ttl(3600).execute 132 | def create(values) 133 | CQLModel::Query::InsertStatement.new(self).create(values) 134 | end 135 | 136 | # Start an UPDATE CQL statement 137 | # The method #keys must be called on the result before #execute 138 | # @see CQLModel::Query::UpdateStatement 139 | # 140 | # @param [Hash] values Hash of column values indexed by column name, optional 141 | # @return [CQLModel::Query::UpdateStatement] a query object to customize (keys, ttl, timestamp etc) then execute 142 | # 143 | # @example 144 | # Person.update(:updated_at => Time.now.utc).keys(:name => ['joe', 'john', 'jane']) 145 | # Person.update.ttl(3600).keys(:name => 'joe') 146 | def update(values={}) 147 | CQLModel::Query::UpdateStatement.new(self).update(values) 148 | end 149 | 150 | # @TODO docs 151 | def each_row(&block) 152 | CQLModel::Query::SelectStatement.new(self).each_row(&block) 153 | end 154 | 155 | # @TODO docs 156 | def each(&block) 157 | CQLModel::Query::SelectStatement.new(self).each(&block) 158 | end 159 | end 160 | --------------------------------------------------------------------------------