datastax_rails 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.rdoc CHANGED
@@ -67,5 +67,10 @@ attributes on any model. DSR will only upload schema files if they have changed
67
67
 
68
68
  === Known issues
69
69
 
70
- Calling :find on a model with a bogus ID returns an empty model instead of RecordNotFound. This is due to a bug
71
- in DSE that is supposedly fixed in the upcoming 2.2 release.
70
+ Calling +find+ on a model with a bogus ID returns an empty model instead of RecordNotFound. This is due to a bug
71
+ in DSE that is supposedly fixed in the upcoming 2.2 release.
72
+
73
+ === More information
74
+
75
+ The documentation for DatastaxRails::Base and DatastaxRails::SearchMethods will give you quite a few examples
76
+ of things you can do. You can find a copy of the latest documentation on line at http://rdoc.info/github/jasonmk/datastax_rails/master/frames.
data/Rakefile CHANGED
@@ -16,6 +16,7 @@ RDoc::Task.new(:rdoc) do |rdoc|
16
16
  rdoc.rdoc_dir = 'rdoc'
17
17
  rdoc.title = 'DatastaxRails'
18
18
  rdoc.options << '--line-numbers'
19
+ rdoc.options << '--main=README.rdoc'
19
20
  rdoc.rdoc_files.include('README.rdoc')
20
21
  rdoc.rdoc_files.include('lib/**/*.rb')
21
22
  end
@@ -67,4 +67,5 @@
67
67
  <% @copy_fields.each do |field| %>
68
68
  <copyField source="<%= field[:source] %>" dest="<%= field[:dest]%>"/>
69
69
  <% end %>
70
+
70
71
  </schema>
@@ -133,6 +133,32 @@ module DatastaxRails #:nodoc:
133
133
  # user.name = "David"
134
134
  # user.occupation = "Code Artist"
135
135
  #
136
+ # == Consistency
137
+ #
138
+ # Cassandra has a concept of consistency levels when it comes to saving records. For a
139
+ # detailed discussion on Cassandra data consistency, see:
140
+ # http://www.datastax.com/docs/1.0/dml/data_consistency
141
+ #
142
+ # DatastaxRails allows you to specify the consistency when you save and retrieve objects.
143
+ #
144
+ # user = User.new(:name => 'David')
145
+ # user.save(:consistency => 'ALL')
146
+ #
147
+ # User.create(params[:user], {:consistency => :local_quorum})
148
+ #
149
+ # User.consistency(:local_quorum).where(:name => 'David')
150
+ #
151
+ # The default consistency level in DatastaxRails is QUORUM for writes and for retrieval
152
+ # by ID. SOLR only supports a consistency level of ONE. See the documentation for
153
+ # SearchMethods#consistency for a more detailed explanation.
154
+ #
155
+ # The overall default consistency for a given model can be overridden by adding a method
156
+ # to the model like so:
157
+ #
158
+ # def default_consistency
159
+ # :local_quorum
160
+ # end
161
+ #
136
162
  # == Conditions
137
163
  #
138
164
  # Conditions are specified as a hash representing key/value pairs that will eventually be passed to SOLR or as
@@ -265,7 +291,6 @@ module DatastaxRails #:nodoc:
265
291
  include ActiveModel::MassAssignmentSecurity
266
292
 
267
293
  include Connection
268
- include Consistency
269
294
  include Identity
270
295
  include FinderMethods
271
296
  include Batches
@@ -281,7 +306,6 @@ module DatastaxRails #:nodoc:
281
306
  include Timestamps
282
307
  include Serialization
283
308
  include Migrations
284
- # include Mocking
285
309
 
286
310
  # Stores the default scope for the class
287
311
  class_attribute :default_scopes, :instance_writer => false
@@ -403,6 +427,10 @@ module DatastaxRails #:nodoc:
403
427
  self.class.attribute_names
404
428
  end
405
429
 
