datastax_rails 1.0.5

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 (148) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +62 -0
  3. data/Rakefile +34 -0
  4. data/config/schema.xml +266 -0
  5. data/config/schema.xml.erb +70 -0
  6. data/config/solrconfig.xml +1564 -0
  7. data/config/stopwords.txt +58 -0
  8. data/lib/datastax_rails/associations/association.rb +224 -0
  9. data/lib/datastax_rails/associations/association_scope.rb +25 -0
  10. data/lib/datastax_rails/associations/belongs_to_association.rb +64 -0
  11. data/lib/datastax_rails/associations/builder/association.rb +56 -0
  12. data/lib/datastax_rails/associations/builder/belongs_to.rb +30 -0
  13. data/lib/datastax_rails/associations/builder/collection_association.rb +48 -0
  14. data/lib/datastax_rails/associations/builder/has_and_belongs_to_many.rb +36 -0
  15. data/lib/datastax_rails/associations/builder/has_many.rb +54 -0
  16. data/lib/datastax_rails/associations/builder/has_one.rb +52 -0
  17. data/lib/datastax_rails/associations/builder/singular_association.rb +56 -0
  18. data/lib/datastax_rails/associations/collection_association.rb +274 -0
  19. data/lib/datastax_rails/associations/collection_proxy.rb +118 -0
  20. data/lib/datastax_rails/associations/has_and_belongs_to_many_association.rb +44 -0
  21. data/lib/datastax_rails/associations/has_many_association.rb +58 -0
  22. data/lib/datastax_rails/associations/has_one_association.rb +68 -0
  23. data/lib/datastax_rails/associations/singular_association.rb +58 -0
  24. data/lib/datastax_rails/associations.rb +86 -0
  25. data/lib/datastax_rails/attribute_methods/definition.rb +20 -0
  26. data/lib/datastax_rails/attribute_methods/dirty.rb +43 -0
  27. data/lib/datastax_rails/attribute_methods/typecasting.rb +50 -0
  28. data/lib/datastax_rails/attribute_methods.rb +104 -0
  29. data/lib/datastax_rails/base.rb +587 -0
  30. data/lib/datastax_rails/batches.rb +35 -0
  31. data/lib/datastax_rails/callbacks.rb +37 -0
  32. data/lib/datastax_rails/collection.rb +9 -0
  33. data/lib/datastax_rails/connection.rb +21 -0
  34. data/lib/datastax_rails/consistency.rb +33 -0
  35. data/lib/datastax_rails/cql/base.rb +15 -0
  36. data/lib/datastax_rails/cql/column_family.rb +38 -0
  37. data/lib/datastax_rails/cql/consistency.rb +13 -0
  38. data/lib/datastax_rails/cql/create_column_family.rb +63 -0
  39. data/lib/datastax_rails/cql/create_keyspace.rb +30 -0
  40. data/lib/datastax_rails/cql/delete.rb +41 -0
  41. data/lib/datastax_rails/cql/drop_column_family.rb +13 -0
  42. data/lib/datastax_rails/cql/drop_keyspace.rb +13 -0
  43. data/lib/datastax_rails/cql/insert.rb +53 -0
  44. data/lib/datastax_rails/cql/select.rb +51 -0
  45. data/lib/datastax_rails/cql/truncate.rb +13 -0
  46. data/lib/datastax_rails/cql/update.rb +68 -0
  47. data/lib/datastax_rails/cql/use_keyspace.rb +13 -0
  48. data/lib/datastax_rails/cql.rb +25 -0
  49. data/lib/datastax_rails/cursor.rb +90 -0
  50. data/lib/datastax_rails/errors.rb +16 -0
  51. data/lib/datastax_rails/identity/abstract_key_factory.rb +26 -0
  52. data/lib/datastax_rails/identity/custom_key_factory.rb +36 -0
  53. data/lib/datastax_rails/identity/hashed_natural_key_factory.rb +10 -0
  54. data/lib/datastax_rails/identity/natural_key_factory.rb +37 -0
  55. data/lib/datastax_rails/identity/uuid_key_factory.rb +23 -0
  56. data/lib/datastax_rails/identity.rb +53 -0
  57. data/lib/datastax_rails/log_subscriber.rb +37 -0
  58. data/lib/datastax_rails/migrations/migration.rb +15 -0
  59. data/lib/datastax_rails/migrations.rb +36 -0
  60. data/lib/datastax_rails/mocking.rb +15 -0
  61. data/lib/datastax_rails/persistence.rb +133 -0
  62. data/lib/datastax_rails/railtie.rb +20 -0
  63. data/lib/datastax_rails/reflection.rb +472 -0
  64. data/lib/datastax_rails/relation/finder_methods.rb +184 -0
  65. data/lib/datastax_rails/relation/modification_methods.rb +80 -0
  66. data/lib/datastax_rails/relation/search_methods.rb +349 -0
  67. data/lib/datastax_rails/relation/spawn_methods.rb +107 -0
  68. data/lib/datastax_rails/relation.rb +393 -0
  69. data/lib/datastax_rails/schema/migration.rb +106 -0
  70. data/lib/datastax_rails/schema/migration_proxy.rb +25 -0
  71. data/lib/datastax_rails/schema/migrator.rb +212 -0
  72. data/lib/datastax_rails/schema.rb +37 -0
  73. data/lib/datastax_rails/scoping.rb +394 -0
  74. data/lib/datastax_rails/serialization.rb +6 -0
  75. data/lib/datastax_rails/tasks/column_family.rb +162 -0
  76. data/lib/datastax_rails/tasks/ds.rake +63 -0
  77. data/lib/datastax_rails/tasks/keyspace.rb +57 -0
  78. data/lib/datastax_rails/timestamps.rb +19 -0
  79. data/lib/datastax_rails/type.rb +16 -0
  80. data/lib/datastax_rails/types/array_type.rb +77 -0
  81. data/lib/datastax_rails/types/base_type.rb +26 -0
  82. data/lib/datastax_rails/types/binary_type.rb +15 -0
  83. data/lib/datastax_rails/types/boolean_type.rb +22 -0
  84. data/lib/datastax_rails/types/date_type.rb +17 -0
  85. data/lib/datastax_rails/types/float_type.rb +18 -0
  86. data/lib/datastax_rails/types/integer_type.rb +18 -0
  87. data/lib/datastax_rails/types/string_type.rb +16 -0
  88. data/lib/datastax_rails/types/text_type.rb +16 -0
  89. data/lib/datastax_rails/types/time_type.rb +17 -0
  90. data/lib/datastax_rails/types.rb +9 -0
  91. data/lib/datastax_rails/validations/uniqueness.rb +119 -0
  92. data/lib/datastax_rails/validations.rb +48 -0
  93. data/lib/datastax_rails/version.rb +3 -0
  94. data/lib/datastax_rails.rb +87 -0
  95. data/lib/solr_no_escape.rb +28 -0
  96. data/spec/datastax_rails/associations/belongs_to_association_spec.rb +7 -0
  97. data/spec/datastax_rails/associations/has_many_association_spec.rb +37 -0
  98. data/spec/datastax_rails/associations_spec.rb +22 -0
  99. data/spec/datastax_rails/attribute_methods_spec.rb +23 -0
  100. data/spec/datastax_rails/base_spec.rb +15 -0
  101. data/spec/datastax_rails/cql/select_spec.rb +12 -0
  102. data/spec/datastax_rails/cql/update_spec.rb +0 -0
  103. data/spec/datastax_rails/relation/finder_methods_spec.rb +54 -0
  104. data/spec/datastax_rails/relation/modification_methods_spec.rb +41 -0
  105. data/spec/datastax_rails/relation/search_methods_spec.rb +117 -0
  106. data/spec/datastax_rails/relation/spawn_methods_spec.rb +28 -0
  107. data/spec/datastax_rails/relation_spec.rb +130 -0
  108. data/spec/datastax_rails/validations/uniqueness_spec.rb +41 -0
  109. data/spec/datastax_rails_spec.rb +5 -0
  110. data/spec/dummy/Rakefile +8 -0
  111. data/spec/dummy/app/assets/javascripts/application.js +9 -0
  112. data/spec/dummy/app/assets/stylesheets/application.css +7 -0
  113. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  114. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  115. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  116. data/spec/dummy/config/application.rb +47 -0
  117. data/spec/dummy/config/boot.rb +10 -0
  118. data/spec/dummy/config/database.yml +25 -0
  119. data/spec/dummy/config/datastax.yml +18 -0
  120. data/spec/dummy/config/environment.rb +5 -0
  121. data/spec/dummy/config/environments/development.rb +30 -0
  122. data/spec/dummy/config/environments/production.rb +60 -0
  123. data/spec/dummy/config/environments/test.rb +39 -0
  124. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  125. data/spec/dummy/config/initializers/inflections.rb +10 -0
  126. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  127. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  128. data/spec/dummy/config/initializers/session_store.rb +8 -0
  129. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  130. data/spec/dummy/config/locales/en.yml +5 -0
  131. data/spec/dummy/config/routes.rb +58 -0
  132. data/spec/dummy/config/sunspot.yml +17 -0
  133. data/spec/dummy/config.ru +4 -0
  134. data/spec/dummy/ks/migrate/20111117224534_models.rb +20 -0
  135. data/spec/dummy/ks/schema.json +180 -0
  136. data/spec/dummy/log/development.log +298 -0
  137. data/spec/dummy/log/production.log +0 -0
  138. data/spec/dummy/log/test.log +20307 -0
  139. data/spec/dummy/public/404.html +26 -0
  140. data/spec/dummy/public/422.html +26 -0
  141. data/spec/dummy/public/500.html +26 -0
  142. data/spec/dummy/public/favicon.ico +0 -0
  143. data/spec/dummy/script/rails +6 -0
  144. data/spec/spec.opts +5 -0
  145. data/spec/spec_helper.rb +29 -0
  146. data/spec/support/datastax_test_hook.rb +14 -0
  147. data/spec/support/models.rb +72 -0
  148. metadata +353 -0
