datastax_rails 1.2.3 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README.rdoc +20 -8
  4. data/config/schema.xml.erb +22 -19
  5. data/config/solrconfig.xml.erb +1 -1
  6. data/lib/cql-rb_extensions.rb +27 -0
  7. data/lib/datastax_rails.rb +13 -17
  8. data/lib/datastax_rails/associations/association.rb +1 -4
  9. data/lib/datastax_rails/associations/collection_proxy.rb +0 -13
  10. data/lib/datastax_rails/attribute_assignment.rb +28 -91
  11. data/lib/datastax_rails/attribute_methods.rb +109 -44
  12. data/lib/datastax_rails/attribute_methods/before_type_cast.rb +71 -0
  13. data/lib/datastax_rails/attribute_methods/dirty.rb +52 -11
  14. data/lib/datastax_rails/attribute_methods/primary_key.rb +87 -0
  15. data/lib/datastax_rails/attribute_methods/read.rb +120 -0
  16. data/lib/datastax_rails/attribute_methods/typecasting.rb +52 -21
  17. data/lib/datastax_rails/attribute_methods/write.rb +59 -0
  18. data/lib/datastax_rails/base.rb +227 -236
  19. data/lib/datastax_rails/cassandra_only_model.rb +25 -19
  20. data/lib/datastax_rails/column.rb +384 -0
  21. data/lib/datastax_rails/connection.rb +12 -13
  22. data/lib/datastax_rails/cql/alter_column_family.rb +0 -1
  23. data/lib/datastax_rails/cql/base.rb +15 -3
  24. data/lib/datastax_rails/cql/column_family.rb +2 -2
  25. data/lib/datastax_rails/cql/create_column_family.rb +7 -18
  26. data/lib/datastax_rails/cql/delete.rb +4 -9
  27. data/lib/datastax_rails/cql/insert.rb +2 -8
  28. data/lib/datastax_rails/cql/select.rb +4 -4
  29. data/lib/datastax_rails/cql/update.rb +8 -17
  30. data/lib/datastax_rails/dynamic_model.rb +98 -0
  31. data/lib/datastax_rails/payload_model.rb +19 -31
  32. data/lib/datastax_rails/persistence.rb +39 -54
  33. data/lib/datastax_rails/railtie.rb +1 -0
  34. data/lib/datastax_rails/reflection.rb +1 -1
  35. data/lib/datastax_rails/relation.rb +20 -20
  36. data/lib/datastax_rails/relation/batches.rb +18 -16
  37. data/lib/datastax_rails/relation/facet_methods.rb +1 -1
  38. data/lib/datastax_rails/relation/finder_methods.rb +6 -10
  39. data/lib/datastax_rails/relation/search_methods.rb +62 -48
  40. data/lib/datastax_rails/rsolr_client_wrapper.rb +1 -1
  41. data/lib/datastax_rails/schema/cassandra.rb +34 -62
  42. data/lib/datastax_rails/schema/migrator.rb +9 -24
  43. data/lib/datastax_rails/schema/solr.rb +13 -30
  44. data/lib/datastax_rails/schema_cache.rb +67 -0
  45. data/lib/datastax_rails/timestamps.rb +84 -11
  46. data/lib/datastax_rails/types/dirty_collection.rb +88 -0
  47. data/lib/datastax_rails/types/dynamic_list.rb +14 -0
  48. data/lib/datastax_rails/types/dynamic_map.rb +32 -0
  49. data/lib/datastax_rails/types/dynamic_set.rb +10 -0
  50. data/lib/datastax_rails/util/solr_repair.rb +4 -5
  51. data/lib/datastax_rails/validations.rb +6 -12
  52. data/lib/datastax_rails/validations/uniqueness.rb +0 -4
  53. data/lib/datastax_rails/version.rb +1 -1
  54. data/lib/datastax_rails/wide_storage_model.rb +13 -29
  55. data/lib/schema_migration.rb +4 -0
  56. data/spec/datastax_rails/associations_spec.rb +0 -1
  57. data/spec/datastax_rails/attribute_methods_spec.rb +9 -6
  58. data/spec/datastax_rails/base_spec.rb +26 -0
  59. data/spec/datastax_rails/column_spec.rb +238 -0
  60. data/spec/datastax_rails/cql/select_spec.rb +1 -1
  61. data/spec/datastax_rails/cql/update_spec.rb +2 -2
  62. data/spec/datastax_rails/persistence_spec.rb +29 -15
  63. data/spec/datastax_rails/relation/batches_spec.rb +5 -5
  64. data/spec/datastax_rails/relation/finder_methods_spec.rb +0 -20
  65. data/spec/datastax_rails/relation/search_methods_spec.rb +8 -0
  66. data/spec/datastax_rails/relation_spec.rb +7 -0
  67. data/spec/datastax_rails/schema/migrator_spec.rb +5 -10
  68. data/spec/datastax_rails/schema/solr_spec.rb +1 -1
  69. data/spec/datastax_rails/types/dynamic_list_spec.rb +20 -0
  70. data/spec/datastax_rails/types/dynamic_map_spec.rb +22 -0
  71. data/spec/datastax_rails/types/dynamic_set_spec.rb +16 -0
  72. data/spec/dummy/config/application.rb +2 -1
  73. data/spec/dummy/config/datastax.yml +6 -3
  74. data/spec/dummy/config/environments/development.rb +4 -5
  75. data/spec/dummy/config/environments/test.rb +0 -5
  76. data/spec/dummy/log/development.log +18 -0
  77. data/spec/dummy/log/test.log +36 -0
  78. data/spec/feature/dynamic_fields_spec.rb +9 -0
  79. data/spec/feature/overloaded_tables_spec.rb +24 -0
  80. data/spec/spec_helper.rb +1 -1
  81. data/spec/support/default_consistency_shared_examples.rb +2 -2
  82. data/spec/support/models.rb +28 -14
  83. metadata +212 -188
  84. data/lib/datastax_rails/identity.rb +0 -64
  85. data/lib/datastax_rails/identity/abstract_key_factory.rb +0 -29
  86. data/lib/datastax_rails/identity/custom_key_factory.rb +0 -37
  87. data/lib/datastax_rails/identity/hashed_natural_key_factory.rb +0 -10
  88. data/lib/datastax_rails/identity/natural_key_factory.rb +0 -39
  89. data/lib/datastax_rails/identity/uuid_key_factory.rb +0 -27
  90. data/lib/datastax_rails/type.rb +0 -16
  91. data/lib/datastax_rails/types.rb +0 -9
  92. data/lib/datastax_rails/types/array_type.rb +0 -86
  93. data/lib/datastax_rails/types/base_type.rb +0 -42
  94. data/lib/datastax_rails/types/binary_type.rb +0 -19
  95. data/lib/datastax_rails/types/boolean_type.rb +0 -22
  96. data/lib/datastax_rails/types/date_type.rb +0 -23
  97. data/lib/datastax_rails/types/float_type.rb +0 -18
  98. data/lib/datastax_rails/types/integer_type.rb +0 -18
  99. data/lib/datastax_rails/types/string_type.rb +0 -16
  100. data/lib/datastax_rails/types/text_type.rb +0 -15
  101. data/lib/datastax_rails/types/time_type.rb +0 -23
  102. data/spec/datastax_rails/types/float_type_spec.rb +0 -31
  103. data/spec/datastax_rails/types/integer_type_spec.rb +0 -31
  104. data/spec/datastax_rails/types/time_type_spec.rb +0 -28