430
+ def valid_consistency?(level) #:nodoc:
431
+ self.class.validate_consistency(level.to_s.upcase)
432
+ end
433
+
406
434
  private
407
435
  def populate_with_current_scope_attributes
408
436
  return unless self.class.scope_attributes?
@@ -486,6 +514,10 @@ module DatastaxRails #:nodoc:
486
514
  search.raw_results.map { |result| result.primary_key }
487
515
  end
488
516
 
517
+ def valid_consistency?(level) #:nodoc:
518
+ DatastaxRails::Cql::Consistency::VALID_CONSISTENCY_LEVELS.include?(level)
519
+ end
520
+
489
521
  protected
490
522
 
491
523
 
@@ -17,7 +17,7 @@ module DatastaxRails
17
17
  define_model_callbacks :save, :create, :update, :destroy
18
18
  end
19
19
 
20
- def destroy #:nodoc:
20
+ def destroy(*) #:nodoc:
21
21
  _run_destroy_callbacks { super }
22
22
  end
23
23
 
@@ -1,10 +1,19 @@
1
1
  module DatastaxRails
2
2
  module Cql
3
3
  class Base
4
- def to_cql #:nodoc:
5
- nil
4
+ # Base initialize that sets the default consistency.
5
+ def initialize(klass, *args)
6
+ @consistency = klass.respond_to?(:default_consistency) ? klass.default_consistency.to_s.upcase : DatastaxRails::Cql::Consistency::QUORUM
7
+ end
8
+
9
+ # Abstract. Should be overridden by subclasses
10
+ def to_cql
11
+ raise NotImplementedError
6
12
  end
7
13
 
14
+ # Generates the CQL and calls Cassandra to execute it.
15
+ # If you are using this outside of Rails, then DatastaxRails::Base.connection must have
16
+ # already been set up (Rails does this for you).
8
17
  def execute
9
18
  cql = self.to_cql
10
19
  Rails.logger.debug(cql)
@@ -1,13 +1,14 @@
1
1
  module DatastaxRails
2
2
  module Cql
3
3
  module Consistency
4
+ ANY = 'ANY'
4
5
  ONE = 'ONE'
5
6
  QUORUM = 'QUORUM'
6
7
  LOCAL_QUORUM = 'LOCAL_QUORUM'
7
8
  EACH_QUORUM = 'EACH_QUORUM'
8
9
  ALL = 'ALL'
9
10
 
10
- VALID_CONSISTENCY_LEVELS = [ONE, QUORUM, LOCAL_QUORUM, EACH_QUORUM, ALL]
11
+ VALID_CONSISTENCY_LEVELS = [ANY, ONE, QUORUM, LOCAL_QUORUM, EACH_QUORUM, ALL]
11
12
  end
12
13
  end
13
14
  end
@@ -4,9 +4,9 @@ module DatastaxRails
4
4
  def initialize(klass, keys)
5
5
  @klass = klass
6
6
  @keys = keys
7
- @consistency = DatastaxRails::Cql::Consistency::QUORUM
8
7
  @timestamp = nil
9
8
  @columns = []
9
+ super
10
10
  end
11
11
 
12
12
  def using(consistency)
@@ -3,10 +3,10 @@ module DatastaxRails
3
3
  class Insert < Base
4
4
  def initialize(klass)
5
5
  @klass = klass
6
- @consistency = DatastaxRails::Cql::Consistency::QUORUM
7
6
  @ttl = nil
8
7
  @timestamp = nil
9
8
  @columns = {}
9
+ super
10
10
  end
11
11
 
12
12
  def using(consistency)
@@ -4,9 +4,9 @@ module DatastaxRails#:nodoc:
4
4
  def initialize(klass, select)
5
5
  @klass = klass
6
6
  @select = select.join(",")
7
- @consistency = DatastaxRails::Cql::Consistency::QUORUM
8
7
  @limit = nil
9
8
  @conditions = {}
9
+ super
10
10
  end