@@ -0,0 +1,587 @@
1
+ require 'active_record/dynamic_finder_match'
2
+ require 'active_record/dynamic_scope_match'
3
+ require 'datastax_rails/log_subscriber'
4
+ require 'datastax_rails/types'
5
+ require 'datastax_rails/errors'
6
+ module DatastaxRails #:nodoc:
7
+ # = DatastaxRails
8
+ #
9
+ # DatastaxRails-based objects differ from Active Record objects in that they specify their
10
+ # attributes directly on the model. This is necessary because of the fact that Cassandra
11
+ # column families do not have a set list of columns but rather can have different columns per
12
+ # row. By specifying the attributes on the model, getters and setters are automatically
13
+ # created, and the attribute is automatically indexed into SOLR.
14
+ #
15
+ #
16
+ # == Primary Keys
17
+ #
18
+ # Several types of primary keys are supported in DSR. The most common type used is UUID.
19
+ # In general, incrementing numbers are not used as there is no way to guarantee a
20
+ # consistent one-up number across nodes. The following will cause a unique UUID to be
21
+ # generated for each model. This works best if you are using the RandomPartitioner in
22
+ # your Datastax cluster.
23
+ #
24
+ # class Person < DatastaxRails::Base
25
+ # key :uuid
26
+ # end
27
+ #
28
+ # If you want to use a natural key (i.e., one or more of the columns of your data),
29
+ # the following would work.
30
+ #
31
+ # class Person < DatastaxRails::Base
32
+ # key :natural, :attributes => [:last_name, :first_name]
33
+ # end
34
+ #
35
+ # Finally, you can create a custom key based on a method on your model.
36
+ #
37
+ # class Person < DatastaxRails::Base
38
+ # key :custom, :method => :my_key
39
+ #
40
+ # def my_key
41
+ # # Some logic to generate a key
42
+ # end
43
+ # end
44
+ #
45
+ # == Attributes
46
+ #
47
+ # Attributes are specified near the top of the model. The following attribute types
48
+ # are supported:
49
+ #
50
+ # * array - an array of strings
51
+ # * binary - a large object that will not be indexed into SOLR (e.g., BLOB)
52
+ # * boolean - true/false values
53
+ # * date - a date without a time component
54
+ # * float - a number in floating point notation
55
+ # * integer - a whole, round number of any size
56
+ # * string - a generic string type that is not tokenized by default
57
+ # * text - like strings but will be tokenized for full-text searching by default
58
+ # * time - a datetime object
59
+ # * timestamps - a special type that instructs DSR to include created_at and updated_at
60
+ #
61
+ # The following options may be specified on the various types to control how they
62
+ # are indexed into SOLR:
63
+ #
64
+ # * indexed - If the attribute should the attribute be indexed into SOLR.
65
+ # Defaults to true for everything but binary.
66
+ # * stored - If the attribute should the attribute be stored in SOLR.
67
+ # Defaults to true for everything but binary. (see note)
68
+ # * sortable - If the attribute should be sortable by SOLR.
69
+ # Defaults to true for everything but binary and text. (see note)
70
+ # * tokenized - If the attribute should be tokenized for full-text searching within the field.
71
+ # Defaults to true for array and text. (see note)
72
+ # * fulltext - If the attribute should be included in the default field for full-text searches.
73
+ # Defaults to true for text and string.
74
+ #
75
+ # NOTES:
76
+ # * No fields are actually stored in SOLR. When a field is requested from SOLR, the field
77
+ # is retrieved from Cassandra behind the scenes and returned as if it were stored. The
78
+ # stored parameter actually controls whether SOLR will return the field at all. If a field
79
+ # is not stored then asking SOLR for it will return a nil value. It will also not be
80
+ # included in the field list when all (*) fields are requested.
81
+ # * If you want a field both sortable and searchable (e.g., a subject) then declare it a
82
+ # text field with <tt>:sortable => true</tt>. This will create two copies of the field in SOLR,
83
+ # one that gets tokenized and one that is a single token for sorting. As this inflates the
84
+ # size of the index, you don't want to do this for large fields (which probably don't make
85
+ # sense to sort on anyways).
86
+ # * Arrays are tokenized specially. Each element of the array is treated as a single token.
87
+ # This means that you can match against any single element, but you cannot search within
88
+ # elements. This functionality may be added at a later time.
89
+ #
90
+ # EXAMPLE:
91
+ #
92
+ # class Person < DatastaxRails::Base
93
+ # key :uuid
94
+ # string :first_name
95
+ # string :user_name
96
+ # text :bio
97
+ # date :birthdate
98
+ # boolean :active
99
+ # timestamps
100
+ # end
101
+ #
102
+ # == Schemas
103
+ #
104
+ # Cassandra itself is a 'schema-optional' database. In general, DSR does not make use of
105
+ # Cassandra schemas. SOLR on the other hand does use a schema to define the data and how
106
+ # it should be indexed. There is a rake task to upload the latest SOLR schema based on
107
+ # the model files. When this happens, if the column family does not exist yet, it will be
108
+ # created. Therefore, migrations to create column families are unnecessary. If the
109
+ # column family does exist, and the new schema differs, the columns that are changed will
110
+ # be automatically reindexed.
111
+ #
112
+ # TODO: Need a way to remove ununsed column families.
113
+ #
114
+ # == Creation
115
+ #
116
+ # DatastaxRails objects accept constructor parameters either in a hash or as a block. The hash
117
+ # method is especially useful when you're receiving the data from somewhere else, like an
118
+ # HTTP request. It works like this:
119
+ #
120
+ # user = User.new(:name => "David", :occupation => "Code Artist")
121
+ # user.name # => "David"
122
+ #
123
+ # You can also use block initialization:
124
+ #
125
+ # user = User.new do |u|
126
+ # u.name = "David"
127
+ # u.occupation = "Code Artist"
128
+ # end
129
+ #
130
+ # And of course you can just create a bare object and specify the attributes after the fact:
131
+ #
132
+ # user = User.new
133
+ # user.name = "David"
134
+ # user.occupation = "Code Artist"
135
+ #
136
+ # == Conditions
137
+ #
138
+ # Conditions are specified as a hash representing key/value pairs that will eventually be passed to SOLR or as
139
+ # a chained call for greater_than and less_than conditions. In addition, fulltext queries may be specified as a
140
+ # string that will eventually be parsed by SOLR as a standard SOLR query.
141
+ #
142
+ # A simple hash without a statement will generate conditions based on equality using boolean AND logic.
143
+ # For instance:
144
+ #
145
+ # Student.where(:first_name => "Harvey", :status => 1)
146
+ # Student.where(params[:student])
147
+ #
148
+ # A range may be used in the hash to use a SOLR range query:
149
+ #
150
+ # Student.where(:grade => 9..12)
151
+ #
152
+ # An array may be used in the hash to construct a SOLR OR query:
153
+ #
154
+ # Student.where(:grade => [9,11,12])
155
+ #
156
+ # Inequality can be tested for like so:
157
+ #
158
+ # Student.where_not(:grade => 9)
159
+ # Student.where(:grade).greater_than(9)
160
+ # Student.where(:grade).less_than(10)
161
+ #
162
+ # Fulltext searching is natively supported. All text fields are automatically indexed for fulltext
163
+ # searching.
164
+ #
165
+ # Post.fulltext('Apple AND "iPhone 4s"')
166
+ #
167
+ # See the documentation on DatastaxRails::SearchMethods for more information and examples.
168
+ #
169
+ # == Overwriting default accessors
170
+ #
171
+ # All column values are automatically available through basic accessors on the DatastaxRails,
172
+ # but sometimes you want to specialize this behavior. This can be done by overwriting
173
+ # the default accessors (using the same name as the attribute) and calling
174
+ # <tt>read_attribute(attr_name)</tt> and <tt>write_attribute(attr_name, value)</tt> to actually
175
+ # change things.
176
+ #
177
+ # class Song < DatastaxRails::Base
178
+ # # Uses an integer of seconds to hold the length of the song
179
+ #
180
+ # def length=(minutes)
181
+ # write_attribute(:length, minutes.to_i * 60)
182
+ # end
183
+ #
184
+ # def length
185
+ # read_attribute(:length) / 60
186
+ # end
187
+ # end
188
+ #
189
+ # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt>
190
+ # instead of <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>.
191
+ #
192
+ # == Dynamic attribute-based finders
193
+ #
194
+ # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects
195
+ # by simple queries without using where chains. They work by appending the name of an attribute
196
+ # to <tt>find_by_</tt> or <tt>find_all_by_</tt> and thus produces finders
197
+ # like <tt>Person.find_by_user_name</tt>, <tt>Person.find_all_by_last_name</tt>, and
198
+ # <tt>Payment.find_by_transaction_id</tt>. Instead of writing
199
+ # <tt>Person.where(:user_name => user_name).first</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>.
200
+ # And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do
201
+ # <tt>Person.find_all_by_last_name(last_name)</tt>.
202
+ #
203
+ # It's also possible to use multiple attributes in the same find by separating them with "_and_".
204
+ #
205
+ # Person.where(:user_name => user_name, :password => password).first
206
+ # Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
207
+ #
208
+ # It's even possible to call these dynamic finder methods on relations and named scopes.
209
+ #
210
+ # Payment.order("created_on").find_all_by_amount(50)
211
+ # Payment.pending.find_last_by_amount(100)
212
+ #
213
+ # The same dynamic finder style can be used to create the object if it doesn't already exist.
214
+ # This dynamic finder is called with <tt>find_or_create_by_</tt> and will return the object if
215
+ # it already exists and otherwise creates it, then returns it. Protected attributes won't be set
216
+ # unless they are given in a block.
217
+ #
218
+ # NOTE: This functionality is currently unimplemented but will be in a release in the near future.
219
+ #
220
+ # # No 'Summer' tag exists
221
+ # Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")
222
+ #
223
+ # # Now the 'Summer' tag does exist
224
+ # Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
225
+ #
226
+ # # Now 'Bob' exist and is an 'admin'
227
+ # User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true }
228
+ #
229
+ # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without
230
+ # saving it first. Protected attributes won't be set unless they are given in a block.
231
+ #
232
+ # # No 'Winter' tag exists
233
+ # winter = Tag.find_or_initialize_by_name("Winter")
234
+ # winter.persisted? # false
235
+ #
236
+ # Just like <tt>find_by_*</tt>, you can also use <tt>scoped_by_*</tt> to retrieve data. The good thing about
237
+ # using this feature is that the very first time result is returned using <tt>method_missing</tt> technique
238
+ # but after that the method is declared on the class. Henceforth <tt>method_missing</tt> will not be hit.
239
+ #
240
+ # User.scoped_by_user_name('David')
241
+ #
242
+ # == Exceptions
243
+ #
244
+ # * DatastaxRailsError - Generic error class and superclass of all other errors raised by DatastaxRails.
245
+ # * AssociationTypeMismatch - The object assigned to the association wasn't of the type
246
+ # specified in the association definition.
247
+ # * ConnectionNotEstablished+ - No connection has been established. Use <tt>establish_connection</tt>
248
+ # before querying.
249
+ # * RecordNotFound - No record responded to the +find+ method. Either the row with the given ID doesn't exist
250
+ # or the row didn't meet the additional restrictions. Some +find+ calls do not raise this exception to signal
251
+ # nothing was found, please check its documentation for further details.
252
+ # * MultiparameterAssignmentErrors - Collection of errors that occurred during a mass assignment using the
253
+ # <tt>attributes=</tt> method. The +errors+ property of this exception contains an array of
254
+ # AttributeAssignmentError objects that should be inspected to determine which attributes triggered the errors.
255
+ # * AttributeAssignmentError - An error occurred while doing a mass assignment through the
256
+ # <tt>attributes=</tt> method.
257
+ # You can inspect the +attribute+ property of the exception object to determine which attribute
258
+ # triggered the error.
259
+ #
260
+ # See the documentation for SearchMethods for more examples of using the search API.
261
+ class Base
262
+ extend ActiveModel::Naming
263
+ include ActiveModel::Conversion
264
+ extend ActiveSupport::DescendantsTracker
265
+ include ActiveModel::MassAssignmentSecurity
266
+
267
+ include Connection
268
+ include Consistency
269
+ include Identity
270
+ include FinderMethods
271
+ include Batches
272
+ include AttributeMethods
273
+ include AttributeMethods::Dirty
274
+ include AttributeMethods::Typecasting
275
+ include Persistence
276
+ include Callbacks
277
+ include Validations
278
+ include Reflection
279
+ include Associations
280
+ include Scoping
281
+ include Timestamps
282
+ include Serialization
283
+ include Migrations
284
+ # include Mocking
285
+
286
+ # Stores the default scope for the class
287
+ class_attribute :default_scopes, :instance_writer => false
288
+ self.default_scopes = []
289
+
290
+ # Stores the configuration information
291
+ class_attribute :config
292
+
293
+ class_attribute :models
294
+ self.models = []
295
+
296
+ attr_reader :attributes
297
+ attr_accessor :key
298
+
299
+ def initialize(attributes = {}, options = {})
300
+ @key = attributes.delete(:key)
301
+ @attributes = {}
302
+
303
+ @relation = nil
304
+ @new_record = true
305
+ @destroyed = false
306
+ @previously_changed = {}
307
+ @changed_attributes = {}
308
+ @schema_version = self.class.current_schema_version
309
+
310
+ populate_with_current_scope_attributes
311
+
312
+ sanitize_for_mass_assignment(attributes).each do |k,v|
313
+ if respond_to?("#{k.to_s.downcase}=")
314
+ send("#{k.to_s.downcase}=",v)
315
+ else
316
+ raise(UnknownAttributeError, "unknown attribute: #{k}")
317
+ end
318
+ end
319
+
320
+ yield self if block_given?
321
+ run_callbacks :initialize
322
+ end
323
+
324
+ # Freeze the attributes hash such that associations are still accessible, even on destroyed records.
325
+ def freeze
326
+ @attributes.freeze; self
327
+ end
328
+
329
+ # Returns +true+ if the attributes hash has been frozen.
330
+ def frozen?
331
+ @attributes.frozen?
332
+ end
333
+
334
+ def to_param
335
+ id.to_s if persisted?
336
+ end
337
+
338
+ def hash
339
+ id.hash
340
+ end
341
+
342
+ def ==(comparison_object)
343
+ comparison_object.equal?(self) ||
344
+ (comparison_object.instance_of?(self.class) &&
345
+ comparison_object.key == key &&
346
+ !comparison_object.new_record?)
347
+ end
348
+
349
+ def eql?(comparison_object)
350
+ self == (comparison_object)
351
+ end
352
+
353
+ # Allows you to set all the attributes for a particular mass-assignment
354
+ # security role by passing in a hash of attributes with keys matching
355
+ # the attribute names (which again matches the column names) and the role
356
+ # name using the :as option.
357
+ #
358
+ # To bypass mass-assignment security you can use the :without_protection => true
359
+ # option.
360
+ #
361
+ # class User < ActiveRecord::Base
362
+ # attr_accessible :name
363
+ # attr_accessible :name, :is_admin, :as => :admin
364
+ # end
365
+ #
366
+ # user = User.new
367
+ # user.assign_attributes({ :name => 'Josh', :is_admin => true })
368
+ # user.name # => "Josh"
369
+ # user.is_admin? # => false
370
+ #
371
+ # user = User.new
372
+ # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
373
+ # user.name # => "Josh"
374
+ # user.is_admin? # => true
375
+ #
376
+ # user = User.new
377
+ # user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
378
+ # user.name # => "Josh"
379
+ # user.is_admin? # => true
380
+ def assign_attributes(new_attributes, options = {})
381
+ return unless new_attributes
382
+
383
+ attributes = new_attributes.stringify_keys
384
+ multi_parameter_attributes = []
385
+ @mass_assignment_options = options
386
+
387
+ unless options[:without_protection]
388
+ attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
389
+ end
390
+
391
+ attributes.each do |k, v|
392
+ if respond_to?("#{k.to_s.downcase}=")
393
+ send("#{k.to_s.downcase}=",v)
394
+ else
395
+ raise(UnknownAttributeError, "unknown attribute: #{k}")
396
+ end
397
+ end
398
+
399
+ @mass_assignment_options = nil
400
+ end
401
+
402
+ def attribute_names
403
+ self.class.attribute_names
404
+ end
405
+
406
+ private
407
+ def populate_with_current_scope_attributes
408
+ return unless self.class.scope_attributes?
409
+
410
+ self.class.scope_attributes.each do |att, value|
411
+ send("#{att}=", value) if respond_to?("#{att}=")
412
+ end
413
+ end
414
+
415
+ class << self
416
+ delegate :find, :first, :all, :exists?, :any?, :many?, :to => :scoped
417
+ delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
418
+ # delegate :find_each, :find_in_batches, :to => :scoped
419
+ delegate :order, :limit, :where, :where_not, :page, :paginate, :select, :to => :scoped
420
+ delegate :per_page, :each, :group, :total_pages, :search, :fulltext, :to => :scoped
421
+ delegate :count, :first, :first!, :last, :last!, :to => :scoped
422
+ delegate :cql, :with_cassandra, :with_solr, :commit_solr, :to => :scoped
423
+
424
+ def column_family=(column_family)
425
+ @column_family = column_family
426
+ end
427
+
428
+ def column_family
429
+ @column_family || name.pluralize
430
+ end
431
+
432
+ def base_class
433
+ klass = self
434
+ while klass.superclass != Base
435
+ klass = klass.superclass
436
+ end
437
+ klass
438
+ end
439
+
440
+ # def find(*keys)
441
+ # scoped.with_cassandra.find(keys)
442
+ # end
443
+
444
+ def find_by_id(id)
445
+ scoped.with_cassandra.find(id)
446
+ rescue RecordNotFound
447
+ nil
448
+ end
449
+
450
+ def logger
451
+ Rails.logger
452
+ end
453
+
454
+ def respond_to?(method_id, include_private = false)
455
+ if match = ActiveRecord::DynamicFinderMatch.match(method_id)
456
+ return true if all_attributes_exists?(match.attribute_names)
457
+ elsif match = ActiveRecord::DynamicScopeMatch.match(method_id)
458
+ return true if all_attributes_exists?(match.attribute_names)
459
+ end
460
+
461
+ super
462
+ end
463
+
464
+ # Returns an array of attribute names as strings
465
+ def attribute_names
466
+ @attribute_names ||= attribute_definitions.keys.collect {|a|a.to_s}
467
+ end
468
+
469
+ # SOLR always paginates all requests. There is no way to disable it, so we are
470
+ # setting the default page size to an arbitrarily high number so that we effectively
471
+ # remove pagination. If you instead want a model set to something more sane, then
472
+ # override this method in your model and set it. Of course, the page size can
473
+ # always be raised or lowered for an individual request.
474
+ #
475
+ # class Model < DatastaxRails::Base
476
+ # def self.default_page_size
477
+ # 30
478
+ # end
479
+ # end
480
+ def default_page_size
481
+ 100000
482
+ end
483
+
484
+ def search_ids(&block)
485
+ search = solr_search(&block)
486
+ search.raw_results.map { |result| result.primary_key }
487
+ end
488
+
489
+ protected
490
+
491
+
492
+
493
+ private
494
+
495
+ def construct_finder_relation(options = {}, scope = nil)
496
+ relation = options.is_a(Hash) ? unscoped.apply_finder_options(options) : options
497
+ relation = scope.merge(relation) if scope
498
+ relation
499
+ end
500
+
501
+ # Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
502
+ # <tt>User.scoped_by_user_name(user_name).
503
+ #
504
+ # It's even possible to use all the additional parameters to +find+. For example, the
505
+ # full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
506
+ #
507
+ # Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
508
+ # is first invoked, so that future attempts to use it do not run through method_missing.
509
+ def method_missing(method_id, *arguments, &block)
510
+ if match = ActiveRecord::DynamicFinderMatch.match(method_id)
511
+ attribute_names = match.attribute_names
512
+ super unless all_attributes_exists?(attribute_names)
513
+ if !arguments.first.is_a?(Hash) && arguments.size < attribute_names.size
514
+ ActiveSupport::Deprecation.warn(
515
+ "Calling dynamic finder with less number of arguments than the number of attributes in " \
516
+ "method name is deprecated and will raise an ArguementError in the next version of Rails. " \
517
+ "Please passing `nil' to the argument you want it to be nil."
518
+ )
519
+ end
520
+ if match.finder?
521
+ options = arguments.extract_options!
522
+ relation = options.any? ? scoped(options) : scoped
523
+ relation.send :find_by_attributes, match, attribute_names, *arguments
524
+ elsif match.instantiator?
525
+ scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
526
+ end
527
+ elsif match = ActiveRecord::DynamicScopeMatch.match(method_id)
528
+ attribute_names = match.attribute_names
529
+ super unless all_attributes_exists?(attribute_names)
530
+ if arguments.size < attribute_names.size
531
+ ActiveSupport::Deprecation.warn(
532
+ "Calling dynamic scope with less number of arguments than the number of attributes in " \
533
+ "method name is deprecated and will raise an ArguementError in the next version of Rails. " \
534
+ "Please passing `nil' to the argument you want it to be nil."
535
+ )
536
+ end
537
+ if match.scope?
538
+ self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
539
+ def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
540
+ attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
541
+ scoped(:conditions => attributes) # scoped(:conditions => attributes)
542
+ end # end
543
+ METHOD
544
+ send(method_id, *arguments)
545
+ end
546
+ else
547
+ super
548
+ end
549
+ end
550
+
551
+ def all_attributes_exists?(attribute_names)
552
+ (attribute_names - self.attribute_names).empty?
553
+ end
554
+
555
+ def relation #:nodoc:
556
+ @relation ||= Relation.new(self, column_family)
557
+ end
558
+
559
+ # Returns the class type of the record using the current module as a prefix. So descendants of
560
+ # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
561
+ def compute_type(type_name)
562
+ if type_name.match(/^::/)
563
+ # If the type is prefixed with a scope operator then we assume that
564
+ # the type_name is an absolute reference.
565
+ ActiveSupport::Dependencies.constantize(type_name)
566
+ else
567
+ # Build a list of candidates to search for
568
+ candidates = []
569
+ name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
570
+ candidates << type_name
571
+
572
+ candidates.each do |candidate|
573
+ begin
574
+ constant = ActiveSupport::Dependencies.constantize(candidate)
575
+ return constant if candidate == constant.to_s
576
+ rescue NameError => e
577
+ # We don't want to swallow NoMethodError < NameError errors
578
+ raise e unless e.instance_of?(NameError)
579
+ end
580
+ end
581
+
582
+ raise NameError, "uninitialized constant #{candidates.first}"
583
+ end
584
+ end
585
+ end
586
+ end
587
+ end
@@ -0,0 +1,35 @@
1
+ module DatastaxRails
2
+ module Batches
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def find_each
7
+ connection.each(column_family) do |k, v|
8
+ yield instantiate(k, v)
9
+ end
10
+ end
11
+
12
+ def find_in_batches(options = {})
13
+ batch_size = options.delete(:batch_size) || 1000
14
+
15
+ batch = []
16
+
17
+ find_each do |record|
18
+ batch << record
19
+ if batch.size == batch_size
20
+ yield(batch)
21
+ batch = []
22
+ end
23
+ end
24
+
25
+ if batch.size > 0
26
+ yield batch
27
+ end
28
+ end
29
+
30
+ def batch(&block)
31
+ connection.batch(&block)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ module DatastaxRails
2
+ module Callbacks
3
+ extend ActiveSupport::Concern
4
+
5
+ CALLBACKS = [
6
+ :after_initialize, :after_find, :after_touch, :before_validation, :after_validation,
7
+ :before_save, :around_save, :after_save, :before_create, :around_create,
8
+ :after_create, :before_update, :around_update, :after_update,
9
+ :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback
10
+ ]
11
+
12
+
13
+ included do
14
+ extend ActiveModel::Callbacks
15
+ include ActiveModel::Validations::Callbacks
16
+ define_model_callbacks :initialize, :find, :touch, :only => :after
17
+ define_model_callbacks :save, :create, :update, :destroy
18
+ end
19
+
20
+ def destroy #:nodoc:
21
+ _run_destroy_callbacks { super }
22
+ end
23
+
24
+ private
25
+ def create_or_update #:nodoc:
26
+ _run_save_callbacks { super }
27
+ end
28
+
29
+ def create #:nodoc:
30
+ _run_create_callbacks { super }
31
+ end
32
+
33
+ def update(*) #:nodoc:
34
+ _run_update_callbacks { super }
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ module DatastaxRails
2
+ class Collection < Array
3
+ attr_accessor :last_column_name, :total_entries
4
+
5
+ def inspect
6
+ "<DatastaxRails::Collection##{object_id} contents: #{super} last_column_name: #{last_column_name.inspect}>"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ module DatastaxRails
2
+ module Connection
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :connection
7
+ end
8
+
9
+ module ClassMethods
10
+ DEFAULT_OPTIONS = {
11
+ :servers => "127.0.0.1:9160",
12
+ :thrift => {}
13
+ }
14
+ def establish_connection(spec)
15
+ DatastaxRails::Base.config = spec.with_indifferent_access
16
+ spec.reverse_merge!(DEFAULT_OPTIONS)
17
+ self.connection = CassandraCQL::Database.new(spec[:servers], :keyspace => spec[:keyspace])
18
+ end
19
+ end
20
+ end
21
+ end