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
@@ -0,0 +1,59 @@
1
+ module DatastaxRails
2
+ module AttributeMethods
3
+ module Write
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attribute_method_suffix "="
8
+ end
9
+
10
+ module ClassMethods
11
+ protected
12
+
13
+ # See define_method_attribute in read.rb for an explanation of
14
+ # this code.
15
+ def define_method_attribute=(name)
16
+ safe_name = name.unpack('h*').first
17
+ generated_attribute_methods::AttrNames.set_name_cache safe_name, name
18
+
19
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
20
+ def __temp__#{safe_name}=(value)
21
+ write_attribute(AttrNames::ATTR_#{safe_name}, value)
22
+ end
23
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
24
+ undef_method :__temp__#{safe_name}=
25
+ STR
26
+ end
27
+ end
28
+
29
+ # Updates the attribute identified by <tt>attr_name</tt> with the
30
+ # specified +value+. Empty strings for fixnum and float columns are
31
+ # turned into +nil+.
32
+ def write_attribute(attr_name, value)
33
+ attr_name = attr_name.to_s
34
+ attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
35
+ @attributes_cache.delete(attr_name)
36
+ column = column_for_attribute(attr_name)
37
+
38
+ # If we're dealing with a binary column, write the data to the cache
39
+ # so we don't attempt to typecast multiple times.
40
+ if column && column.binary?
41
+ @attributes_cache[attr_name] = value
42
+ end
43
+
44
+ if column || @attributes.has_key?(attr_name)
45
+ @attributes[attr_name] = value
46
+ else
47
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
48
+ end
49
+ end
50
+ alias_method :raw_write_attribute, :write_attribute
51
+
52
+ private
53
+ # Handle *= for method_missing.
54
+ def attribute=(attribute_name, value)
55
+ write_attribute(attribute_name, value)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,11 +1,3 @@
1
- if Rails.version =~ /^3.*/
2
- # Dynamic finders are only supported in Rails 3.x applications (depricated in 4.x)
3
- require 'active_record/dynamic_finder_match'
4
- require 'active_record/dynamic_scope_match'
5
- elsif Rails.version =~ /^4.*./
6
- require 'active_record/deprecated_finders/dynamic_matchers'
7
- end
8
- require 'datastax_rails/types'
9
1
  require 'datastax_rails/errors'
10
2
  module DatastaxRails #:nodoc:
11
3
  # = DatastaxRails
@@ -13,7 +5,8 @@ module DatastaxRails #:nodoc:
13
5
  # DatastaxRails-based objects differ from Active Record objects in that they specify their
14
6
  # attributes directly on the model. This is necessary because of the fact that Cassandra
15
7
  # column families do not have a set list of columns but rather can have different columns per
16
- # row. By specifying the attributes on the model, getters and setters are automatically
8
+ # row. (This is not strictly true any more, but it's still not as nailed down as SQL.)
9
+ # By specifying the attributes on the model, getters and setters are automatically
17
10
  # created, and the attribute is automatically indexed into SOLR.
18
11
  #
19
12
  #
@@ -26,24 +19,14 @@ module DatastaxRails #:nodoc:
26
19
  # your Datastax cluster.
27
20
  #
28
21
  # class Person < DatastaxRails::Base
29
- # key :uuid
30
- # end
31
- #
32
- # If you want to use a natural key (i.e., one or more of the columns of your data),
33
- # the following would work.
34
- #
35
- # class Person < DatastaxRails::Base
36
- # key :natural, :attributes => [:last_name, :first_name]
22
+ # uuid :id
37
23
  # end
38
24
  #
39
- # Finally, you can create a custom key based on a method on your model.
25
+ # You don't have to use a uuid. You can use a different column as your primary key.
40
26
  #
41
27
  # class Person < DatastaxRails::Base
42
- # key :custom, :method => :my_key
43
- #
44
- # def my_key
45
- # # Some logic to generate a key
46
- # end
28
+ # self.primary_key = 'userid'
29
+ # string :userid
47
30
  # end
48
31
  #
49
32
  # == Attributes
@@ -51,30 +34,35 @@ module DatastaxRails #:nodoc:
51
34
  # Attributes are specified near the top of the model. The following attribute types
52
35
  # are supported:
53
36
  #
54
- # * array - an array of strings
55
37
  # * binary - a large object that will not be indexed into SOLR (e.g., BLOB)
56
38
  # * boolean - true/false values
57
39
  # * date - a date without a time component
58
40
  # * float - a number in floating point notation
59
41
  # * integer - a whole, round number of any size
42
+ # * list - an ordered list of values of a single type
43
+ # * map - a collection of key/value pairs of a single type (keys are always strings)
44
+ # * set - an un-ordered set of unique values of a single type
60
45
  # * string - a generic string type that is not tokenized by default
61
46
  # * text - like strings but will be tokenized for full-text searching by default
62
47
  # * time - a datetime object
63
48
  # * timestamps - a special type that instructs DSR to include created_at and updated_at
49
+ # * uuid - a UUID in standard UUID format
64
50
  #
65
51
  # The following options may be specified on the various types to control how they
66
52
  # are indexed into SOLR:
67
53
  #
68
- # * indexed - If the attribute should the attribute be indexed into SOLR.
54
+ # * solr_index - If the attribute should the attribute be indexed into SOLR.
69
55
  # Defaults to true for everything but binary.
70
- # * stored - If the attribute should the attribute be stored in SOLR.
56
+ # * solr_store - If the attribute should the attribute be stored in SOLR.
71
57
  # Defaults to true for everything but binary. (see note)
72
58
  # * sortable - If the attribute should be sortable by SOLR.
73
59
  # Defaults to true for everything but binary and text. (see note)
74
60
  # * tokenized - If the attribute should be tokenized for full-text searching within the field.
75
- # Defaults to true for array and text. (see note)
61
+ # Defaults to true for text.
76
62
  # * fulltext - If the attribute should be included in the default field for full-text searches.
77
63
  # Defaults to true for text and string.
64
+ # * multi_valued - If the field will contain multiple values in Solr.
65
+ # Defaults to true for list and set. This should never need to be set manually.
78
66
  #
79
67
  # NOTES:
80
68
  # * No fields are actually stored in SOLR. When a field is requested from SOLR, the field
@@ -87,14 +75,11 @@ module DatastaxRails #:nodoc:
87
75
  # one that gets tokenized and one that is a single token for sorting. As this inflates the
88
76
  # size of the index, you don't want to do this for large fields (which probably don't make
89
77
  # sense to sort on anyways).
90
- # * Arrays are tokenized specially. Each element of the array is treated as a single token.
91
- # This means that you can match against any single element, but you cannot search within
92
- # elements. This functionality may be added at a later time.
93
78
  #
94
79
  # EXAMPLE:
95
80
  #
96
81
  # class Person < DatastaxRails::Base
97
- # key :uuid
82
+ # uuid :id
98
83
  # string :first_name
99
84
  # string :user_name
100
85
  # text :bio
@@ -105,13 +90,15 @@ module DatastaxRails #:nodoc:
105
90
  #
106
91
  # == Schemas
107
92
  #
108
- # Cassandra itself is a 'schema-optional' database. In general, DSR does not make use of
109
- # Cassandra schemas. SOLR on the other hand does use a schema to define the data and how
110
- # it should be indexed. There is a rake task to upload the latest SOLR schema based on
111
- # the model files. When this happens, if the column family does not exist yet, it will be
112
- # created. Therefore, migrations to create column families are unnecessary. If the
113
- # column family does exist, and the new schema differs, the columns that are changed will
114
- # be automatically reindexed.
93
+ # DSR will automatically manage both the Cassandra and Solr schemas for you based on the
94
+ # attributes that you specify on the model. You can override the Solr schema if you
95
+ # want to have something custom. There is a rake task that manages all of the schema
96
+ # information. It will create column families and columns as needed and upload the
97
+ # Solr schema when necessary. If there are changes, it will automatically kick off a
98
+ # reindex in the background.
99
+ #
100
+ # As of Cassandra 1.2, there is no way to remove a column. Cassandra 2.0 supports it,
101
+ # but it hasn't been implemented in DSR yet.
115
102
  #
116
103
  # TODO: Need a way to remove ununsed column families.
117
104
  #
@@ -121,7 +108,7 @@ module DatastaxRails #:nodoc:
121
108
  # method is especially useful when you're receiving the data from somewhere else, like an
122
109
  # HTTP request. It works like this:
123
110
  #
124
- # user = User.new(:name => "David", :occupation => "Code Artist")
111
+ # user = User.new(name: "David", occupation: "Code Artist")
125
112
  # user.name # => "David"
126
113
  #
127
114
  # You can also use block initialization:
@@ -141,16 +128,16 @@ module DatastaxRails #:nodoc:
141
128
  #
142
129
  # Cassandra has a concept of consistency levels when it comes to saving records. For a
143
130
  # detailed discussion on Cassandra data consistency, see:
144
- # http://www.datastax.com/docs/1.0/dml/data_consistency
131
+ # http://www.datastax.com/documentation/cassandra/1.2/cassandra/dml/dml_config_consistency_c.html
145
132
  #
146
133
  # DatastaxRails allows you to specify the consistency when you save and retrieve objects.
147
134
  #
148
- # user = User.new(:name => 'David')
149
- # user.save(:consistency => 'ALL')
135
+ # user = User.new(name: 'David')
136
+ # user.save(consistency: 'ALL')
150
137
  #
151
- # User.create(params[:user], {:consistency => :local_quorum})
138
+ # User.create(params[:user], {consistency: :local_quorum})
152
139
  #
153
- # User.consistency(:local_quorum).where(:name => 'David')
140
+ # User.consistency(:local_quorum).where(name: 'David')
154
141
  #
155
142
  # The default consistency level in DatastaxRails is QUORUM for writes and for retrieval
156
143
  # by ID. SOLR only supports a consistency level of ONE. See the documentation for
@@ -177,33 +164,36 @@ module DatastaxRails #:nodoc:
177
164
  # A simple hash without a statement will generate conditions based on equality using boolean AND logic.
178
165
  # For instance:
179
166
  #
180
- # Student.where(:first_name => "Harvey", :status => 1)
167
+ # Student.where(first_name: "Harvey", status: 1)
181
168
  # Student.where(params[:student])
182
169
  #
183
170
  # A range may be used in the hash to use a SOLR range query:
184
171
  #
185
- # Student.where(:grade => 9..12)
172
+ # Student.where(grade: 9..12)
186
173
  #
187
174
  # An array may be used in the hash to construct a SOLR OR query:
188
175
  #
189
- # Student.where(:grade => [9,11,12])
176
+ # Student.where(grade: [9,11,12])
190
177
  #
191
178
  # Inequality can be tested for like so:
192
179
  #
193
- # Student.where_not(:grade => 9)
180
+ # Student.where_not(grade: 9)
194
181
  # Student.where(:grade).greater_than(9)
195
182
  # Student.where(:grade).less_than(10)
196
183
  #
197
- # Fulltext searching is natively supported. All text fields are automatically indexed for fulltext
198
- # searching.
184
+ # NOTE that Solr inequalities are inclusive so really, the second example above is retrieving records
185
+ # where grace is greater than or equal to 9. Be sure to keep this in mind when you do inequality queries.
186
+ #
187
+ # Fulltext searching is natively supported. All string and text fields are automatically indexed for
188
+ # fulltext searching.
199
189
  #
200
190
  # Post.fulltext('Apple AND "iPhone 4s"')
201
191
  #
202
- # See the documentation on DatastaxRails::SearchMethods for more information and examples.
192
+ # See the documentation on {DatastaxRails::SearchMethods} for more information and examples.
203
193
  #
204
194
  # == Overwriting default accessors
205
195
  #
206
- # All column values are automatically available through basic accessors on the DatastaxRails,
196
+ # All column values are automatically available through basic accessors on the object,
207
197
  # but sometimes you want to specialize this behavior. This can be done by overwriting
208
198
  # the default accessors (using the same name as the attribute) and calling
209
199
  # <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually
@@ -226,81 +216,121 @@ module DatastaxRails #:nodoc:
226
216
  #
227
217
  # == Dynamic attribute-based finders
228
218
  #
229
- # Note: These are only available in Rails 3.x applications, and are not supported in Rails 4.x
219
+ # Dynamic finders have been removed from Rails. As a result, they have also been removed from DSR.
220
+ # In its place, the +find_by+ method can be used:
230
221
  #
231
- # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects
232
- # by simple queries without using where chains. They work by appending the name of an attribute
233
- # to <tt>find_by_</tt> or <tt>find_all_by_</tt> and thus produces finders
234
- # like <tt>Person.find_by_user_name</tt>, <tt>Person.find_all_by_last_name</tt>, and
235
- # <tt>Payment.find_by_transaction_id</tt>. Instead of writing
236
- # <tt>Person.where(:user_name => user_name).first</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>.
237
- # And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do
238
- # <tt>Person.find_all_by_last_name(last_name)</tt>.
222
+ # Student.find_by(name: 'Jason')
239
223
  #
240
- # It's also possible to use multiple attributes in the same find by separating them with "_and_".
224
+ # NOTE: there is a subtle difference between the following that does not exist in ActiveRecord:
241
225
  #
242
- # Person.where(:user_name => user_name, :password => password).first
243
- # Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
226
+ # Student.find_by(name: 'Jason')
227
+ # Student.where(name: 'Jason').first
244
228
  #
245
- # It's even possible to call these dynamic finder methods on relations and named scopes.
229
+ # The difference is that the first is escaped so that special characters can be used. The
230
+ # second method requires you to do the escaping yourself if you need it done. As an example,
246
231
  #
247
- # Payment.order("created_on").find_all_by_amount(50)
248
- # Payment.pending.find_last_by_amount(100)
232
+ # Company.find_by(name: 'All*') #=> finds only the company with the literal name 'All*'
233
+ # Company.where(name: 'All*').first #=> finds the first company whose name begins with All
249
234
  #
250
- # The same dynamic finder style can be used to create the object if it doesn't already exist.
251
- # This dynamic finder is called with <tt>find_or_create_by_</tt> and will return the object if
252
- # it already exists and otherwise creates it, then returns it. Protected attributes won't be set
253
- # unless they are given in a block.
235
+ # See DatastaxRails::FinderMethods for more information
236
+ #
237
+ # == Facets
254
238
  #
255
- # NOTE: This functionality is currently unimplemented but will be in a release in the near future.
239
+ # DSR support both field and range facets. For additional detail on facets, see the documentation
240
+ # available under the {DatastaxRails::FacetMethods} module. The result is available through the
241
+ # facets accessor.
256
242
  #
257
- # # No 'Summer' tag exists
258
- # Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")
243
+ # results = Article.field_facet(:author)
244
+ # results.facets #=> {"author"=>["vonnegut", 2. "asimov", 3]}
259
245
  #
260
- # # Now the 'Summer' tag does exist
261
- # Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
246
+ # Model.field_facet(:author)
247
+ # Model.field_facet(:author, sort: 'count', limit: 10, mincount: 1)
248
+ # Model.range_facet(:price, 500, 1000, 10)
249
+ # Model.range_facet(:price, 500, 1000, 10, include: 'all')
250
+ # Model.range_facet(:publication_date, "1968-01-01T00:00:00Z", "2000-01-01T00:00:00Z", "+1YEAR")
262
251
  #
263
- # # Now 'Bob' exist and is an 'admin'
264
- # User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true }
252
+ # Range Gap syntax for dates: +1YEAR, +5YEAR, +5YEARS, +1MONTH, +1DAY
265
253
  #
266
- # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without
267
- # saving it first. Protected attributes won't be set unless they are given in a block.
254
+ # Useful constants:
268
255
  #
269
- # # No 'Winter' tag exists
270
- # winter = Tag.find_or_initialize_by_name("Winter")
271
- # winter.persisted? # false
256
+ # DatastaxRails::FacetMethods::BY_YEAR (+1YEAR)
257
+ # DatastaxRails::FacetMethods::BY_MONTH (+1MONTH)
258
+ # DatastaxRails::FacetMethods::BY_DAY (+1DAY)
272
259
  #
273
- # Just like <tt>find_by_*</tt>, you can also use <tt>scoped_by_*</tt> to retrieve data. The good thing about
274
- # using this feature is that the very first time result is returned using <tt>method_missing</tt> technique
275
- # but after that the method is declared on the class. Henceforth <tt>method_missing</tt> will not be hit.
260
+ # Model.range_facet(:publication_date, "1968-01-01T00:00:00Z", "2000-01-01T00:00:00Z", DatastaxRails::FacetMethods::BY_YEAR)
276
261
  #
277
- # User.scoped_by_user_name('David')
262
+ # == Collections
278
263
  #
279
- # == Facets
264
+ # Cassandra supports the notion of collections on a row. The three types of supported
265
+ # collections are +set+, +list+, and +map+.
280
266
  #
281
- # DSR support both field and range facets. For additional detail on facets, see the documentation
282
- # available under the FacetMethods module. The result is available through the facets accessor
267
+ # By default collections hold strings. You can override this by passing a :holds option in the
268
+ # attribute definition. Sets can hold anything other than other collections, however, a given
269
+ # collection can only hold a single type of values.
283
270
  #
284
- # Facet examples:
271
+ # NOTE: There is a limitation in Cassandra where only the first 64k entries of a collection are
272
+ # ever returned with a query. Therefore, if you put more than 64k entries in a collection you
273
+ # will lose data.
285
274
  #
286
- # results = Article.field_facet(:author)
287
- # results.facets => {"author"=>["vonnegut", 2. "asimov", 3]}
275
+ # === Set
276
+ #
277
+ # A set is an un-ordered collection of unique values. This collection is fully searchable in Solr.
288
278
  #
289
- # Model.field_facet(:author)
290
- # Model.field_facet(:author, :sort => 'count', :limit => 10, :mincount => 1)
291
- # Model.range_facet(:price, 500, 1000, 10)
292
- # Model.range_facet(:price, 500, 1000, 10, :include => 'all')
293
- # Model.range_facet(:publication_date, "1968-01-01T00:00:00Z", "2000-01-01T00:00:00Z", "+1YEAR")
279
+ # class User < DatastaxRails::Base
280
+ # uuid :id
281
+ # string :username
282
+ # set :emails
283
+ # end
294
284
  #
295
- # Range Gap syntax for dates: +1YEAR, +5YEAR, +5YEARS, +1MONTH, +1DAY
285
+ # The default set will hold strings. You can modify this behavior like so:
296
286
  #
297
- # Useful constants:
287
+ # class Student < DatastaxRails::Base
288
+ # uuid :id
289
+ # string :name
290
+ # set :grades, holds: :integers
291
+ # end
298
292
  #
299
- # DatastaxRails::FacetMethods::BY_YEAR (+1YEAR)
300
- # DatastaxRails::FacetMethods::BY_MONTH (+1MONTH)
301
- # DatastaxRails::FacetMethods::BY_DAY (+1DAY)
293
+ # User.where(emails: 'jim@example.com') #=> Returns all users where jim@example.com is in the set
294
+ # user = User.new(name: 'Jim', emails: ['jim@example.com'])
295
+ # user.emails << 'jim@example.com'
296
+ # user.emails #=> ['jim@example.com']
302
297
  #
303
- # Model.range_facet(:publication_date, "1968-01-01T00:00:00Z", "2000-01-01T00:00:00Z", DatastaxRails::FacetMethods::BY_YEAR)
298
+ # === List
299
+ #
300
+ # An ordered collection of values. They do not necessarily have to be unique. The collection
301
+ # will be fully searchable in Solr.
302
+ #
303
+ # class Student < DatastaxRails::Base
304
+ # uuid :id
305
+ # string :name
306
+ # list :classrooms, holds: integers
307
+ # end
308
+ #
309
+ # Student.where(classrooms: 307) #=> Returns all students that have a class in room 307.
310
+ # student = Student.new(name: 'Sally', classrooms: [307, 305, 301, 307])
311
+ # student.classrooms << 304
312
+ # student.classrooms #=> [307, 305, 301, 307, 304]
313
+ #
314
+ # === Map
315
+ #
316
+ # A collection of key/value pairs where the key is a string and the value is the
317
+ # specified type. The collection becomes available in Solr as dynamic fields.
318
+ #
319
+ # class Student < DatastaxRails::Base
320
+ # uuid :id
321
+ # string :name
322
+ # map :scores_, holds: :integers
323
+ # end
324
+ #
325
+ # student = Student.new(:name 'Sally')
326
+ # student.scores['midterm'] = 98
327
+ # student.scores['final'] = 97
328
+ # student.scores #=> {'scores_midterm' => 98, 'scores_final' => 97}
329
+ # Student.where(scores_final: 97) #=> Returns all students that scored 97 on their final
330
+ #
331
+ # Note that the map name gets prepended to the key. This is how Solr maps it's dynamic fields
332
+ # into the cassandra map. For this reason, it's usually a good idea to put an underscore (_)
333
+ # at the end of the map name to prevent collisions.
304
334
  #
305
335
  # == Exceptions
306
336
  #
@@ -312,30 +342,21 @@ module DatastaxRails #:nodoc:
312
342
  # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist
313
343
  # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal
314
344
  # nothing was found, please check its documentation for further details.
315
- # * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
316
- # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of
317
- # AttributeAssignmentError objects that should be inspected to determine which attributes triggered the errors.
318
- # * AttributeAssignmentError - An error occurred while doing a mass assignment through the
319
- # <tt>attributes=</tt> method.
320
- # You can inspect the +attribute+ property of the exception object to determine which attribute
321
- # triggered the error.
322
- #
323
- # See the documentation for SearchMethods for more examples of using the search API.
345
+ # * UnknownAttributeError - The specified attribute isn't defined on your model.
346
+ #
347
+ # See the documentation for {DatastaxRails::SearchMethods} for more examples of using the search API.
324
348
  class Base
325
349
  extend ActiveModel::Naming
326
350
  include ActiveModel::Conversion
327
351
  extend ActiveSupport::DescendantsTracker
328
352
 
353
+ include Persistence
329
354
  include Connection
330
355
  include Inheritance
331
- include Identity
332
356
  include FinderMethods
333
357
  include Batches
334
358
  include AttributeAssignment
335
359
  include AttributeMethods
336
- include AttributeMethods::Dirty
337
- include AttributeMethods::Typecasting
338
- include Persistence
339
360
  include Callbacks
340
361
  include Validations
341
362
  include Reflection
@@ -349,15 +370,29 @@ module DatastaxRails #:nodoc:
349
370
  class_attribute :default_scopes, :instance_writer => false
350
371
  self.default_scopes = []
351
372
 
352
- # Stores the configuration information
373
+ # Stores the connection configuration information
353
374
  class_attribute :config
354
375
 
376
+ class_attribute :default_timezone, :instance_writer => false
377
+ self.default_timezone = :utc
378
+
379
+ # Stores the default consistency level (QUORUM by default)
355
380
  class_attribute :default_consistency
356
381
  self.default_consistency = :quorum
357
382
 
383
+ # Stores the method of saving data (CQL by default)
358
384
  class_attribute :storage_method
359
385
  self.storage_method = :cql
360
386
 
387
+ # Stores any additional information that should be used when creating the column family
388
+ # See {DatastaxRails::WideStorageModel} or {DatastaxRails::Payload} model for an example
389
+ class_attribute :create_options
390
+
391
+ # Stores the attribute that wide models should cluster on. Basically, this is the
392
+ # attribute that CQL uses to "group" columns into logical records even though they
393
+ # are stored on the same row.
394
+ class_attribute :cluster_by
395
+
361
396
  attr_reader :attributes
362
397
  attr_reader :loaded_attributes
363
398
  attr_accessor :key
@@ -371,32 +406,70 @@ module DatastaxRails #:nodoc:
371
406
  class_attribute :legacy_mapping
372
407
 
373
408
  def initialize(attributes = {}, options = {})
374
- @key = attributes.delete(:key)
375
- @attributes = {}.with_indifferent_access
376
- @loaded_attributes = {}.with_indifferent_access
377
-
378
- @new_record = true
379
- @destroyed = false
380
- @previously_changed = {}
381
- @changed_attributes = {}
382
-
383
- __set_defaults
409
+ defaults = self.class.column_defaults.dup
410
+ defaults.each { |k, v| v.duplicable? ? v.dup : v }
384
411
 
412
+ @attributes = self.initialize_attributes(defaults)
413
+ @column_types = self.class.columns_hash
414
+
415
+ init_internals
416
+ init_changed_attributes
385
417
  populate_with_current_scope_attributes
386
418
 
387
- assign_attributes(attributes, options) if attributes
419
+ assign_attributes(attributes) if attributes
388
420
 
389
421
  yield self if block_given?
390
- run_callbacks :initialize
422
+ run_callbacks :initialize unless _initialize_callbacks.empty?
391
423
  end
392
424
 
393
- # Set any default attributes specified by the schema
394
- def __set_defaults
395
- self.class.attribute_definitions.each do |a,d|
396
- unless(d.coder.default.nil?)
397
- self.attributes[a]=d.coder.default
398
- self.send(a.to_s+"_will_change!")
399
- end
425
+ # Initialize an empty model object from +coder+. +coder+ must contain
426
+ # the attributes necessary for initializing an empty model object. For
427
+ # example:
428
+ #
429
+ # class Post < DatastaxRails::Base
430
+ # end
431
+ #
432
+ # post = Post.allocate
433
+ # post.init_with('attributes' => { 'title' => 'hello world' })
434
+ # post.title # => 'hello world'
435
+ def init_with(coder)
436
+ Types::DirtyCollection.ignore_modifications do
437
+ @attributes = self.initialize_attributes(coder['attributes'])
438
+ @column_types_override = coder['column_types']
439
+ @column_types = self.class.columns_hash
440
+
441
+ init_internals
442
+
443
+ @new_record = false
444
+
445
+ run_callbacks :find
446
+ run_callbacks :initialize
447
+ end
448
+ self
449
+ end
450
+
451
+ def init_internals
452
+ pk = self.class.primary_key
453
+ @attributes[pk] = nil unless @attributes.key?(pk)
454
+
455
+ @association_cache = {}
456
+ @attributes_cache = {}
457
+ @previously_changed = {}
458
+ @changed_attributes = {}
459
+ @loaded_attributes = Hash[@attributes.map{|k,v| [k,true]}].with_indifferent_access
460
+ @readonly = false
461
+ @destroyed = false
462
+ @marked_for_destruction = false
463
+ @destroyed_by_association = nil
464
+ @new_record = true
465
+ end
466
+
467
+ def init_changed_attributes
468
+ # Intentionally avoid using #column_defaults since overridden defaults
469
+ # won't get written unless they get marked as changed
470
+ self.class.columns.each do |c|
471
+ attr, orig_value = c.name, c.default
472
+ @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
400
473
  end
401
474
  end
402
475
 
@@ -421,7 +494,7 @@ module DatastaxRails #:nodoc:
421
494
  def ==(comparison_object)
422
495
  comparison_object.equal?(self) ||
423
496
  (comparison_object.instance_of?(self.class) &&
424
- comparison_object.key == key &&
497
+ comparison_object.id == self.id &&
425
498
  !comparison_object.new_record?)
426
499
  end
427
500
 
@@ -432,6 +505,7 @@ module DatastaxRails #:nodoc:
432
505
  def attribute_names
433
506
  self.class.attribute_names
434
507
  end
508
+ alias :column_names :attribute_names
435
509
 
436
510
  def valid_consistency?(level) #:nodoc:
437
511
  self.class.validate_consistency(level.to_s.upcase)
@@ -485,7 +559,7 @@ module DatastaxRails #:nodoc:
485
559
  end
486
560
 
487
561
  def legacy_mapping?
488
- @legacy_mapping
562
+ self.legacy_mapping
489
563
  end
490
564
 
491
565
  def base_class
@@ -506,27 +580,15 @@ module DatastaxRails #:nodoc:
506
580
  Rails.logger
507
581
  end
508
582
 
509
- def respond_to?(method_id, include_private = false)
510
-
511
- if Rails.version =~ /^3.*/
512
- if match = ActiveRecord::DynamicFinderMatch.match(method_id)
513
- return true if all_attributes_exists?(match.attribute_names)
514
- elsif match = ActiveRecord::DynamicScopeMatch.match(method_id)
515
- return true if all_attributes_exists?(match.attribute_names)
516
- end
517
- elsif Rails.version =~ /^4.*/
518
- if match = ActiveRecord::DynamicMatchers::Method.match(self, method_id)
519
- return true if all_attributes_exists?(match.attribute_names)
520
- end
521
- end
522
-
523
- super
524
- end
525
-
526
583
  # Returns an array of attribute names as strings
527
584
  def attribute_names
528
585
  @attribute_names ||= attribute_definitions.keys.collect {|a|a.to_s}
529
586
  end
587
+ alias :column_names :attribute_names
588
+
589
+ def columns
590
+ @columns ||= attribute_definitions.values
591
+ end
530
592
 
531
593
  # SOLR always paginates all requests. There is no way to disable it, so we are
532
594
  # setting the default page size to an arbitrarily high number so that we effectively
@@ -552,9 +614,15 @@ module DatastaxRails #:nodoc:
552
614
  DatastaxRails::Cql::Consistency::VALID_CONSISTENCY_LEVELS.include?(level)
553
615
  end
554
616
 
555
- protected
556
-
557
-
617
+ # Returns a string like 'Post(id:integer, title:string, body:text)'
618
+ def inspect
619
+ if self == Base
620
+ super
621
+ else
622
+ attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
623
+ "#{super}(#{attr_list})"
624
+ end
625
+ end
558
626
 
559
627
  private
560
628
 
@@ -564,83 +632,6 @@ module DatastaxRails #:nodoc:
564
632
  relation
565
633
  end
566
634
 
567
- # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
568
- # <tt>User.scoped_by_user_name(user_name).
569
- #
570
- # It's even possible to use all the additional parameters to +find+. For example, the
571
- # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
572
- #
573
- # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
574
- # is first invoked, so that future attempts to use it do not run through method_missing.
575
- def method_missing(method_id, *arguments, &block)
576
- if Rails.version =~ /^3.*/
577
- if match = ActiveRecord::DynamicFinderMatch.match(method_id)
578
- attribute_names = match.attribute_names
579
- super unless all_attributes_exists?(attribute_names)
580
- if !arguments.first.is_a?(Hash) && arguments.size < attribute_names.size
581
- ActiveSupport::Deprecation.warn(
582
- "Calling dynamic finder with less number of arguments than the number of attributes in " \
583
- "method name is deprecated and will raise an ArguementError in the next version of Rails. " \
584
- "Please passing `nil' to the argument you want it to be nil."
585
- )
586
- end
587
- if match.finder?
588
- options = arguments.extract_options!
589
- relation = options.any? ? scoped(options) : scoped
590
- relation.send :find_by_attributes, match, attribute_names, *arguments
591
- elsif match.instantiator?
592
- scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
593
- end
594
- elsif match = ActiveRecord::DynamicScopeMatch.match(method_id)
595
- attribute_names = match.attribute_names
596
- super unless all_attributes_exists?(attribute_names)
597
- if arguments.size < attribute_names.size
598
- ActiveSupport::Deprecation.warn(
599
- "Calling dynamic scope with less number of arguments than the number of attributes in " \
600
- "method name is deprecated and will raise an ArguementError in the next version of Rails. " \
601
- "Please passing `nil' to the argument you want it to be nil."
602
- )
603
- end
604
- if match.scope?
605
- self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
606
- def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
607
- attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
608
- scoped(:conditions => attributes) # scoped(:conditions => attributes)
609
- end # end
610
- METHOD
611
- send(method_id, *arguments)
612
- end
613
- else
614
- super
615
- end
616
- elsif Rails.version =~ /^4.*/
617
- if match = ActiveRecord::DynamicMatchers::Method.match(self, method_id)
618
- attribute_names = match.attribute_names
619
- super unless all_attributes_exists?(attribute_names)
620
- if !arguments.first.is_a?(Hash) && arguments.size < attribute_names.size
621
- ActiveSupport::Deprecation.warn(
622
- "Calling dynamic scope with less number of arguments than the number of attributes in " \
623
- "method name is deprecated and will raise an ArguementError in the next version of Rails. " \
624
- "Please passing `nil' to the argument you want it to be nil."
625
- )
626
- end
627
- if match.finder.present?
628
- options = arguments.extract_options!
629
- relation = options.any? ? scoped(options) : scoped
630
- relation.send :find_by_attributes, match, attribute_names, *arguments
631
- elsif match.instantiator?
632
- scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
633
- end
634
- end
635
- else
636
- super
637
- end
638
- end
639
-
640
- def all_attributes_exists?(attribute_names)
641
- (attribute_names - self.attribute_names).empty?
642
- end
643
-
644
635
  def relation #:nodoc:
645
636
  Relation.new(self, column_family)
646
637
  end