11
11
 
12
12
  def using(consistency)
@@ -3,6 +3,7 @@ module DatastaxRails
3
3
  class Truncate < Base
4
4
  def initialize(klass)
5
5
  @klass = klass
6
+ super
6
7
  end
7
8
 
8
9
  def to_cql
@@ -4,8 +4,8 @@ module DatastaxRails
4
4
  def initialize(klass, key)
5
5
  @klass = klass
6
6
  @key = key
7
- @consistency = DatastaxRails::Cql::Consistency::QUORUM
8
7
  @columns = {}
8
+ super
9
9
  end
10
10
 
11
11
  def using(consistency)
@@ -34,6 +34,7 @@ module DatastaxRails
34
34
  end
35
35
 
36
36
  def to_cql
37
+ columns = @columns.dup
37
38
  values = []
38
39
 
39
40
  stmt = "update #{@klass.column_family} using consistency #{@consistency} "
@@ -46,19 +47,21 @@ module DatastaxRails
46
47
  stmt << "AND TIMESTAMP #{@timestamp}"
47
48
  end
48
49
 
49
- stmt << "SET "
50
-
51
- first_entry = @columns.shift
52
-
53
- stmt << "#{first_entry.first.to_s} = ? "
54
- values << first_entry.last
55
-
56
- @columns.each do |k,v|
57
- stmt << ", #{k.to_s} = ? "
58
- values << v
50
+ unless columns.empty?
51
+ stmt << "SET "
52
+
53
+ first_entry = columns.shift
54
+
55
+ stmt << "#{first_entry.first.to_s} = ?"
56
+ values << first_entry.last
57
+
58
+ columns.each do |k,v|
59
+ stmt << ", #{k.to_s} = ?"
60
+ values << v
61
+ end
59
62
  end
60
63
 
61
- stmt << "WHERE KEY IN (?)"
64
+ stmt << " WHERE KEY IN (?)"
62
65
  values << @key
63
66
 
64
67
  CassandraCQL::Statement.sanitize(stmt, values)
@@ -1,16 +1,16 @@
1
1
  module DatastaxRails
2
- class DatastaxRailsError < StandardError
2
+ class DatastaxRailsError < StandardError #:nodoc:
3
3
  end
4
4
 
5
- class AssociationTypeMismatch < DatastaxRailsError
5
+ class AssociationTypeMismatch < DatastaxRailsError #:nodoc:
6
6
  end
7
7
 
8
- class RecordNotSaved < DatastaxRailsError
8
+ class RecordNotSaved < DatastaxRailsError #:nodoc:
9
9
  end
10
10
 
11
- class DeleteRestrictionError < DatastaxRailsError
11
+ class DeleteRestrictionError < DatastaxRailsError #:nodoc:
12
12
  end
13
13
 
14
- class RecordNotFound < DatastaxRailsError
14
+ class RecordNotFound < DatastaxRailsError #:nodoc:
15
15
  end
16
16
  end
@@ -9,8 +9,21 @@ module DatastaxRails
9
9
  module ClassMethods
10
10
  # Removes one or more records with corresponding keys
11
11
  def remove(*keys)
12
+ options = {}
13
+ if keys.last.is_a?(Hash)
14
+ options = keys.pop
15
+ end
12
16
  ActiveSupport::Notifications.instrument("remove.datastax_rails", :column_family => column_family, :key => key) do
13
- cql.delete(keys).using(thrift_write_consistency).execute
17
+ c = cql.delete(keys)
18
+ if(options[:consistency])
19
+ level = options[:consistency].to_s.upcase
20
+ if(valid_consistency?(level))
21
+ c.using(level)
22
+ else
23
+ raise ArgumentError, "'#{level}' is not a valid Cassandra consistency level"
24
+ end
25
+ end
26
+ c.execute
14
27
  end
15
28
  end
16
29
 
@@ -28,11 +41,20 @@ module DatastaxRails
28
41
  end