@@ -10,46 +10,29 @@ module DatastaxRails
10
10
  @fields = []
11
11
  @copy_fields = []
12
12
  @fulltext_fields = []
13
- @custom_fields = ""
14
- model.attribute_definitions.values.each do |attr|
15
- coder = attr.coder
16
- if coder.options[:solr_type]
17
- @fields.push({ :name => attr.name,
18
- :type => coder.options[:solr_type].to_s,
19
- :indexed => (coder.options[:indexed] == :solr || coder.options[:indexed] == :both).to_s,
20
- :stored => coder.options[:stored].to_s,
21
- :multi_valued => coder.options[:multi_valued].to_s })
22
- end
23
- if coder.options[:sortable] && coder.options[:tokenized]
24
- @fields.push({ :name => "sort_" + attr.name,
25
- :type => "string",
26
- :indexed => true,
27
- :stored => false,
28
- :multi_valued => false })
29
- @copy_fields.push({ :source => attr.name, :dest => "sort_" + attr.name }) if (coder.options[:indexed] || coder.options[:stored])
30
- end
31
- if coder.options[:fulltext]
32
- @fulltext_fields << attr.name if (coder.options[:indexed] || coder.options[:stored])
33
- end
13
+ if model <= WideStorageModel
14
+ @primary_key = "(#{model.primary_key},#{model.cluster_by})"
15
+ else
16
+ @primary_key = model.primary_key
34
17
  end
