datastax_rails 1.2.3 → 2.0.3

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.
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