29
42
  end
30
43
 
31
- def write(key, attributes, schema_version)
44
+ def write(key, attributes, options = {})
32
45
  key.tap do |key|
33
- attributes = encode_attributes(attributes, schema_version)
46
+ attributes = encode_attributes(attributes, options[:schema_version])
34
47
  ActiveSupport::Notifications.instrument("insert.datastax_rails", :column_family => column_family, :key => key, :attributes => attributes) do
35
- cql.update(key.to_s).columns(attributes).using(thrift_write_consistency).execute
48
+ c = cql.update(key.to_s).columns(attributes)
49
+ if(options[:consistency])
50
+ level = options[:consistency].to_s.upcase
51
+ if(valid_consistency?(level))
52
+ c.using(options[:consistency])
53
+ else
54
+ raise ArgumentError, "'#{level}' is not a valid Cassandra consistency level"
55
+ end
56
+ end
57
+ c.execute
36
58
  end
37
59
  end
38
60
  end
@@ -89,8 +111,8 @@ module DatastaxRails
89
111
  create_or_update(options) || raise(RecordNotSaved)
90
112
  end
91
113
 
92
- def destroy
93
- self.class.remove(key)
114
+ def destroy(options = {})
115
+ self.class.remove(key, options)
94
116
  @destroyed = true
95
117
  freeze
96
118
  end
@@ -134,7 +156,7 @@ module DatastaxRails
134
156
 
135
157
  def write(options) #:nodoc:
136
158
  changed_attributes = changed.inject({}) { |h, n| h[n] = read_attribute(n); h }
137
- self.class.write(key, changed_attributes, schema_version)
159
+ self.class.write(key, changed_attributes, options.merge(:schema_version => schema_version))
138
160
  end
139
161
  end
140
162
  end
@@ -1,8 +1,8 @@
1
1
  require 'active_support/core_ext/class/attribute'
2
2
  require 'active_support/core_ext/object/inclusion'
3
3
 
4
- # This is shamelessly ripped from Active Record 3.1
5
4
  module DatastaxRails
5
+ # This is shamelessly ripped from Active Record 3.1
6
6
  # = DatastaxRails Reflection
7
7
  module Reflection # :nodoc:
8
8
  extend ActiveSupport::Concern
@@ -1,6 +1,31 @@
1
1
  module DatastaxRails
2
2
  module SearchMethods
3
3
 
4
+ # The default consistency level for DSR is QUORUM when searching by ID.
5
+ # For all searches using SOLR, the default consistency is ONE. Use this
6
+ # to override it in either case.
7
+ #
8
+ # Model.consistency(:local_quorum).find("12345")
9
+ #
10
+ # Note that Solr searches (basically anything but find by id) don't allow you
11
+ # to specify the consistency level. DSR sort of gets around this by taking the
12
+ # search results and then going to Cassandra to retrieve the objects by ID using
13
+ # the consistency you specified. However, it is possible that you might not get
14
+ # all of the records you are expecting if the SOLR node you were talking to hasn't
15
+ # been updated yet with the results. In practice, this should not happen for
16
+ # records that were created over your connection, but it is possible for other
17
+ # connections to create records that you can't see yet.
18
+ def consistency(level)
19
+ level = level.to_s.upcase
20
+ unless self.valid_consistency?(level)
21
+ raise ArgumentError, "'#{level}' is not a valid Cassandra consistency level"
22
+ end
23
+
24
+ clone.tap do |r|
25
+ r.consistency_value = level
26
+ end
27
+ end
28
+
4
29
  # Normally special characters (other than wild cards) are escaped before the search
5
30
  # is submitted. If you want to handle escaping yourself because you need to use
6
31
  # those special characters, then just include this in your chain.
@@ -36,7 +36,6 @@ module DatastaxRails
36
36
  @per_page_value = @klass.default_page_size
37
37
  @page_value = 1
38
38
  @use_solr_value = true