35
- # Sort the fields so that no matter what order the attributes are arranged into the
36
- # same schema file gets generated
18
+ @custom_fields = ""
19
+ @columns = model.attribute_definitions.values
37
20
  @fields.sort! {|a,b| a[:name] <=> b[:name]}
38
21
  @copy_fields.sort! {|a,b| a[:source] <=> b[:source]}
39
22
  @fulltext_fields.sort!
40
23
 
41
24
  if Rails.root.join('config','solr',"#{model.column_family}-schema.xml.erb").exist?
42
25
  say "Using custom schema for #{model.name}", :subitem
43
- ERB.new(Rails.root.join('config','solr',"#{model.column_family}-schema.xml.erb").read).result(binding)
26
+ ERB.new(Rails.root.join('config','solr',"#{model.column_family}-schema.xml.erb").read, 0, '>').result(binding)
44
27
  else
45
- ERB.new(File.read(File.join(File.dirname(__FILE__),"..","..","..","config","schema.xml.erb"))).result(binding)
28
+ ERB.new(File.read(File.join(File.dirname(__FILE__),"..","..","..","config","schema.xml.erb")), 0, '>').result(binding)
46
29
  end
47
30
  end
48
31
 
49
32
  # Sends a command to Solr instructing it to reindex the data. The data is reindexed in the background,
50
33
  # and the new index is swapped in once it is finished.
51
- def reindex_solr(model)
52
- url = "#{DatastaxRails::Base.solr_base_url}/admin/cores?action=RELOAD&name=#{DatastaxRails::Base.config[:keyspace]}.#{model.column_family}&reindex=true&deleteAll=false"
34
+ def reindex_solr(model, destructive=false)
35
+ url = "#{DatastaxRails::Base.solr_base_url}/admin/cores?action=RELOAD&name=#{DatastaxRails::Base.config[:keyspace]}.#{model.column_family}&reindex=true&deleteAll=#{destructive.to_s}"
53
36
  say "Posting reindex command to '#{url}'", :subitem
54
37
  `curl -s -X POST '#{url}'`
55
38
  say "Reindexing will run in the background", :subitem
@@ -89,11 +72,11 @@ module DatastaxRails
89
72
  stopwords_digest = Digest::SHA1.hexdigest(stopwords)
90
73
  schema_digest = Digest::SHA1.hexdigest(schema)
91
74
 
92
- newcf = !column_family_exists?(model.column_family.to_s)
75
+ newcf = !column_exists?(model.column_family, 'solr_query')
93
76
  force ||= newcf
94
77
 
95
- results = DatastaxRails::Cql::Select.new(SchemaMigration, ['*']).conditions(:key => model.column_family).execute
96
- sm_digests = CassandraCQL::Result.new(results).fetch.try(:to_hash) || {}
78
+ results = DatastaxRails::Cql::Select.new(SchemaMigration, ['*']).conditions(:cf => model.column_family).execute
79
+ sm_digests = results.first || {}
97
80
 
98
81
  solr_url = "#{DatastaxRails::Base.solr_base_url}/resource/#{@keyspace}.#{model.column_family}"
99
82
 
