Class: CouchRest::Model::Designs::View

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/couchrest/model/designs/view.rb

Overview

A proxy class that allows view queries to be created using chained method calls. After each call a new instance of the method is created based on the original in a similar fashion to ruby's Sequel library, or Rails 3's Arel.

CouchDB views have inherent limitations, so joins and filters as used in a normal relational database are not possible.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(design_doc, parent, new_query = {}, name = nil) ⇒ View

Initialize a new View object. This method should not be called from outside CouchRest Model.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/couchrest/model/designs/view.rb', line 21

def initialize(design_doc, parent, new_query = {}, name = nil)
  self.design_doc = design_doc

  # Extrace important non-regular query values
  proxy  = new_query.delete(:proxy)
  delete = new_query.delete(:delete)

  if parent.is_a?(Class) && parent < CouchRest::Model::Base
    raise "Name must be provided for view to be initialized" if name.nil?
    self.model    = (proxy || parent)
    self.owner    = parent
    self.name     = name.to_s
    # Default options:
    self.query    = { }
  elsif parent.is_a?(self.class)
    self.model    = (proxy || parent.model)
    self.owner    = parent.owner
    self.name     = parent.name
    self.query    = parent.query.dup
  else
    raise "View cannot be initialized without a parent Model or View"
  end

  # Update the local query hash
  query.update(new_query)
  delete.each{|k| query.delete(k)} if delete

  super()
end

Instance Attribute Details

#design_docObject

Returns the value of attribute design_doc



17
18
19
# File 'lib/couchrest/model/designs/view.rb', line 17

def design_doc
  @design_doc
end

#modelObject

Returns the value of attribute model



17
18
19
# File 'lib/couchrest/model/designs/view.rb', line 17

def model
  @model
end

#nameObject

Returns the value of attribute name



17
18
19
# File 'lib/couchrest/model/designs/view.rb', line 17

def name
  @name
end

#ownerObject

Returns the value of attribute owner



17
18
19
# File 'lib/couchrest/model/designs/view.rb', line 17

def owner
  @owner
end

#queryObject

Returns the value of attribute query



17
18
19
# File 'lib/couchrest/model/designs/view.rb', line 17

def query
  @query
end

#resultObject

Returns the value of attribute result



17
18
19
# File 'lib/couchrest/model/designs/view.rb', line 17

def result
  @result
end

Class Method Details

.create_model_methods(design_doc, name, opts = {}) ⇒ Object



516
517
518
519
520
521
522
523
524
525
526
527
528
529
# File 'lib/couchrest/model/designs/view.rb', line 516

def create_model_methods(design_doc, name, opts = {})
  method = design_doc.method_name
  design_doc.model.instance_eval <<-EOS, __FILE__, __LINE__ + 1
    def #{name}(opts = {})
      #{method}.view('#{name}', opts)
    end
    def find_#{name}(*key)
      #{name}.key(*key).first()
    end
    def find_#{name}!(*key)
      find_#{name}(*key) || raise(CouchRest::Model::DocumentNotFound)
    end
  EOS
end

.define(design_doc, name, opts = {}) ⇒ Object

Simplified view definition. A new view will be added to the provided design document using the name and options.

If the view name starts with “by_” and :by is not provided in the options, the new view's map method will be interpreted and generated automatically. For example:

View.define(Meeting, design, "by_date_and_name")

Will create a view that searches by the date and name properties. Explicity setting the attributes to use is possible using the :by option. For example:

View.define(Meeting, design, "by_date_and_name", :by => [:date, :firstname, :lastname])

The view name is the same, but three keys would be used in the subsecuent index.

By default, a check is made on each of the view's keys to ensure they do not contain a nil value ('null' in javascript). This is probably what you want in most cases but sometimes in can be useful to create an index where nil is permited. Set the :allow_nil option to true to remove this check.

Conversely, keys are not checked to see if they are empty or blank. If you'd like to enable this, set the :allow_blank option to false. The default is true, empty strings are permited in the indexes.



464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
# File 'lib/couchrest/model/designs/view.rb', line 464