39
- @consistency_value = "QUORUM"
40
39
  @extensions = []
41
40
  @create_with_value = {}
42
41
  @escape_value = true
@@ -241,6 +240,9 @@ module DatastaxRails
241
240
  limit(1).select(:id).to_a.total_entries
242
241
  end
243
242
 
243
+ # Escapes values that might otherwise mess up the URL or confuse SOLR.
244
+ # If you want to handle escaping yourself for a particular query then
245
+ # SearchMethods#dont_escape is what you're looking for.
244
246
  def solr_escape(str)
245
247
  if str.is_a?(String) && escape_value
246
248
  str.gsub(SOLR_CHAR_RX, '\\\\\1')
@@ -317,9 +319,17 @@ module DatastaxRails
317
319
  response = rsolr.paginate(@page_value, @per_page_value, 'select', :params => params)["response"]
318
320
  results = DatastaxRails::Collection.new
319
321
  results.total_entries = response['numFound'].to_i
320
- response['docs'].each do |doc|
321
- key = doc.delete('id')
322
- results << @klass.instantiate(key,doc)
322
+ if @consistency_value
323
+ response['docs'].each do |doc|
324
+ id = doc['id']
325
+ obj = @klass.with_cassandra.consistency(@consistency_value).find_by_id(id)
326
+ results << obj if obj
327
+ end
328
+ else
329
+ response['docs'].each do |doc|
330
+ key = doc.delete('id')
331
+ results << @klass.instantiate(key,doc)
332
+ end
323
333
  end
324
334
  results
325
335
  end
@@ -1,3 +1,4 @@
1
1
  module DatastaxRails
2
- VERSION = "1.0.6"
2
+ # The current version of the gem
3
+ VERSION = "1.0.8"
3
4
  end
@@ -1,6 +1,7 @@
1
1
  require 'active_support/all'
2
2
  require 'cassandra-cql/1.0'
3
3
 
4
+ # Welcome to DatastaxRails. DatastaxRails::Base is probably a good place to start.
4
5
  module DatastaxRails
5
6
  extend ActiveSupport::Autoload
6
7
 
@@ -11,12 +12,10 @@ module DatastaxRails
11
12
  autoload :Callbacks
12
13
  autoload :Collection
13
14
  autoload :Connection
14
- autoload :Consistency
15
15
  autoload :Cql
16
16
  autoload :Cursor
17
17
  autoload :Identity
18
18
  autoload :Migrations
19
- #autoload :Mocking
20
19
  autoload :Persistence
21
20
  autoload :Reflection
22
21
  autoload :Relation
@@ -69,8 +68,9 @@ module DatastaxRails
69
68
  end
70
69
  end
71
70
 
72
- # Fixup the thrift library
73
71
  require "thrift"
72
+ # Thrift is how we communicate with Cassandra. We need to do a little fixup
73
+ # work to handle UTF-8 properly in Ruby 1.8.6.
74
74
  module Thrift
75
75
  class BinaryProtocol
76
76
  def write_string(str)
@@ -82,6 +82,5 @@ end
82
82
 
83
83
  require 'datastax_rails/railtie' if defined?(Rails)
84
84
  require 'datastax_rails/errors'
85
- # require 'solr_no_escape'
86
85
 
87
86
  ActiveSupport.run_load_hooks(:datastax_rails, DatastaxRails::Base)
@@ -12,4 +12,10 @@ describe DatastaxRails::Base do
12
12
  p.save!
13
13
  p.instance_variable_get(:@after_save_ran).should == "yup"
14
14
  end
15
+
16
+ it "should raise RecordNotFound when finding a bogus ID" do
17
+ pending "Datastax Enterprise 2.2 should fix this" do
18
+ lambda { Person.find("xyzzy") }.should raise_exception(DatastaxRails::RecordNotFound)
19
+ end
20
+ end
15
21
  end
