datastax_rails 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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,212 @@
|
|
1
|
+
module DatastaxRails
|
2
|
+
module Schema
|
3
|
+
|
4
|
+
class Migrator
|
5
|
+
|
6
|
+
def self.migrate(migrations_path, target_version = nil)
|
7
|
+
case
|
8
|
+
when target_version.nil?
|
9
|
+
up(migrations_path, target_version)
|
10
|
+
when current_version == 0 && target_version == 0
|
11
|
+
when current_version > target_version
|
12
|
+
down(migrations_path, target_version)
|
13
|
+
else
|
14
|
+
up(migrations_path, target_version)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.rollback(migrations_path, steps = 1)
|
19
|
+
move(:down, migrations_path, steps)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.forward(migrations_path, steps = 1)
|
23
|
+
move(:up, migrations_path, steps)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.up(migrations_path, target_version = nil)
|
27
|
+
new(:up, migrations_path, target_version).migrate
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.down(migrations_path, target_version = nil)
|
31
|
+
new(:down, migrations_path, target_version).migrate
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.run(direction, migrations_path, target_version)
|
35
|
+
new(direction, migrations_path, target_version).run
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.migrations_path
|
39
|
+
'ks/migrate'
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.schema_migrations_column_family
|
43
|
+
:schema_migrations
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.column_family_tasks
|
47
|
+
cas = DatastaxRails::Base.connection
|
48
|
+
Tasks::ColumnFamily.new(cas.keyspace)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.get_all_versions
|
52
|
+
cas = DatastaxRails::Base.connection
|
53
|
+
cas.get(schema_migrations_column_family, 'all').map {|(name, _value)| name.to_i}.sort
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.current_version
|
57
|
+
sm_cf = schema_migrations_column_family
|
58
|
+
if column_family_tasks.exists?(sm_cf)
|
59
|
+
get_all_versions.max || 0
|
60
|
+
else
|
61
|
+
0
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def self.move(direction, migrations_path, steps)
|
68
|
+
migrator = self.new(direction, migrations_path)
|
69
|
+
start_index = migrator.migrations.index(migrator.current_migration)
|
70
|
+
|
71
|
+
if start_index
|
72
|
+
finish = migrator.migrations[start_index + steps]
|
73
|
+
version = finish ? finish.version : 0
|
74
|
+
send(direction, migrations_path, version)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
public
|
79
|
+
|
80
|
+
def initialize(direction, migrations_path, target_version = nil)
|
81
|
+
sm_cf = self.class.schema_migrations_column_family
|
82
|
+
|
83
|
+
unless column_family_tasks.exists?(sm_cf)
|
84
|
+
column_family_tasks.create(sm_cf) do |cf|
|
85
|
+
cf.comparator_type = 'LongType'
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
@direction, @migrations_path, @target_version = direction, migrations_path, target_version
|
90
|
+
end
|
91
|
+
|
92
|
+
def current_version
|
93
|
+
migrated.last || 0
|
94
|
+
end
|
95
|
+
|
96
|
+
def current_migration
|
97
|
+
migrations.detect { |m| m.version == current_version }
|
98
|
+
end
|
99
|
+
|
100
|
+
def run
|
101
|
+
target = migrations.detect { |m| m.version == @target_version }
|
102
|
+
raise UnknownMigrationVersionError.new(@target_version) if target.nil?
|
103
|
+
unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
|
104
|
+
target.migrate(@direction)
|
105
|
+
record_version_state_after_migrating(target)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def migrate
|
110
|
+
current = migrations.detect { |m| m.version == current_version }
|
111
|
+
target = migrations.detect { |m| m.version == @target_version }
|
112
|
+
|
113
|
+
if target.nil? && !@target_version.nil? && @target_version > 0
|
114
|
+
raise UnknownMigrationVersionError.new(@target_version)
|
115
|
+
end
|
116
|
+
|
117
|
+
start = up? ? 0 : (migrations.index(current) || 0)
|
118
|
+
finish = migrations.index(target) || migrations.size - 1
|
119
|
+
runnable = migrations[start..finish]
|
120
|
+
|
121
|
+
# skip the last migration if we're headed down, but not ALL the way down
|
122
|
+
runnable.pop if down? && !target.nil?
|
123
|
+
|
124
|
+
runnable.each do |migration|
|
125
|
+
#puts "Migrating to #{migration.name} (#{migration.version})"
|
126
|
+
|
127
|
+
# On our way up, we skip migrating the ones we've already migrated
|
128
|
+
next if up? && migrated.include?(migration.version.to_i)
|
129
|
+
|
130
|
+
# On our way down, we skip reverting the ones we've never migrated
|
131
|
+
if down? && !migrated.include?(migration.version.to_i)
|
132
|
+
migration.announce 'never migrated, skipping'; migration.write
|
133
|
+
next
|
134
|
+
end
|
135
|
+
|
136
|
+
migration.migrate(@direction)
|
137
|
+
record_version_state_after_migrating(migration)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def migrations
|
142
|
+
@migrations ||= begin
|
143
|
+
files = Dir["#{@migrations_path}/[0-9]*_*.rb"]
|
144
|
+
|
145
|
+
migrations = files.inject([]) do |klasses, file|
|
146
|
+
version, name = file.scan(/([0-9]+)_([_a-z0-9]*).rb/).first
|
147
|
+
|
148
|
+
raise IllegalMigrationNameError.new(file) unless version
|
149
|
+
version = version.to_i
|
150
|
+
|
151
|
+
if klasses.detect { |m| m.version == version }
|
152
|
+
raise DuplicateMigrationVersionError.new(version)
|
153
|
+
end
|
154
|
+
|
155
|
+
if klasses.detect { |m| m.name == name.camelize }
|
156
|
+
raise DuplicateMigrationNameError.new(name.camelize)
|
157
|
+
end
|
158
|
+
|
159
|
+
migration = MigrationProxy.new
|
160
|
+
migration.name = name.camelize
|
161
|
+
migration.version = version
|
162
|
+
migration.filename = file
|
163
|
+
klasses << migration
|
164
|
+
end
|
165
|
+
|
166
|
+
migrations = migrations.sort_by { |m| m.version }
|
167
|
+
down? ? migrations.reverse : migrations
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def pending_migrations
|
172
|
+
already_migrated = migrated
|
173
|
+
migrations.reject { |m| already_migrated.include?(m.version.to_i) }
|
174
|
+
end
|
175
|
+
|
176
|
+
def migrated
|
177
|
+
@migrated_versions ||= self.class.get_all_versions
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
def column_family_tasks
|
183
|
+
Tasks::ColumnFamily.new(connection.keyspace)
|
184
|
+
end
|
185
|
+
|
186
|
+
def connection
|
187
|
+
DatastaxRails::Base.connection
|
188
|
+
end
|
189
|
+
|
190
|
+
def record_version_state_after_migrating(migration)
|
191
|
+
sm_cf = self.class.schema_migrations_column_family
|
192
|
+
|
193
|
+
@migrated_versions ||= []
|
194
|
+
if down?
|
195
|
+
@migrated_versions.delete(migration.version)
|
196
|
+
connection.remove sm_cf, 'all', migration.version
|
197
|
+
else
|
198
|
+
@migrated_versions.push(migration.version).sort!
|
199
|
+
connection.insert sm_cf, 'all', { migration.version => migration.name }
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def up?
|
204
|
+
@direction == :up
|
205
|
+
end
|
206
|
+
|
207
|
+
def down?
|
208
|
+
@direction == :down
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module DatastaxRails
|
2
|
+
module Schema
|
3
|
+
extend ActiveSupport::Autoload
|
4
|
+
|
5
|
+
class IrreversibleMigration < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
class DuplicateMigrationVersionError < StandardError#:nodoc:
|
9
|
+
def initialize(version)
|
10
|
+
super("Multiple migrations have the version number #{version}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class DuplicateMigrationNameError < StandardError#:nodoc:
|
15
|
+
def initialize(name)
|
16
|
+
super("Multiple migrations have the name #{name}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class UnknownMigrationVersionError < StandardError #:nodoc:
|
21
|
+
def initialize(version)
|
22
|
+
super("No migration with version number #{version}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class IllegalMigrationNameError < StandardError#:nodoc:
|
27
|
+
def initialize(name)
|
28
|
+
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
autoload :Migrator
|
33
|
+
autoload :Migration
|
34
|
+
autoload :MigrationProxy
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,394 @@
|
|
1
|
+
module DatastaxRails
|
2
|
+
module Scoping
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def scoped(options = nil)
|
7
|
+
if options
|
8
|
+
scoped.apply_finder_options(options)
|
9
|
+
else
|
10
|
+
if current_scope
|
11
|
+
current_scope.clone
|
12
|
+
else
|
13
|
+
relation.clone.tap do |scope|
|
14
|
+
scope.default_scoped = true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds a class method for retrieving and querying objects. A \scope represents a narrowing of a SOLR query,
|
21
|
+
# such as <tt>where(:color => :red).order(:size)</tt>.
|
22
|
+
#
|
23
|
+
# class Shirt < DatastaxRails::Base
|
24
|
+
# scope :red, where(:color => 'red')
|
25
|
+
# scope :dry_clean_only, where(:dry_clean => true)
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
|
29
|
+
# in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>.
|
30
|
+
#
|
31
|
+
# Note that this is simply 'syntactic sugar' for defining an actual class method:
|
32
|
+
#
|
33
|
+
# class Shirt < ActiveRecord::Base
|
34
|
+
# def self.red
|
35
|
+
# where(:color => 'red')
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it
|
40
|
+
# resembles the association object constructed by a <tt>has_many</tt> declaration. For instance,
|
41
|
+
# you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>.
|
42
|
+
# Also, just as with the association objects, named \scopes act like an Array, implementing Enumerable;
|
43
|
+
# <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt>
|
44
|
+
# all behave as if Shirt.red really was an Array.
|
45
|
+
#
|
46
|
+
# These named \scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce
|
47
|
+
# all shirts that are both red and dry clean only.
|
48
|
+
# Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
|
49
|
+
# returns the number of garments for which these criteria obtain. Similarly with
|
50
|
+
# <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
|
51
|
+
#
|
52
|
+
# All \scopes are available as class methods on the DatastaxRails::Base descendant upon which
|
53
|
+
# the \scopes were defined. But they are also available to <tt>has_many</tt> associations. If,
|
54
|
+
#
|
55
|
+
# class Person < DatastaxRails::Base
|
56
|
+
# has_many :shirts
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
|
60
|
+
# only shirts.
|
61
|
+
#
|
62
|
+
# Named \scopes can also be procedural:
|
63
|
+
#
|
64
|
+
# class Shirt < DatastaxRails::Base
|
65
|
+
# scope :colored, lambda { |color| where(:color => color) }
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
|
69
|
+
#
|
70
|
+
# Note that scopes defined with \scope will be evaluated when they are defined, rather than
|
71
|
+
# when they are used. For example, the following would be incorrect:
|
72
|
+
#
|
73
|
+
# class Post < DatastaxRails::Base
|
74
|
+
# scope :recent, where('published_at >= ?', Time.current - 1.week)
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# The example above would be 'frozen' to the <tt>Time.current</tt> value when the <tt>Post</tt>
|
78
|
+
# class was defined, and so the resultant SOLR query would always be the same. The correct
|
79
|
+
# way to do this would be via a lambda, which will re-evaluate the scope each time
|
80
|
+
# it is called:
|
81
|
+
#
|
82
|
+
# class Post < DatastaxRails::Base
|
83
|
+
# scope :recent, lambda { where('published_at >= ?', Time.current - 1.week) }
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
|
87
|
+
#
|
88
|
+
# class Shirt < DatastaxRails::Base
|
89
|
+
# scope :red, where(:color => 'red') do
|
90
|
+
# def dom_id
|
91
|
+
# 'red_shirts'
|
92
|
+
# end
|
93
|
+
# end
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# Scopes can also be used while creating/building a record.
|
97
|
+
#
|
98
|
+
# class Article < DatastaxRails::Base
|
99
|
+
# scope :published, where(:published => true)
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# Article.published.new.published # => true
|
103
|
+
# Article.published.create.published # => true
|
104
|
+
#
|
105
|
+
# Class methods on your model are automatically available
|
106
|
+
# on scopes. Assuming the following setup:
|
107
|
+
#
|
108
|
+
# class Article < DatastaxRails::Base
|
109
|
+
# scope :published, where(:published => true)
|
110
|
+
# scope :featured, where(:featured => true)
|
111
|
+
#
|
112
|
+
# def self.latest_article
|
113
|
+
# order('published_at desc').first
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# def self.titles
|
117
|
+
# map(&:title)
|
118
|
+
# end
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# We are able to call the methods like this:
|
122
|
+
#
|
123
|
+
# Article.published.featured.latest_article
|
124
|
+
# Article.featured.titles
|
125
|
+
def scope(name, scope_options = {})
|
126
|
+
name = name.to_sym
|
127
|
+
valid_scope_name?(name)
|
128
|
+
extension = Module.new(&Proc.new) if block_given?
|
129
|
+
|
130
|
+
scope_proc = lambda do |*args|
|
131
|
+
options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options
|
132
|
+
options = scoped.apply_finder_options(options) if options.is_a?(Hash)
|
133
|
+
|
134
|
+
relation = scoped.merge(options)
|
135
|
+
|
136
|
+
extension ? relation.extending(extension) : relation
|
137
|
+
end
|
138
|
+
|
139
|
+
singleton_class.send(:redefine_method, name, &scope_proc)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns a scope for this class without taking into account the default_scope.
|
143
|
+
#
|
144
|
+
# class Post < DatastaxRails::Base
|
145
|
+
# def self.default_scope
|
146
|
+
# where :published => true
|
147
|
+
# end
|
148
|
+
# end
|
149
|
+
#
|
150
|
+
# Post.all # Finds posts where +published+ is +true+
|
151
|
+
# Post.unscoped.all # Finds all posts regardless of +published+'s truthiness
|
152
|
+
#
|
153
|
+
# This method also accepts a block meaning that all queries inside the block will
|
154
|
+
# not use the default_scope:
|
155
|
+
#
|
156
|
+
# Post.unscoped {
|
157
|
+
# Post.limit(10) # Finds the first 10 posts
|
158
|
+
# }
|
159
|
+
#
|
160
|
+
# It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt>
|
161
|
+
# does not work. Assuming that <tt>published</tt> is a <tt>scope</tt> following two statements are same.
|
162
|
+
#
|
163
|
+
# Post.unscoped.published
|
164
|
+
# Post.published
|
165
|
+
def unscoped #:nodoc:
|
166
|
+
block_given? ? relation.scoping { yield } : relation
|
167
|
+
end
|
168
|
+
|
169
|
+
def before_remove_const #:nodoc:
|
170
|
+
self.current_scope = nil
|
171
|
+
end
|
172
|
+
|
173
|
+
# with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be
|
174
|
+
# <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while
|
175
|
+
# <tt>:create</tt> parameters are an attributes hash.
|
176
|
+
#
|
177
|
+
# class Article < DatastaxRails::Base
|
178
|
+
# def self.create_with_scope
|
179
|
+
# with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do
|
180
|
+
# find(1) # => WHERE blog_id = 1 AND id = 1
|
181
|
+
# a = create(1)
|
182
|
+
# a.blog_id # => 1
|
183
|
+
# end
|
184
|
+
# end
|
185
|
+
# end
|
186
|
+
#
|
187
|
+
# In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of
|
188
|
+
# <tt>where</tt> which is merged.
|
189
|
+
#
|
190
|
+
# You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method.
|
191
|
+
#
|
192
|
+
# class Article < DatastaxRails::Base
|
193
|
+
# def self.find_with_exclusive_scope
|
194
|
+
# with_scope(:find => where(:blog_id => 1).limit(1)) do
|
195
|
+
# with_exclusive_scope(:find => limit(10)) do
|
196
|
+
# all # => SELECT * from articles LIMIT 10
|
197
|
+
# end
|
198
|
+
# end
|
199
|
+
# end
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
# *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+.
|
203
|
+
def with_scope(scope = {}, action = :merge, &block)
|
204
|
+
# If another DatastaxRails class has been passed in, get its current scope
|
205
|
+
scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope)
|
206
|
+
|
207
|
+
previous_scope = self.current_scope
|
208
|
+
|
209
|
+
if scope.is_a?(Hash)
|
210
|
+
# Dup first and second level of hash (method and params).
|
211
|
+
scope = scope.dup
|
212
|
+
scope.each do |method, params|
|
213
|
+
scope[method] = params.dup unless params == true
|
214
|
+
end
|
215
|
+
|
216
|
+
scope.assert_valid_keys([ :find, :create ])
|
217
|
+
relation = construct_finder_relation(scope[:find] || {})
|
218
|
+
relation.default_scoped = true unless action == :overwrite
|
219
|
+
|
220
|
+
if previous_scope && previous_scope.create_with_value && scope[:create]
|
221
|
+
scope_for_create = if action == :merge
|
222
|
+
previous_scope.create_with_value.merge(scope[:create])
|
223
|
+
else
|
224
|
+
scope[:create]
|
225
|
+
end
|
226
|
+
|
227
|
+
relation = relation.create_with(scope_for_create)
|
228
|
+
else
|
229
|
+
scope_for_create = scope[:create]
|
230
|
+
scope_for_create ||= previous_scope.create_with_value if previous_scope
|
231
|
+
relation = relation.create_with(scope_for_create) if scope_for_create
|
232
|
+
end
|
233
|
+
|
234
|
+
scope = relation
|
235
|
+
end
|
236
|
+
|
237
|
+
scope = previous_scope.merge(scope) if previous_scope && action == :merge
|
238
|
+
|
239
|
+
self.current_scope = scope
|
240
|
+
begin
|
241
|
+
yield
|
242
|
+
ensure
|
243
|
+
self.current_scope = previous_scope
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Works like with_scope, but discards any nested properties.
|
248
|
+
def with_exclusive_scope(method_scoping = {}, &block)
|
249
|
+
if method_scoping.values.any? { |e| e.is_a?(DatastaxRails::Relation) }
|
250
|
+
raise ArgumentError, <<-MSG
|
251
|
+
New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope:
|
252
|
+
|
253
|
+
User.unscoped.where(:active => true)
|
254
|
+
|
255
|
+
Or call unscoped with a block:
|
256
|
+
|
257
|
+
User.unscoped do
|
258
|
+
User.where(:active => true).all
|
259
|
+
end
|
260
|
+
|
261
|
+
MSG
|
262
|
+
end
|
263
|
+
with_scope(method_scoping, :overwrite, &block)
|
264
|
+
end
|
265
|
+
|
266
|
+
# Use this macro in your model to set a default scope for all operations on
|
267
|
+
# the model.
|
268
|
+
#
|
269
|
+
# class Article < DatastaxRails::Base
|
270
|
+
# default_scope where(:published => true)
|
271
|
+
# end
|
272
|
+
#
|
273
|
+
# Article.all # => all articles where published = true
|
274
|
+
#
|
275
|
+
# The <tt>default_scope</tt> is also applied while creating/building a record. It is not
|
276
|
+
# applied while updating a record.
|
277
|
+
#
|
278
|
+
# Article.new.published # => true
|
279
|
+
# Article.create.published # => true
|
280
|
+
#
|
281
|
+
# You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated:
|
282
|
+
#
|
283
|
+
# class Article < DatastaxRails::Base
|
284
|
+
# default_scope { where(:published_at => Time.now - 1.week) }
|
285
|
+
# end
|
286
|
+
#
|
287
|
+
# (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt>
|
288
|
+
# macro, and it will be called when building the default scope.)
|
289
|
+
#
|
290
|
+
# If you use multiple <tt>default_scope</tt> declarations in your model then they will
|
291
|
+
# be merged together:
|
292
|
+
#
|
293
|
+
# class Article < DatastaxRails::Base
|
294
|
+
# default_scope where(:published => true)
|
295
|
+
# default_scope where(:rating => 'G')
|
296
|
+
# end
|
297
|
+
#
|
298
|
+
# Article.all # => all articles where published = true AND rating = 'G'
|
299
|
+
#
|
300
|
+
# This is also the case with inheritance and module includes where the parent or module
|
301
|
+
# defines a <tt>default_scope</tt> and the child or including class defines a second one.
|
302
|
+
#
|
303
|
+
# If you need to do more complex things with a default scope, you can alternatively
|
304
|
+
# define it as a class method:
|
305
|
+
#
|
306
|
+
# class Article < DatastaxRails::Base
|
307
|
+
# def self.default_scope
|
308
|
+
# # Should return a scope, you can call 'super' here etc.
|
309
|
+
# end
|
310
|
+
# end
|
311
|
+
def default_scope(scope = {})
|
312
|
+
scope = Proc.new if block_given?
|
313
|
+
self.default_scopes = default_scopes + [scope]
|
314
|
+
end
|
315
|
+
|
316
|
+
def build_default_scope #:nodoc:
|
317
|
+
if method(:default_scope).owner != Base.singleton_class
|
318
|
+
evaluate_default_scope { default_scope }
|
319
|
+
elsif default_scopes.any?
|
320
|
+
evaluate_default_scope do
|
321
|
+
default_scopes.inject(relation) do |default_scope, scope|
|
322
|
+
if scope.is_a?(Hash)
|
323
|
+
default_scope.apply_finder_options(scope)
|
324
|
+
elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
|
325
|
+
default_scope.merge(scope.call)
|
326
|
+
else
|
327
|
+
default_scope.merge(scope)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def ignore_default_scope? #:nodoc:
|
335
|
+
Thread.current["#{self}_ignore_default_scope"]
|
336
|
+
end
|
337
|
+
|
338
|
+
def ignore_default_scope=(ignore) #:nodoc:
|
339
|
+
Thread.current["#{self}_ignore_default_scope"] = ignore
|
340
|
+
end
|
341
|
+
|
342
|
+
# The ignore_default_scope flag is used to prevent an infinite recursion situation where
|
343
|
+
# a default scope references a scope which has a default scope which references a scope...
|
344
|
+
def evaluate_default_scope
|
345
|
+
return if ignore_default_scope?
|
346
|
+
|
347
|
+
begin
|
348
|
+
self.ignore_default_scope = true
|
349
|
+
yield
|
350
|
+
ensure
|
351
|
+
self.ignore_default_scope = false
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# Collects attributes from scopes that should be applied when creating
|
356
|
+
# an SO instance for the particular class this is called on.
|
357
|
+
def scope_attributes # :nodoc:
|
358
|
+
if current_scope
|
359
|
+
current_scope.scope_for_create
|
360
|
+
else
|
361
|
+
relation.clone.tap do |scope|
|
362
|
+
scope.default_scoped = true
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
# Are there default attributes associated with this scope?
|
368
|
+
def scope_attributes? # :nodoc:
|
369
|
+
current_scope || default_scopes.any?
|
370
|
+
end
|
371
|
+
|
372
|
+
protected
|
373
|
+
|
374
|
+
def current_scope #:nodoc:
|
375
|
+
Thread.current["#{self}_current_scope"]
|
376
|
+
end
|
377
|
+
|
378
|
+
def current_scope=(scope) #:nodoc:
|
379
|
+
Thread.current["#{self}_current_scope"] = scope
|
380
|
+
end
|
381
|
+
|
382
|
+
def apply_default_scope
|
383
|
+
|
384
|
+
end
|
385
|
+
|
386
|
+
def valid_scope_name?(name)
|
387
|
+
if respond_to?(name, true)
|
388
|
+
logger.warn "Creating scope :#{name}. " \
|
389
|
+
"Overwriting existing method #{self.name}.#{name}."
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|