@@ -0,0 +1,67 @@
1
+ module DatastaxRails
2
+ class SchemaCache
3
+ attr_reader :primary_keys, :tables
4
+ attr_reader :connection
5
+
6
+ def initialize(conn)
7
+ @connection = conn
8
+ @tables = {}
9
+
10
+ @columns = Hash.new do |h, table_name|
11
+ h[table_name] = connection.columns(table_name, "#{table_name} Columns")
12
+ end
13
+
14
+ @columns_hash = Hash.new do |h, table_name|
15
+ h[table_name] = Hash[columns[table_name].map { |col|
16
+ [col.name, col]
17
+ }]
18
+ end
19
+
20
+ @primary_keys = Hash.new do |h, table_name|
21
+ h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil
22
+ end
23
+ end
24
+
25
+ # A cached lookup for table existence.
26
+ def table_exists?(name)
27
+ return @tables[name] if @tables.key? name
28
+
29
+ @tables[name] = connection.table_exists?(name)
30
+ end
31
+
32
+ # Get the columns for a table
33
+ def columns(table = nil)
34
+ if table
35
+ @columns[table]
36
+ else
37
+ @columns
38
+ end
39
+ end
40
+
41
+ # Get the columns for a table as a hash, key is the column name
42
+ # value is the column object.
43
+ def columns_hash(table = nil)
44
+ if table
45
+ @columns_hash[table]
46
+ else
47
+ @columns_hash
48
+ end
49
+ end
50
+
51
+ # Clears out internal caches
52
+ def clear!
53
+ @columns.clear
54
+ @columns_hash.clear
55
+ @primary_keys.clear
56
+ @tables.clear
57
+ end
58
+
59
+ # Clear out internal caches for table with +table_name+.
60
+ def clear_table_cache!(table_name)
61
+ @columns.delete table_name
62
+ @columns_hash.delete table_name
63
+ @primary_keys.delete table_name
64
+ @tables.delete table_name
65
+ end
66
+ end
67
+ end
@@ -1,25 +1,98 @@
1
1
  module DatastaxRails
2
+ # = DatastaxRails Timestamps
3
+ #
4
+ # DatastaxRails automatically timestamps create and update operations if the
5
+ # table has fields named <tt>created_at</tt> or <tt>updated_at</tt>.
6
+ #
7
+ # Timestamping can be turned off by setting:
8
+ #
9
+ # DatastaxRails::Base.record_timestamps = false
2
10
  module Timestamps
3
11
  extend ActiveSupport::Concern
4
12
 
5
13
  included do
6
- # attribute :created_at, :type => :time#_with_zone
7
- # attribute :updated_at, :type => :time#_with_zone
14
+ class_attribute :record_timestamps
15
+ self.record_timestamps = true
16
+ end
8
17
 
9
- before_create do #|r|
10
- if self.respond_to?(:created_at=)
11
- self.created_at ||= Time.current
12
- end
13
- if self.respond_to?(:updated_at=)
14
- self.updated_at ||= Time.current
18
+ def initialize_dup(other) # :nodoc:
19
+ clear_timestamp_attributes
20
+ super
21
+ end
22
+
23
+ private
24
+
25
+ def _create_record(*args)
26
+ if self.record_timestamps
27
+ current_time = current_time_from_proper_timezone
28
+
29
+ all_timestamp_attributes.each do |column|
30
+ if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
31
+ write_attribute(column.to_s, current_time)
32
+ end
15
33
  end
16
34
  end
17
35
 
18
- before_update :if => :changed? do #|r|
19
- if self.respond_to?(:updated_at=)
20
- self.updated_at = Time.current
36
+ super
37
+ end
38
+
39
+ def _update_record(*args)
40
+ if should_record_timestamps?
41
+ current_time = current_time_from_proper_timezone
42
+
43
+ timestamp_attributes_for_update_in_model.each do |column|
44
+ column = column.to_s
45
+ next if attribute_changed?(column)
46
+ write_attribute(column, current_time)
21
47
  end
22
48
  end
