georgepalmer-couch_foo 0.7.1

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/README.rdoc ADDED
@@ -0,0 +1,113 @@
1
+ = CouchFoo
2
+
3
+ == Overview
4
+
5
+ CouchFoo provides an ActiveRecord styled interface to CouchDB. The external API is nearly identical
6
+ to ActiveRecord so it should be possible to migrate your applications quite easily. That said, there
7
+ are a few minor differences to the way CouchDB works. In particular:
8
+ * CouchDB is schema free so property defintions for the document are defined in the model (like DataMapper)
9
+ * :select, :joins, :having, :group, :from and :lock are not available on find or associations as they don't apply (locking is handled as conflict resolution at insertion time)
10
+ * :conditions can only accept a hash and not an array or SQL. For example :conditions => {:user_name => "Georgio_1999"}
11
+ * :offset is less efficient in CouchDB - there's more on this in the rdoc
12
+ * :order is applied after results are retrieved from the database. Therefore :order cannot be used with :limit without a new option :use_key. This is explained fully in the quick start guide and CouchFoo#find documentation
13
+ * :include isn't implemented yet but the finders and associations still accept the option so you won't need to make any code changes
14
+ * By default results are ordered by document key. The key uses a UUID scheme so these don't auto-increment and are likely to come out in a different order to insertion. default_sort can be used on a model to sort by create date by default and overcome this
15
+ * validates_uniqueness_of has had the :case_sensitive option removed
16
+ * Because there's no SQL there's no SQL finder methods
17
+ * Timezones, aggregations and fixtures are not yet implemented
18
+ * The price of index updating is paid when next accessing the index rather than the point of insertion. This can be more efficient or less depending on your application. It may make sense to use an external process to do the updating for you - see CouchFoo#find for more on this
19
+ * On that note, occasional compacting of CouchDB is required to recover space from old versions of documents. This can be kicked off in several ways (see quick start guide)
20
+
21
+ It is recommend that you read the quick start and performance sections in the rdoc for a full overview of differences and points to be aware of when developing.
22
+
23
+
24
+ == Getting started
25
+
26
+ CouchFoo::Base.set_database("http://localhost:5984/opnli_dev", <yourcouchdbversion>)
27
+
28
+ If using with Rails you will need to create an initializer to do this (until proper integration is added)
29
+
30
+
31
+ == Examples of usage
32
+
33
+ Basic operations are the same as ActiveRecord:
34
+ class Address < CouchFoo::Base
35
+ property :number, Integer
36
+ property :street, String
37
+ property :postcode # Any generic type is fine as long as .to_json can be called on it
38
+ end
39
+
40
+ address1 = Address.create(:number => 3, :street => "My Street", :postcode => "secret") # Create address
41
+ address2 = Address.create(:number => 27, :street => "Another Street", :postcode => "secret")
42
+ Address.find.all # = [address1, address2] or maybe [address2, address2] depending on key generation
43
+ Address.first # = address1 or address2 depending on keys so probably isn't as expected
44
+ Address.find_by_street("My Street") # = address1
45
+
46
+ As key generation is through a UUID scheme, the order can't be predicted. However you can order the
47
+ results by default:
48
+ class Address < CouchFoo::Base
49
+ property :number, Integer
50
+ property :street, String
51
+ property :postcode # Any generic type is fine as long as .to_json can be called on it
52
+ property :created_at, DateTime
53
+
54
+ default_sort :created_at
55
+ end
56
+
57
+ Address.find.all # = [address1, address2]
58
+ Address.first # = address1 or address2, sorting is applied after results
59
+ Address.first(:use_key => :created_at) # = address1 but at the price of creating a new index
60
+
61
+ Conditions work slightly differently:
62
+ Address.find(:all, :conditions {:street => "My Street"}) # = address1, creates index on :street
63
+ Address.find(:all, :conditions {:created_at => "sometime"}) # Uses same index as :use_key => :created_at
64
+ Address.find(:all, :use_key => :street, :startkey => 'p') # All streets from p in alphabet, reuses the index created 2 lines up
65
+
66
+ As well as providing support for people using relational databases, CouchFoo attempts to provide a library for those wanting to use CouchDB as a document-orientated database:
67
+ class Document < CouchFoo::Base
68
+ property :number, Integer
69
+ property :street, String
70
+
71
+ view :number_ordered, "function(doc) {emit([doc.number , doc.street], doc); }", nil, :descending => true
72
+ end
73
+
74
+ Document.number_ordered(:limit => 75) # Will get the last 75 documents in the database ordered by number, street attributes
75
+
76
+ Associations work as expected but you must to remember to add the properties required for an association (we'll make this automatic soon):
77
+ class House < CouchFoo::Base
78
+ has_many :windows
79
+ end
80
+
81
+ class Window < CouchFoo::Base
82
+ property :house_id, String
83
+ belongs_to :house
84
+ end
85
+
86
+
87
+ == Credits
88
+
89
+ This gem was inspired some excellent work on CouchPotato, CouchREST, ActiveCouch and RelaxDB gems. Each offered
90
+ its own benefits and own challenges. After hacking with each I couldn't get a library was happy with. So I
91
+ started with ActiveRecord and modified it to work with CouchDB. Some areas required more work than others but
92
+ a lot of features were achieved for free once the base level of functionality had been achieved. Credit to DHH,
93
+ the rails core guys and the CouchDB gems that inspired this work.
94
+
95
+
96
+ == What's left to do?
97
+
98
+ Please feel free to fork and hit me with a request to merge back in. At the moment, the following areas need addressing:
99
+
100
+ * Proper integration with rails using a couchdb.yml config file
101
+ * Example ruby script for updating indexes as an external process, and the same for database compacting
102
+ * :include for database queries
103
+ * Remove dependency from CouchRest? Add support for attachments and log calls to server so know performance times. General tidy up of database.rb
104
+ * inline and HABTM inline associations. Also x_id and x_type properties should be automatically defined
105
+ * compatability with willpaginate and friends
106
+ * cleanup of associations, reflection, validations classes (were modified in a rush)
107
+ * has_many :through
108
+ * Rest of calculations, timezones, aggregations and fixtures
109
+ * Diff with AR to check got everything in
110
+ * DataMapper interface
111
+ * Tool to migrate DB from MySQL to CouchDB
112
+ * some kind of generic admin interface where can just browse around data structure?
113
+ * grep of code and doc for records and columns, should be documents and attributes
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 7
4
+ :patch: 1
data/lib/boolean.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Boolean < TrueClass
2
+
3
+ end
@@ -0,0 +1,346 @@
1
+ require 'set'
2
+
3
+ module CouchFoo
4
+ module Associations
5
+ class AssociationCollection < AssociationProxy #:nodoc:
6
+ def initialize(owner, reflection)
7
+ super
8
+ construct_conditions
9
+ end
10
+
11
+ def find(*args)
12
+ options = args.extract_options!
13
+
14
+ options[:conditions] = @association_conditions.merge(options[:conditions] || {})
15
+
16
+ # Multiple ordering not supported at the minute
17
+ #if options[:order] && @reflection.options[:order]
18
+ # options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
19
+ #elsif @reflection.options[:order]
20
+ if @reflection.options[:order]
21
+ options[:order] = @reflection.options[:order]
22
+ end
23
+
24
+ # Build options specific to association
25
+ construct_find_options!(options)
26
+
27
+ merge_options_from_reflection!(options)
28
+
29
+ # Pass through args exactly as we received them.
30
+ args << options
31
+
32
+ @reflection.klass.find(*args)
33
+ end
34
+
35
+ # Fetches the first element directly if it can
36
+ def first(*args)
37
+ if fetch_first_or_last_using_find? args
38
+ find(:first, *args)
39
+ else
40
+ load_target unless loaded?
41
+ @target.first(*args)
42
+ end
43
+ end
44
+
45
+ # Fetches the last element directly if it can
46
+ def last(*args)
47
+ if fetch_first_or_last_using_find? args
48
+ find(:last, *args)
49
+ else
50
+ load_target unless loaded?
51
+ @target.last(*args)
52
+ end
53
+ end
54
+
55
+ def to_ary
56
+ load_target
57
+ @target.to_ary
58
+ end
59
+
60
+ def reset
61
+ reset_target!
62
+ @loaded = false
63
+ end
64
+
65
+ def build(attributes = {}, &block)
66
+ if attributes.is_a?(Array)
67
+ attributes.collect { |attr| build(attr, &block) }
68
+ else
69
+ build_record(attributes) do |record|
70
+ block.call(record) if block_given?
71
+ set_belongs_to_association_for(record)
72
+ end
73
+ end
74
+ end
75
+
76
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
77
+ # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
78
+ def <<(*records)
79
+ result = true
80
+ load_target if @owner.new_record?
81
+
82
+ @owner.transaction do
83
+ flatten_deeper(records).each do |record|
84
+ raise_on_type_mismatch(record)
85
+ add_record_to_target_with_callbacks(record) do |r|
86
+ result &&= insert_record(record) unless @owner.new_record?
87
+ end
88
+ end
89
+ end
90
+
91
+ result && self
92
+ end
93
+
94
+ alias_method :push, :<<
95
+ alias_method :concat, :<<
96
+
97
+ # Remove all records from this association
98
+ def delete_all
99
+ load_target
100
+ delete(@target)
101
+ reset_target!
102
+ end
103
+
104
+ # Calculate sum
105
+ def sum(*args)
106
+ if block_given?
107
+ calculate(:sum, *args) { |*block_args| yield(*block_args) }
108
+ else
109
+ calculate(:sum, *args)
110
+ end
111
+ end
112
+
113
+ # Remove +records+ from this association. Does not destroy +records+.
114
+ def delete(*records)
115
+ records = flatten_deeper(records)
116
+ records.each { |record| raise_on_type_mismatch(record) }
117
+
118
+ @owner.transaction do
119
+ records.each { |record| callback(:before_remove, record) }
120
+
121
+ old_records = records.reject {|r| r.new_record? }
122
+ delete_records(old_records) if old_records.any?
123
+
124
+ records.each do |record|
125
+ @target.delete(record)
126
+ callback(:after_remove, record)
127
+ end
128
+ end
129
+ end
130
+
131
+ # Removes all records from this association. Returns +self+ so method calls may be chained.
132
+ def clear
133
+ return self if length.zero? # forces load_target if it hasn't happened already
134
+
135
+ if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
136
+ destroy_all
137
+ else
138
+ delete_all
139
+ end
140
+
141
+ self
142
+ end
143
+
144
+ def destroy_all
145
+ @owner.transaction do
146
+ each { |record| record.destroy }
147
+ end
148
+
149
+ reset_target!
150
+ end
151
+
152
+ def create(attrs = {})
153
+ if attrs.is_a?(Array)
154
+ attrs.collect { |attr| create(attr) }
155
+ else
156
+ create_record(attrs) do |record|
157
+ yield(record) if block_given?
158
+ record.save
159
+ end
160
+ end
161
+ end
162
+
163
+ def create!(attrs = {})
164
+ create_record(attrs) do |record|
165
+ yield(record) if block_given?
166
+ record.save!
167
+ end
168
+ end
169
+
170
+ # Returns the size of the collection by executing a count query if the collection hasn't been
171
+ # loaded and calling collection.size if it has. If it's more likely than not that the
172
+ # collection does have a size larger than zero and you need to fetch that collection afterwards,
173
+ # it'll take one database query if you use length.
174
+ def size
175
+ if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
176
+ @target.size
177
+ elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
178
+ unsaved_records = @target.select { |r| r.new_record? }
179
+ unsaved_records.size + count_records
180
+ else
181
+ count_records
182
+ end
183
+ end
184
+
185
+ # Returns the size of the collection by loading it and calling size on the array. If you want
186
+ # to use this method to check whether the collection is empty, use collection.length.zero?
187
+ # instead of collection.empty?
188
+ def length
189
+ load_target.size
190
+ end
191
+
192
+ def empty?
193
+ size.zero?
194
+ end
195
+
196
+ def any?
197
+ if block_given?
198
+ method_missing(:any?) { |*block_args| yield(*block_args) }
199
+ else
200
+ !empty?
201
+ end
202
+ end
203
+
204
+ def uniq(collection = self)
205
+ seen = Set.new
206
+ collection.inject([]) do |kept, record|
207
+ unless seen.include?(record.id)
208
+ kept << record
209
+ seen << record.id
210
+ end
211
+ kept
212
+ end
213
+ end
214
+
215
+ # Replace this collection with +other_array+
216
+ # This will perform a diff and delete/add only records that have changed.
217
+ def replace(other_array)
218
+ other_array.each { |val| raise_on_type_mismatch(val) }
219
+
220
+ load_target
221
+ other = other_array.size < 100 ? other_array : other_array.to_set
222
+ current = @target.size < 100 ? @target : @target.to_set
223
+
224
+ @owner.transaction do
225
+ delete(@target.select { |v| !other.include?(v) })
226
+ concat(other_array.select { |v| !current.include?(v) })
227
+ end
228
+ end
229
+
230
+ def include?(record)
231
+ return false unless record.is_a?(@reflection.klass)
232
+ load_target if !loaded?
233
+ return @target.include?(record) if loaded?
234
+ exists?(record)
235
+ end
236
+
237
+ protected
238
+ def construct_find_options!(options)
239
+ end
240
+
241
+ def load_target
242
+ if !@owner.new_record? || foreign_key_present
243
+ begin
244
+ if !loaded?
245
+ if @target.is_a?(Array) && @target.any?
246
+ @target = find_target + @target.find_all {|t| t.new_record? }
247
+ else
248
+ @target = find_target
249
+ end
250
+ end
251
+ rescue CouchFoo::DocumentNotFound
252
+ reset
253
+ end
254
+ end
255
+
256
+ loaded if target
257
+ target
258
+ end
259
+
260
+ def method_missing(method, *args)
261
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
262
+ if block_given?
263
+ super { |*block_args| yield(*block_args) }
264
+ else
265
+ super
266
+ end
267
+ elsif @reflection.klass.scopes.include?(method)
268
+ @reflection.klass.scopes[method].call(self, *args)
269
+ else
270
+ with_scope(construct_scope) do
271
+ if block_given?
272
+ @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }
273
+ else
274
+ @reflection.klass.send(method, *args)
275
+ end
276
+ end
277
+ end
278
+ end
279
+
280
+ # overloaded in derived Association classes to provide useful scoping depending on association type.
281
+ def construct_scope
282
+ {}
283
+ end
284
+
285
+ def reset_target!
286
+ @target = Array.new
287
+ end
288
+
289
+ def find_target
290
+ @reflection.options[:uniq] ? uniq(records) : find(:all)
291
+ end
292
+
293
+ private
294
+ def create_record(attrs)
295
+ attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
296
+ ensure_owner_is_not_new
297
+ record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) }
298
+ if block_given?
299
+ add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
300
+ else
301
+ add_record_to_target_with_callbacks(record)
302
+ end
303
+ end
304
+
305
+ def build_record(attrs)
306
+ attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
307
+ record = @reflection.klass.new(attrs)
308
+ if block_given?
309
+ add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
310
+ else
311
+ add_record_to_target_with_callbacks(record)
312
+ end
313
+ end
314
+
315
+ def add_record_to_target_with_callbacks(record)
316
+ callback(:before_add, record)
317
+ yield(record) if block_given?
318
+ @target ||= [] unless loaded?
319
+ @target << record unless @reflection.options[:uniq] && @target.include?(record)
320
+ callback(:after_add, record)
321
+ record
322
+ end
323
+
324
+ def callback(method, record)
325
+ callbacks_for(method).each do |callback|
326
+ ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record)
327
+ end
328
+ end
329
+
330
+ def callbacks_for(callback_name)
331
+ full_callback_name = "#{callback_name}_for_#{@reflection.name}"
332
+ @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
333
+ end
334
+
335
+ def ensure_owner_is_not_new
336
+ if @owner.new_record?
337
+ raise CouchFoo::DocumentNotSaved, "You cannot call create unless the parent is saved"
338
+ end
339
+ end
340
+
341
+ def fetch_first_or_last_using_find?(args)
342
+ args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || !@target.blank? || args.first.kind_of?(Integer))
343
+ end
344
+ end
345
+ end
346
+ end
@@ -0,0 +1,204 @@
1
+ module CouchFoo
2
+ module Associations
3
+ # This is the root class of all association proxies:
4
+ #
5
+ # AssociationProxy
6
+ # BelongsToAssociation
7
+ # HasOneAssociation
8
+ # BelongsToPolymorphicAssociation
9
+ # AssociationCollection
10
+ # HasAndBelongsToManyAssociation
11
+ # HasManyAssociation
12
+ #
13
+ # At there moment there are no Through associations
14
+ #
15
+ # Association proxies in Couch Foo are middlemen between the object that
16
+ # holds the association, known as the <tt>@owner</tt>, and the actual associated
17
+ # object, known as the <tt>@target</tt>. The kind of association any proxy is
18
+ # about is available in <tt>@reflection</tt>. That's an instance of the class
19
+ # CouchFoo::Reflection::AssociationReflection.
20
+ #
21
+ # For example, given
22
+ #
23
+ # class Blog < CouchFoo::Base
24
+ # has_many :posts
25
+ # end
26
+ #
27
+ # blog = Blog.find(:first)
28
+ #
29
+ # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
30
+ # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
31
+ # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
32
+ #
33
+ # This class has most of the basic instance methods removed, and delegates
34
+ # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
35
+ # corner case, it even removes the +class+ method and that's why you get
36
+ #
37
+ # blog.posts.class # => Array
38
+ #
39
+ # though the object behind <tt>blog.posts</tt> is not an Array, but an
40
+ # CouchFoo::Associations::HasManyAssociation.
41
+ #
42
+ # The <tt>@target</tt> object is not loaded until needed. For example,
43
+ #
44
+ # blog.posts.count
45
+ #
46
+ # is computed directly through a count view and does not trigger by itself the
47
+ # instantiation of the actual post records.
48
+ class AssociationProxy #:nodoc:
49
+ alias_method :proxy_respond_to?, :respond_to?
50
+ alias_method :proxy_extend, :extend
51
+ delegate :to_param, :to => :proxy_target
52
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
53
+
54
+ def initialize(owner, reflection)
55
+ @owner, @reflection = owner, reflection
56
+ Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
57
+ reset
58
+ end
59
+
60
+ def proxy_owner
61
+ @owner
62
+ end
63
+
64
+ def proxy_reflection
65
+ @reflection
66
+ end
67
+
68
+ def proxy_target
69
+ @target
70
+ end
71
+
72
+ def respond_to?(*args)
73
+ proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
74
+ end
75
+
76
+ # Explicitly proxy === because the instance method removal above
77
+ # doesn't catch it.
78
+ def ===(other)
79
+ load_target
80
+ other === @target
81
+ end
82
+
83
+ def conditions
84
+ @conditions ||= @reflection.options[:conditions]
85
+ end
86
+
87
+ def reset
88
+ @loaded = false
89
+ @target = nil
90
+ end
91
+
92
+ def reload
93
+ reset
94
+ load_target
95
+ self unless @target.nil?
96
+ end
97
+
98
+ def loaded?
99
+ @loaded
100
+ end
101
+
102
+ def loaded
103
+ @loaded = true
104
+ end
105
+
106
+ def target
107
+ @target
108
+ end
109
+
110
+ def target=(target)
111
+ @target = target
112
+ loaded
113
+ end
114
+
115
+ def inspect
116
+ load_target
117
+ @target.inspect
118
+ end
119
+
120
+ protected
121
+ def dependent?
122
+ @reflection.options[:dependent]
123
+ end
124
+
125
+ def quoted_record_ids(records)
126
+ records.map { |record| record.quoted_id }.join(',')
127
+ end
128
+
129
+ def set_belongs_to_association_for(record)
130
+ if @reflection.options[:as]
131
+ record["#{@reflection.options[:as]}_id".to_sym] = @owner.id unless @owner.new_record?
132
+ record["#{@reflection.options[:as]}_type".to_sym] = @owner.class.name.to_s
133
+ else
134
+ record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
135
+ end
136
+ end
137
+
138
+ def merge_options_from_reflection!(options)
139
+ options.reverse_merge!(
140
+ :limit => @reflection.options[:limit],
141
+ :offset => @reflection.options[:offset],
142
+ :include => @reflection.options[:include],
143
+ :readonly => @reflection.options[:readonly]
144
+ )
145
+ end
146
+
147
+ def with_scope(*args, &block)
148
+ @reflection.klass.send :with_scope, *args, &block
149
+ end
150
+
151
+ private
152
+ def method_missing(method, *args)
153
+ if load_target
154
+ if block_given?
155
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
156
+ else
157
+ @target.send(method, *args)
158
+ end
159
+ end
160
+ end
161
+
162
+ # Loads the target if needed and returns it.
163
+ #
164
+ # This method is abstract in the sense that it relies on +find_target+,
165
+ # which is expected to be provided by descendants.
166
+ #
167
+ # If the target is already loaded it is just returned. Thus, you can call
168
+ # +load_target+ unconditionally to get the target.
169
+ #
170
+ # CouchFoo::DocumentNotFound is rescued within the method, and it is
171
+ # not reraised. The proxy is reset and +nil+ is the return value.
172
+ def load_target
173
+ return nil unless defined?(@loaded)
174
+
175
+ if !loaded? and (!@owner.new_record? || foreign_key_present)
176
+ @target = find_target
177
+ end
178
+
179
+ @loaded = true
180
+ @target
181
+ rescue CouchFoo::DocumentNotFound
182
+ reset
183
+ end
184
+
185
+ # Can be overwritten by associations that might have the foreign key available for an association without
186
+ # having the object itself (and still being a new record). Currently, only belongs_to presents this scenario.
187
+ def foreign_key_present
188
+ false
189
+ end
190
+
191
+ def raise_on_type_mismatch(record)
192
+ unless record.is_a?(@reflection.klass)
193
+ message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
194
+ raise CouchFoo::AssociationTypeMismatch, message
195
+ end
196
+ end
197
+
198
+ # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
199
+ def flatten_deeper(array)
200
+ array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
201
+ end
202
+ end
203
+ end
204
+ end