@@ -4,9 +4,12 @@ describe DatastaxRails::Cql::Select do
4
4
  before(:each) do
5
5
  @model_class = mock("Model Class", :column_family => 'users')
6
6
  end
7
+
7
8
  it "should generate valid CQL" do
8
9
  cql = DatastaxRails::Cql::Select.new(@model_class, ["*"])
9
10
  cql.using(DatastaxRails::Cql::Consistency::QUORUM).conditions(:key => '12345').limit(1)
10
11
  cql.to_cql.should == "SELECT * FROM users USING CONSISTENCY QUORUM WHERE key = '12345' LIMIT 1"
11
12
  end
13
+
14
+ it_has_behavior "default_consistency"
12
15
  end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe DatastaxRails::Cql::Update do
4
+ before(:each) do
5
+ @model_class = mock("Model Class", :column_family => 'users')
6
+ end
7
+
8
+ it "should generate valid CQL" do
9
+ cql = DatastaxRails::Cql::Update.new(@model_class, "12345")
10
+ cql.using(DatastaxRails::Cql::Consistency::QUORUM).columns(:name => 'John', :age => '23')
11
+ cql.to_cql.should == "update users using consistency QUORUM SET age = '23', name = 'John' WHERE KEY IN ('12345')"
12
+ end
13
+
14
+ it_has_behavior "default_consistency"
15
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe "DatastaxRails::Base" do
4
+ describe "persistence" do
5
+ describe "#create" do
6
+ it "should persist at the given consistency level" do
7
+ DatastaxRails::Base.connection.stub(:execute_cql_query)
8
+ DatastaxRails::Base.connection.should_receive(:execute_cql_query).with(/USING CONSISTENCY LOCAL_QUORUM/i).and_return(true)
9
+ Person.create({:name => 'Steven'},{:consistency => 'LOCAL_QUORUM'})
10
+ end
11
+ end
12
+
13
+ describe "#save" do
14
+ it "should persist at the given consistency level" do
15
+ DatastaxRails::Base.connection.stub(:execute_cql_query)
16
+ DatastaxRails::Base.connection.should_receive(:execute_cql_query).with(/USING CONSISTENCY LOCAL_QUORUM/i).and_return(true)
17
+ p=Person.new(:name => 'Steven')
18
+ p.save(:consistency => 'LOCAL_QUORUM')
19
+ end
20
+ end
21
+
22
+ describe "#remove" do
23
+ it "should remove at the given consistency level" do
24
+ p=Person.create(:name => 'Steven')
25
+ DatastaxRails::Base.connection.stub(:execute_cql_query)
26
+ DatastaxRails::Base.connection.should_receive(:execute_cql_query).with(/USING CONSISTENCY LOCAL_QUORUM/i).and_return(true)
27
+ p.destroy(:consistency => :local_quorum)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -5,6 +5,24 @@ describe DatastaxRails::Relation do
5
5
  @relation = DatastaxRails::Relation.new(Hobby, "hobbies")
6
6
  end
7
7
 
8
+ describe "#consistency" do
9
+ it "should throw an ArgumentError for invalid consistency levels" do
10
+ lambda { @relation.consistency(:foo) }.should raise_exception(ArgumentError)
11
+ end
12
+
13
+ it "should not raise an exception for a valid consistency level" do
14
+ lambda { @relation.consistency(:local_quorum) }.should_not raise_exception
15
+ end
16
+
17
+ it "should call cassandra to enforce consistency" do
18
+ h=Hobby.create(:name => 'swimming')
19
+ Hobby.commit_solr
20
+ Hobby.stub_chain(:with_cassandra,:consistency).and_return(@relation)
21
+ @relation.should_receive(:find_by_id).with(h.id)
22
+ @relation.consistency(:all).where(:name => 'swimming').all
23
+ end
24
+ end
25
+
8
26
  describe "#limit" do
9
27
  it "should limit the page size" do
10
28
  "a".upto("l") do |letter|