49
+ super
50
+ end
51
+
52
+ def should_record_timestamps?
53
+ self.record_timestamps && (changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
54
+ end
55
+
56
+ def timestamp_attributes_for_create_in_model
57
+ timestamp_attributes_for_create.select { |c| self.class.column_names.include?(c.to_s) }
58
+ end
59
+
60
+ def timestamp_attributes_for_update_in_model
61
+ timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) }
62
+ end
63
+
64
+ def all_timestamp_attributes_in_model
65
+ timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model
66
+ end
67
+
68
+ def timestamp_attributes_for_update
69
+ [:updated_at]
70
+ end
71
+
72
+ def timestamp_attributes_for_create
73
+ [:created_at]
74
+ end
75
+
76
+ def all_timestamp_attributes
77
+ timestamp_attributes_for_create + timestamp_attributes_for_update
78
+ end
79
+
80
+ def max_updated_column_timestamp
81
+ if (timestamps = timestamp_attributes_for_update.map { |attr| self[attr] }.compact).present?
82
+ timestamps.map { |ts| ts.to_time }.max
83
+ end
84
+ end
85
+
86
+ def current_time_from_proper_timezone
87
+ self.class.default_timezone == :utc ? Time.now.utc : Time.now
88
+ end
89
+
90
+ # Clear attributes and changed_attributes
91
+ def clear_timestamp_attributes
92
+ all_timestamp_attributes_in_model.each do |attribute_name|
93
+ self[attribute_name] = nil
94
+ changed_attributes.delete(attribute_name)
95
+ end
23
96
  end
24
97
  end
25
98
  end
@@ -0,0 +1,88 @@
1
+ # An extension to normal arrays and hashes that allow for tracking of dirty values. This is
2
+ # used by ActiveModel's change tracking framework.
3
+ module DatastaxRails
4
+ module Types
5
+ module DirtyCollection
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attr_accessor :record, :name
10
+
11
+ methods = [:<<, :delete, :[]=, :add, :subtract, :store, :push, :pop, :unshift, :shift, :insert, :clear] +
12
+ ActiveSupport::HashWithIndifferentAccess.instance_methods(true).select{|m| m.to_s.ends_with?('!')} +
13
+ Array.instance_methods(true).select{|m| m.to_s.ends_with?('!')} +
14
+ Set.instance_methods(true).select{|m| m.to_s.ends_with?('!')}
15
+
16
+ methods.each do |m|
17
+ if self.instance_methods.include?(m)
18
+ alias_method "___#{m}", m
19
+ original_method = self.instance_method(m)
20
+ define_method(m) do |*args, &block|
21
+ modifying do
22
+ original_method.bind(self).call(*args, &block)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ def initialize(record, name, collection)
30
+ @record = record
31
+ @name = name.to_s
32
+
33
+ super(collection)
34
+ organize_collection
35
+ end
36
+
37
+ def delete(obj)
38
+ modifying do
39
+ super
40
+ end
41
+ end
42
+
43
+ def self.ignore_modifications
44
+ original = $dsr_ignore_modifications
45
+ $dsr_ignore_modifications = true
46
+ result = yield
47
+ $dsr_ignore_modifications = original
48
+ result
49
+ end
50
+
51
+ private
52
+ def modifying
53
+ # So there's a problem with overriding the map! method on Array.
54
+ # When we do the update to record.attributes, HashWithIndifferentAccess
55
+ # calls .map! on our Array. This causes infinite recursion which
56
+ # I find is generally not a desired behavior. We use a variable
57
+ # to tell if we've already hijacked the call.
58
+ if $dsr_ignore_modifications
59
+ yield
60
+ else
61
+ DirtyCollection.ignore_modifications do
62
+ unless record.changed_attributes.key?(name)
63
+ original = dup
64
+ end
65
+
66
+ result = yield
67
+
68
+ organize_collection
69
+
70
+ if !record.changed_attributes.key?(name) && original != self
71
+ record.changed_attributes[name] = original
72
+ end
73
+
74
+ record.attributes[name] = self
75
+
76
+ result
77
+ end
78
+ end
79
+ end
80
+
81
+ # A hook to allow implementing classes to muck with the collection
82
+ # before we check it for equality.
83
+ def organize_collection
84
+ # No-op
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,14 @@
1
+ module DatastaxRails
2
+ module Types
3
+ # A collection type that allows you to store ordered arrays in Cassandra.
4
+ # Changes are tracked by hooking into ActiveModel's built-in change
5
+ # tracking.
6
+ class DynamicList < Array
7
+ include DirtyCollection
8
+
9
+ def initialize(record, name, collection)
10
+ super(record, name, collection || [])
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ module DatastaxRails
2
+ module Types
3
+ # A collection type that allows you to store key/value pairs in Cassandra.
4
+ # Changes are tracked by hooking into ActiveModel's built-in change
5
+ # tracking.
6
+ #
7
+ # Keys are converted to have the name of the collection prefixed
8
+ # to them as this is how the Solr/Cassandra integration converts
9
+ # between them and dynamic fields.
10
+ class DynamicMap < ActiveSupport::HashWithIndifferentAccess
11
+ include DirtyCollection
12
+
13
+ def dup
14
+ self.class.new(record, name, self).tap do |new_hash|
15
+ new_hash.default = default
16
+ end
17
+ end
18
+
19
+ def [](key)
20
+ super(convert_key(key))
21
+ end
22
+
23
+ protected
24
+ def convert_key(key)
25
+ unless key.to_s.starts_with?(name)
26
+ key = name + key.to_s
27
+ end
28
+ super(key)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ module DatastaxRails
2
+ module Types
3
+ # A collection type that allows you to store an un-ordered, unique
4
+ # set of entries. Changes are tracked by hooking into ActiveModel's
5
+ # built-in change tracking.
6
+ class DynamicSet < Set
7
+ include DirtyCollection
8
+ end
9
+ end
10
+ end
@@ -1,12 +1,11 @@
1
1
  module DatastaxRails
