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.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +62 -0
- data/Rakefile +34 -0
- data/config/schema.xml +266 -0
- data/config/schema.xml.erb +70 -0
- data/config/solrconfig.xml +1564 -0
- data/config/stopwords.txt +58 -0
- data/lib/datastax_rails/associations/association.rb +224 -0
- data/lib/datastax_rails/associations/association_scope.rb +25 -0
- data/lib/datastax_rails/associations/belongs_to_association.rb +64 -0
- data/lib/datastax_rails/associations/builder/association.rb +56 -0
- data/lib/datastax_rails/associations/builder/belongs_to.rb +30 -0
- data/lib/datastax_rails/associations/builder/collection_association.rb +48 -0
- data/lib/datastax_rails/associations/builder/has_and_belongs_to_many.rb +36 -0
- data/lib/datastax_rails/associations/builder/has_many.rb +54 -0
- data/lib/datastax_rails/associations/builder/has_one.rb +52 -0
- data/lib/datastax_rails/associations/builder/singular_association.rb +56 -0
- data/lib/datastax_rails/associations/collection_association.rb +274 -0
- data/lib/datastax_rails/associations/collection_proxy.rb +118 -0
- data/lib/datastax_rails/associations/has_and_belongs_to_many_association.rb +44 -0
- data/lib/datastax_rails/associations/has_many_association.rb +58 -0
- data/lib/datastax_rails/associations/has_one_association.rb +68 -0
- data/lib/datastax_rails/associations/singular_association.rb +58 -0
- data/lib/datastax_rails/associations.rb +86 -0
- data/lib/datastax_rails/attribute_methods/definition.rb +20 -0
- data/lib/datastax_rails/attribute_methods/dirty.rb +43 -0
- data/lib/datastax_rails/attribute_methods/typecasting.rb +50 -0
- data/lib/datastax_rails/attribute_methods.rb +104 -0
- data/lib/datastax_rails/base.rb +587 -0
- data/lib/datastax_rails/batches.rb +35 -0
- data/lib/datastax_rails/callbacks.rb +37 -0
- data/lib/datastax_rails/collection.rb +9 -0
- data/lib/datastax_rails/connection.rb +21 -0
- data/lib/datastax_rails/consistency.rb +33 -0
- data/lib/datastax_rails/cql/base.rb +15 -0
- data/lib/datastax_rails/cql/column_family.rb +38 -0
- data/lib/datastax_rails/cql/consistency.rb +13 -0
- data/lib/datastax_rails/cql/create_column_family.rb +63 -0
- data/lib/datastax_rails/cql/create_keyspace.rb +30 -0
- data/lib/datastax_rails/cql/delete.rb +41 -0
- data/lib/datastax_rails/cql/drop_column_family.rb +13 -0
- data/lib/datastax_rails/cql/drop_keyspace.rb +13 -0
- data/lib/datastax_rails/cql/insert.rb +53 -0
- data/lib/datastax_rails/cql/select.rb +51 -0
- data/lib/datastax_rails/cql/truncate.rb +13 -0
- data/lib/datastax_rails/cql/update.rb +68 -0
- data/lib/datastax_rails/cql/use_keyspace.rb +13 -0
- data/lib/datastax_rails/cql.rb +25 -0
- data/lib/datastax_rails/cursor.rb +90 -0
- data/lib/datastax_rails/errors.rb +16 -0
- data/lib/datastax_rails/identity/abstract_key_factory.rb +26 -0
- data/lib/datastax_rails/identity/custom_key_factory.rb +36 -0
- data/lib/datastax_rails/identity/hashed_natural_key_factory.rb +10 -0
- data/lib/datastax_rails/identity/natural_key_factory.rb +37 -0
- data/lib/datastax_rails/identity/uuid_key_factory.rb +23 -0
- data/lib/datastax_rails/identity.rb +53 -0
- data/lib/datastax_rails/log_subscriber.rb +37 -0
- data/lib/datastax_rails/migrations/migration.rb +15 -0
- data/lib/datastax_rails/migrations.rb +36 -0
- data/lib/datastax_rails/mocking.rb +15 -0
- data/lib/datastax_rails/persistence.rb +133 -0
- data/lib/datastax_rails/railtie.rb +20 -0
- data/lib/datastax_rails/reflection.rb +472 -0
- data/lib/datastax_rails/relation/finder_methods.rb +184 -0
- data/lib/datastax_rails/relation/modification_methods.rb +80 -0
- data/lib/datastax_rails/relation/search_methods.rb +349 -0
- data/lib/datastax_rails/relation/spawn_methods.rb +107 -0
- data/lib/datastax_rails/relation.rb +393 -0
- data/lib/datastax_rails/schema/migration.rb +106 -0
- data/lib/datastax_rails/schema/migration_proxy.rb +25 -0
- data/lib/datastax_rails/schema/migrator.rb +212 -0
- data/lib/datastax_rails/schema.rb +37 -0
- data/lib/datastax_rails/scoping.rb +394 -0
- data/lib/datastax_rails/serialization.rb +6 -0
- data/lib/datastax_rails/tasks/column_family.rb +162 -0
- data/lib/datastax_rails/tasks/ds.rake +63 -0
- data/lib/datastax_rails/tasks/keyspace.rb +57 -0
- data/lib/datastax_rails/timestamps.rb +19 -0
- data/lib/datastax_rails/type.rb +16 -0
- data/lib/datastax_rails/types/array_type.rb +77 -0
- data/lib/datastax_rails/types/base_type.rb +26 -0
- data/lib/datastax_rails/types/binary_type.rb +15 -0
- data/lib/datastax_rails/types/boolean_type.rb +22 -0
- data/lib/datastax_rails/types/date_type.rb +17 -0
- data/lib/datastax_rails/types/float_type.rb +18 -0
- data/lib/datastax_rails/types/integer_type.rb +18 -0
- data/lib/datastax_rails/types/string_type.rb +16 -0
- data/lib/datastax_rails/types/text_type.rb +16 -0
- data/lib/datastax_rails/types/time_type.rb +17 -0
- data/lib/datastax_rails/types.rb +9 -0
- data/lib/datastax_rails/validations/uniqueness.rb +119 -0
- data/lib/datastax_rails/validations.rb +48 -0
- data/lib/datastax_rails/version.rb +3 -0
- data/lib/datastax_rails.rb +87 -0
- data/lib/solr_no_escape.rb +28 -0
- data/spec/datastax_rails/associations/belongs_to_association_spec.rb +7 -0
- data/spec/datastax_rails/associations/has_many_association_spec.rb +37 -0
- data/spec/datastax_rails/associations_spec.rb +22 -0
- data/spec/datastax_rails/attribute_methods_spec.rb +23 -0
- data/spec/datastax_rails/base_spec.rb +15 -0
- data/spec/datastax_rails/cql/select_spec.rb +12 -0
- data/spec/datastax_rails/cql/update_spec.rb +0 -0
- data/spec/datastax_rails/relation/finder_methods_spec.rb +54 -0
- data/spec/datastax_rails/relation/modification_methods_spec.rb +41 -0
- data/spec/datastax_rails/relation/search_methods_spec.rb +117 -0
- data/spec/datastax_rails/relation/spawn_methods_spec.rb +28 -0
- data/spec/datastax_rails/relation_spec.rb +130 -0
- data/spec/datastax_rails/validations/uniqueness_spec.rb +41 -0
- data/spec/datastax_rails_spec.rb +5 -0
- data/spec/dummy/Rakefile +8 -0
- data/spec/dummy/app/assets/javascripts/application.js +9 -0
- data/spec/dummy/app/assets/stylesheets/application.css +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +47 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/datastax.yml +18 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +30 -0
- data/spec/dummy/config/environments/production.rb +60 -0
- data/spec/dummy/config/environments/test.rb +39 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/config/sunspot.yml +17 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/ks/migrate/20111117224534_models.rb +20 -0
- data/spec/dummy/ks/schema.json +180 -0
- data/spec/dummy/log/development.log +298 -0
- data/spec/dummy/log/production.log +0 -0
- data/spec/dummy/log/test.log +20307 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/datastax_test_hook.rb +14 -0
- data/spec/support/models.rb +72 -0
- 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,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
|