def define(design_doc, name, opts = {})
  model = design_doc.model

  # Is this an all view?
  if name.to_s == 'all'
    opts[:map] = <<-EOF
      function(doc) {
        if (doc['#{model.model_type_key}'] == '#{model.model_type_value}') {
          emit(doc._id, null);
        }
      }
    EOF
  elsif !opts[:map]
    if opts[:by].nil? && name.to_s =~ /^by_(.+)/
      opts[:by] = $1.split(/_and_/)
    end
    raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil?

    opts[:allow_blank] = opts[:allow_blank].nil? ? true : opts[:allow_blank]
    opts[:guards] ||= []
    opts[:guards].push "(doc['#{model.model_type_key}'] == '#{model.model_type_value}')"

    keys = opts[:by].map{|o| "doc['#{o}']"}
    emit = keys.length == 1 ? keys.first : "[#{keys.join(', ')}]"
    opts[:guards] += keys.map{|k| "(#{k} != null)"} unless opts[:allow_nil]
    opts[:guards] += keys.map{|k| "(#{k} != '')"} unless opts[:allow_blank]
    opts[:map] = <<-EOF
      function(doc) {
        if (#{opts[:guards].join(' && ')}) {
          emit(#{emit}, 1);
        }
      }
    EOF
    if opts[:reduce].nil?
      # Use built-in sum function by default
      opts[:reduce] = "_sum"
    end
  end

  if opts[:reduce].is_a?(Symbol)
    # Assume calling a built in method, convert to a string
    opts[:reduce] = "_#{opts[:reduce]}"
  end

  design_doc['views'] ||= {}
  view = design_doc['views'][name.to_s] = { }
  view['map'] = opts[:map]
  view['reduce'] = opts[:reduce] if opts[:reduce]
  view
end

.define_and_create(design_doc, name, opts = {}) ⇒ Object



431
432
433
434
# File 'lib/couchrest/model/designs/view.rb', line 431

def define_and_create(design_doc, name, opts = {})
  define(design_doc, name, opts)
  create_model_methods(design_doc, name, opts)
end

Instance Method Details

#[](value) ⇒ Object

Accept requests as if the view was an array. Used for backwards compatibity with older queries:

Model.all(:raw => true, :limit => 0)['total_rows']

In this example, the raw option will be ignored, and the total rows will still be accessible.



173
174
175
# File 'lib/couchrest/model/designs/view.rb', line 173

def [](value)
  execute[value]
end

#allObject

Fetch all the documents the view can access. If the view has not already been prepared for including documents in the query, it will be added automatically and reset any previously cached results.



72
73
74
75
# File 'lib/couchrest/model/designs/view.rb', line 72

def all
  include_docs!
  docs
end

#countObject

Perform a count operation based on the current view. If the view can be reduced, the reduce will be performed and return the first value. This is okay for most simple queries, but may provide unexpected results if your reduce method does not calculate the total number of documents in a result set.

Trying to use this method with the group option will raise an error.

If no reduce function is defined, a query will be performed to return the total number of rows, this is the equivalant of:

view.limit(0).total_rows


123
124
125
126
127
128
129
130
131
# File 'lib/couchrest/model/designs/view.rb', line 123

def count
  raise "View#count cannot be used with group options" if query[:group]
  if can_reduce?
    row = reduce.skip(0).limit(1).rows.first
    row.nil? ? 0 : row.value
  else
    limit(0).total_rows
  end
end

#current_pageObject



387
388
389
# File 'lib/couchrest/model/designs/view.rb', line 387

def current_page
  (offset_value / limit_value) + 1
end

#database(value) ⇒ Object

Specify the database the view should use. If not defined, an attempt will be made to load its value from the model.



327
328
329
# File 'lib/couchrest/model/designs/view.rb', line 327

def database(value)
  update_query(:database => value)
end

#descendingObject

The results should be provided in descending order. If the startkey or endkey query options have already been seen set, calling this method will automatically swap the options around. If you don't want this, simply set descending before any other option.

Descending is false by default, and this method cannot be undone once used, it has no inverse option.



259
260
261
262
263
264
265
266
# File 'lib/couchrest/model/designs/view.rb', line 259

def descending
  if query[:startkey] || query[:endkey]
    query[:startkey], query[:endkey] = query[:endkey], query[:startkey]
  elsif query[:startkey_docid] || query[:endkey_docid]
    query[:startkey_docid], query[:endkey_docid] = query[:endkey_docid], query[:startkey_docid]
  end
  update_query(:descending => true)
end

#docsObject

Provide all the documents from the view. If the view has not been prepared with the include_docs option, each document will be loaded individually.



80
81
82
# File 'lib/couchrest/model/designs/view.rb', line 80

def docs
  @docs ||= rows.map{|r| r.doc}
end

#each(&block) ⇒ Object

Run through each document provided by the #all method. This is also used by the Enumerator mixin to provide all the standard ruby collection directly on the view.



143
144
145
# File 'lib/couchrest/model/designs/view.rb', line 143

def each(&block)
  all.each(&block)
end

#empty?Boolean

Check to see if the array of documents is empty. This will perform the query and return all documents ready to use, if you don't want to load anything, use #total_rows or #count instead.

Returns:

  • (Boolean)


136
137
138
# File 'lib/couchrest/model/designs/view.rb', line 136

def empty?
  all.empty?
end

#endkey(value) ⇒ Object

The opposite of #startkey, finds all index entries whose key is before the value specified.

See the #startkey method for more details and the #inclusive_end option.



223
224
225
226
# File 'lib/couchrest/model/designs/view.rb', line 223

def endkey(value)
  raise "View#endkey cannot be used when key has been set" unless query[:key].nil? && query[:keys].nil?
  update_query(:endkey => value)
end

#endkey_doc(value) ⇒ Object

The result set should end at the position of the provided document. The value may be provided as an object that responds to the #id call or a string.



231
232
233
# File 'lib/couchrest/model/designs/view.rb', line 231

def endkey_doc(value)
  update_query(:endkey_docid => value.is_a?(String) ? value : value.id)
end

#firstObject

If another request has been made on the view, this will return the first document in the set. If not, a new query object will be generated with a limit of 1 so that only the first document is loaded.



88
89
90
# File 'lib/couchrest/model/designs/view.rb', line 88

def first
  result ? all.first : limit(1).all.first
end

#groupObject

Control whether the reduce function reduces to a set of distinct keys or to a single result row.

By default the value is false, and can only be set when the view's #reduce option has been set.



295
296
297
298
# File 'lib/couchrest/model/designs/view.rb', line 295

def group
  raise "View#reduce must have been set before grouping is permitted" unless query[:reduce]
  update_query(:group => true)
end

#group_level(value) ⇒ Object

Will set the level the grouping should be performed to. As per the CouchDB API, it only makes sense when the index key is an array.

This will automatically set the group option.



304
305
306
# File 'lib/couchrest/model/designs/view.rb', line 304

def group_level(value)
  group.update_query(:group_level => value.to_i)
end

#include_docsObject



308
309
310
# File 'lib/couchrest/model/designs/view.rb', line 308

def include_docs
  update_query.include_docs!
end

#infoObject

No yet implemented. Eventually this will provide a raw hash of the information CouchDB holds about the view.



179
180
181
# File 'lib/couchrest/model/designs/view.rb', line 179

def info
  raise "Not yet implemented"
end

#key(value) ⇒ Object

Find all entries in the index whose key matches the value provided.

Cannot be used when the #startkey or #endkey have been set.



194
195
196
197
# File 'lib/couchrest/model/designs/view.rb', line 194

def key(value)
  raise "View#key cannot be used when startkey or endkey have been set" unless query[:keys].nil? && query[:startkey].nil? && query[:endkey].nil?
  update_query(:key => value)
end

#keys(*keys) ⇒ Object

Keys is a special CouchDB option that will cause the view request to be POSTed including an array of keys. Only documents with the matching keys will be returned. This is much faster than sending multiple requests for a set non-consecutive documents.

If no values are provided, this method will act as a wrapper around the rows result set, providing an array of keys.



242
243
244
245
246
247
248
249
# File 'lib/couchrest/model/designs/view.rb', line 242

def keys(*keys)
  if keys.empty?
    rows.map{|r| r.key}
  else
    raise "View#keys cannot by used when key or startkey/endkey have been set" unless query[:key].nil? && query[:startkey].nil? && query[:endkey].nil?
    update_query(:keys => keys.first)
  end
end

#lastObject

Same as first but will order the view in descending order. This does not however reverse the search keys or the offset, so if you are using a startkey and endkey you might end up with unexpected results.

If in doubt, don't use this method!



99
100
101
# File 'lib/couchrest/model/designs/view.rb', line 99

def last
  result ? all.last : limit(1).descending.all.last
end

#lengthObject

Return the number of documents in the currently defined result set. Use #count for the total number of documents regardless of the current limit defined.



106
107
108
# File 'lib/couchrest/model/designs/view.rb', line 106

def length
  docs.length
end

#limit(value) ⇒ Object

Limit the result set to the value supplied.



269
270
271
# File 'lib/couchrest/model/designs/view.rb', line 269

def limit(value)
  update_query(:limit => value)
end

#limit_valueObject



378
379
380
# File 'lib/couchrest/model/designs/view.rb', line 378

def limit_value
  query[:limit]
end

#offsetObject

Wrapper for the results offset. As per the CouchDB API, this may be nil if groups are used.



149
150
151
# File 'lib/couchrest/model/designs/view.rb', line 149

def offset
  execute['offset']
end

#offset_valueObject



374
375
376
# File 'lib/couchrest/model/designs/view.rb', line 374

def offset_value
  query[:skip]
end

#page(page) ⇒ Object

Kaminari compatible pagination support

Based on the really simple support for scoped pagination in the the Kaminari gem, we provide compatible methods here to perform the same actions you'd expect.



357
358
359
# File 'lib/couchrest/model/designs/view.rb', line 357

def page(page)
  limit(owner.default_per_page).skip(owner.default_per_page * ([page.to_i, 1].max - 1))
end

#per(num) ⇒ Object



361
362
363
364
365
366
367
368
# File 'lib/couchrest/model/designs/view.rb', line 361

def per(num)
  raise "View#page must be called before #per!" if limit_value.nil? || offset_value.nil?
  if (n = num.to_i) <= 0
    self
  else
    limit(num).skip(offset_value / limit_value * n)
  end
end

#proxy(value) ⇒ Object

Set the view's proxy that will be used instead of the model for any future searches. As soon as this enters the new view's initializer it will be removed and set as the model object.

See the Proxyable mixin for more details.



338
339
340
# File 'lib/couchrest/model/designs/view.rb', line 338

def proxy(value)
  update_query(:proxy => value)
end

#reduceObject

Use the reduce function on the view. If none is available this method will fail.



285
286
287
288
# File 'lib/couchrest/model/designs/view.rb', line 285

def reduce
  raise "Cannot reduce a view without a reduce method" unless can_reduce?
  update_query(:reduce => true, :delete => [:include_docs])
end

#reset!Object

Return any cached values to their nil state so that any queries requested later will have a fresh set of data.



344
345
346
347
348
# File 'lib/couchrest/model/designs/view.rb', line 344

def reset!
  self.result = nil
  @rows = nil
  @docs = nil
end

#rowsObject

Return each row wrapped in a ViewRow object. Unlike the raw CouchDB request, this will provide an empty array if there are no results.



59
60
61
62
63
64
65
66
# File 'lib/couchrest/model/designs/view.rb', line 59

def rows
  return @rows if @rows
  if execute && result['rows']
    @rows ||= result['rows'].map{|v| ViewRow.new(v, model)}
  else 
    [ ]
  end
end

#skip(value = 0) ⇒ Object

Skip the number of entries in the index specified by value. This would be the equivilent of an offset in SQL.

The CouchDB documentation states that the skip option should not be used with large data sets as it is inefficient. Use the startkey_doc method instead to skip ranges efficiently.



279
280
281
# File 'lib/couchrest/model/designs/view.rb', line 279

def skip(value = 0)
  update_query(:skip => value)
end

#stale(value) ⇒ Object

Allow the results of a query to be provided “stale”. Setting to 'ok' will disable all view updates for the query. When 'update_after' is provided the index will be update after the result has been returned.



318
319
320
321
322
323
# File 'lib/couchrest/model/designs/view.rb', line 318

def stale(value)
  unless (['ok', 'update_after'].include?(value.to_s))
    raise "View#stale can only be set with 'ok' or 'update_after'."
  end
  update_query(:stale => value.to_s)
end

#startkey(value) ⇒ Object

Find all index keys that start with the value provided. May or may not be used in conjunction with the endkey option.

When the #descending option is used (not the default), the start and end keys should be reversed, as per the CouchDB API.

Cannot be used if the key has been set.



206
207
208
209
# File 'lib/couchrest/model/designs/view.rb', line 206

def startkey(value)
  raise "View#startkey cannot be used when key has been set" unless query[:key].nil? && query[:keys].nil?
  update_query(:startkey => value)
end

#startkey_doc(value) ⇒ Object

The result set should start from the position of the provided document. The value may be provided as an object that responds to the #id call or a string.



214
215
216
# File 'lib/couchrest/model/designs/view.rb', line 214

def startkey_doc(value)
  update_query(:startkey_docid => value.is_a?(String) ? value : value.id)
end

#total_countObject



370
371
372
# File 'lib/couchrest/model/designs/view.rb', line 370

def total_count
  @total_count ||= limit(nil).skip(nil).count
end

#total_pagesObject Also known as: num_pages



382
383
384
# File 'lib/couchrest/model/designs/view.rb', line 382

def total_pages
  (total_count.to_f / limit_value).ceil
end

#total_rowsObject

Wrapper for the total_rows value provided by the query. As per the CouchDB API, this may be nil if groups are used.



155
156
157
# File 'lib/couchrest/model/designs/view.rb', line 155

def total_rows
  execute['total_rows']
end

#valuesObject

Convenience wrapper to provide all the values from the route set without having to go through rows.



161
162
163
# File 'lib/couchrest/model/designs/view.rb', line 161

def values
  rows.map{|r| r.value}
end