2
2
  module SolrRepair
3
3
  def repair_solr
4
- my_attrs = self.attributes.symbolize_keys.reject do |k,v|
5
- v.nil? ||
6
- !(self.class.attribute_definitions[k].coder.options[:stored] ||
7
- self.class.attribute_definitions[k].coder.options[:indexed])
4
+ encoded = {}
5
+ self.attributes.keys.each do |column_name|
6
+ value = self.read_attribute(column_name)
7
+ encoded[column_name.to_s] = self.class.column_for_attribute(column_name).type_cast_for_solr(value)
8
8
  end
9
- encoded = self.class.encode_attributes(my_attrs).merge(:id => self.id)
10
9
  xml_doc = RSolr::Xml::Generator.new.add(encoded)
11
10
  self.class.solr_connection.update(:data => xml_doc, :params => {:replacefields => false})
12
11
  end
@@ -1,5 +1,5 @@
1
1
  module DatastaxRails
2
- class RecordInvalid < StandardError
2
+ class RecordInvalid < DatastaxRailsError
3
3
  attr_reader :record
4
4
  def initialize(record)
5
5
  @record = record
@@ -11,14 +11,10 @@ module DatastaxRails
11
11
  extend ActiveSupport::Concern
12
12
  include ActiveModel::Validations
13
13
 
14
- included do
15
- define_model_callbacks :validation
16
- define_callbacks :validate, :scope => :name
17
- end
18
-
19
14
  module ClassMethods
20
15
  def create!(attributes = {})
21
16
  new(attributes).tap do |object|
17
+ yield(object) if block_given?
22
18
  object.save!
23
19
  end
24
20
  end
@@ -34,11 +30,9 @@ module DatastaxRails
34
30
  # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
35
31
  # some <tt>:on</tt> option will only run in the specified context.
36
32
  def valid?(context = nil)
37
- run_callbacks :validation do
38
- context ||= (new_record? ? :create : :update)
39
- output = super(context)
40
- errors.empty? && output
41
- end
33
+ context ||= (new_record? ? :create : :update)
34
+ output = super(context)
35
+ errors.empty? && output
42
36
  end
43
37
 
44
38
  def save(options={})
@@ -51,7 +45,7 @@ module DatastaxRails
51
45
 
52
46
  protected
53
47
  def perform_validations(options={})
54
- (options[:validate] != false) ? valid? : true
48
+ options[:validate] == false || valid?(options[:context])
55
49
  end
56
50
  end
57
51
  end