paulcarey-relaxdb 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -1,6 +1,10 @@
1
1
  h3. What's New?
2
- * Pagination! CouchDB offers great support for retrieving a subset of data, but the housekeeping is tricky. RelaxDB takes care of it.
3
- * Works with CouchDB 0.9 trunk as of 2008/10/08. Note that pagination won't work on trunk unil https://issues.apache.org/jira/browse/COUCHDB-135 is fixed.
2
+
3
+ * Pagination! CouchDB offers great support for retrieving a subset of data, but the housekeeping is tricky. RelaxDB takes care of it.
4
+ ** Note that if you invoke paginate_by on an already created view, the necessary reduce function won't be automatically created. Take a look at SortedByView and create the reduce func by hand.
5
+ * Works with CouchDB 0.9 trunk as of 2008/10/08. Note that pagination won't work correctly on trunk until issue "COUCHDB-135":http://issues.apache.org/jira/browse/COUCHDB-135 is fixed.
6
+
7
+ *Note*: 0.2.1 requires CouchDB 0.9 trunk. 0.2.0 works with CouchDB 0.8 onwards.
4
8
 
5
9
  h2. Overview
6
10
 
@@ -68,7 +72,7 @@ h3. Exploring models
68
72
  </code>
69
73
  </pre>
70
74
 
71
- h3. Paginating
75
+ h3. Paginating models
72
76
 
73
77
  <pre>
74
78
  <code>
@@ -94,6 +98,20 @@ h3. Paginating
94
98
  </code>
95
99
  </pre>
96
100
 
101
+ h3. Paginating over your own views
102
+
103
+ <pre>
104
+ <code>
105
+
106
+ RelaxDB.paginate_view(page_params, "Letter", "by_letter_and_number", :letter, :number) do |p|
107
+ p.startkey(["b"]).endkey(["b", {}]).count(2)
108
+ end
109
+
110
+ </code>
111
+ </pre>
112
+
113
+ A more illustrative example is listed in the .paginate_view spec in spec/paginate_spec.rb
114
+
97
115
  h3. Creating views by hand
98
116
 
99
117
  <pre>
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ require 'spec/rake/spectask'
4
4
 
5
5
  PLUGIN = "relaxdb"
6
6
  NAME = "relaxdb"
7
- GEM_VERSION = "0.2.0"
7
+ GEM_VERSION = "0.2.1"
8
8
  AUTHOR = "Paul Carey"
9
9
  EMAIL = "paul.p.carey@gmail.com"
10
10
  HOMEPAGE = "http://github.com/paulcarey/relaxdb/"
@@ -9,25 +9,25 @@ module RelaxDB
9
9
  #
10
10
  class AllDelegator < Delegator
11
11
 
12
- def initialize(klass)
12
+ def initialize(class_name)
13
13
  super([])
14
- @klass = klass
14
+ @class_name = class_name
15
15
  end
16
16
 
17
17
  def __getobj__
18
- view_path = "_view/#{@klass}/all"
19
- map_function = ViewCreator.all(@klass)
18
+ view_path = "_view/#{@class_name}/all"
19
+ map_function = ViewCreator.all(@class_name)
20
20
 
21
- @all = RelaxDB.retrieve(view_path, @klass, "all", map_function)
21
+ @all = RelaxDB.retrieve(view_path, @class_name, "all", map_function)
22
22
  end
23
23
 
24
24
  def sorted_by(*atts)
25
- v = SortedByView.new(@klass.name, *atts)
25
+ view = SortedByView.new(@class_name, *atts)
26
26
 
27
- q = Query.new(@klass.name, v.view_name)
28
- yield q if block_given?
27
+ query = Query.new(@class_name, view.view_name)
28
+ yield query if block_given?
29
29
 
30
- RelaxDB.retrieve(q.view_path, @klass, v.view_name, v.map_function)
30
+ view.query(query)
31
31
  end
32
32
 
33
33
  # Note that this method leaves the corresponding DesignDoc for the associated class intact
@@ -296,7 +296,7 @@ module RelaxDB
296
296
  end
297
297
 
298
298
  def self.all
299
- @all_delegator ||= AllDelegator.new(self)
299
+ @all_delegator ||= AllDelegator.new(self.name)
300
300
  end
301
301
 
302
302
  # destroy! nullifies all relationships with peers and children before deleting
@@ -352,23 +352,34 @@ module RelaxDB
352
352
  end
353
353
  end
354
354
 
355
- def self.paginate_by(page_params, *atts)
355
+ #
356
+ # Document views are generated on demand if they don't exist when queried.
357
+ # The paginator is used with both document generated views, and user defined views.
358
+ # Auto generating document views on demand in the paginator makes it significantly
359
+ # more complex and less amendable to use by user defined views. For these reasons,
360
+ # the first call to paginate_by for a particular list of attributes for a particular
361
+ # Document class will update the corresponding design doc, even if it already contains
362
+ # the required view. This should not incur a penalty with CouchDB as it uses the
363
+ # same index for byte identical views.
364
+ #
365
+ def self.paginate_by(page_params, *view_keys)
356
366
  paginate_params = PaginateParams.new
357
367
  yield paginate_params
358
368
  raise paginate_params.error_msg if paginate_params.invalid?
359
369
 
360
370
  paginator = Paginator.new(paginate_params, page_params)
361
371
 
362
- doc_view = SortedByView.new(self.name, *atts)
363
- doc_query = Query.new(self.name, doc_view.view_name)
364
- doc_query.merge(paginator.paginate_params)
372
+ design_doc_name = self.name
373
+ view = SortedByView.new(design_doc_name, *view_keys)
374
+ query = Query.new(design_doc_name, view.view_name)
375
+ query.merge(paginate_params)
365
376
 
366
- @docs = RelaxDB.retrieve(doc_query.view_path, self, doc_view.view_name, doc_view.map_function)
367
- @docs.reverse! if paginate_params.order_inverted?
377
+ docs = view.query(query)
378
+ docs.reverse! if paginate_params.order_inverted?
368
379
 
369
- paginator.add_next_and_prev(@docs, self.name, doc_view, atts)
380
+ paginator.add_next_and_prev(docs, design_doc_name, view.view_name, view_keys)
370
381
 
371
- @docs
382
+ docs
372
383
  end
373
384
 
374
385
  end
@@ -2,7 +2,7 @@ module RelaxDB
2
2
 
3
3
  class PaginateParams
4
4
 
5
- @@params = %w(key startkey startkey_docid endkey endkey_docid count update descending group)
5
+ @@params = %w(key startkey startkey_docid endkey endkey_docid count update descending group reduce)
6
6
 
7
7
  @@params.each do |param|
8
8
  define_method(param.to_sym) do |*val|
@@ -20,6 +20,8 @@ module RelaxDB
20
20
  def initialize
21
21
  # If a client hasn't explicitly set descending, set it to the CouchDB default
22
22
  @descending = false if @descending.nil?
23
+ # CouchDB defaults reduce to true when a reduce func is present
24
+ @reduce = false
23
25
  end
24
26
 
25
27
  def update(params)
@@ -9,48 +9,32 @@ module RelaxDB
9
9
  @orig_paginate_params = @paginate_params.clone
10
10
 
11
11
  page_params = page_params.is_a?(String) ? JSON.parse(page_params).to_mash : page_params
12
+ # Where the magic happens - the original params are updated with the page specific params
12
13
  @paginate_params.update(page_params)
13
14
  end
14
15
 
15
- def total_doc_count(design_doc, view)
16
- query = lambda do
17
- RelaxDB.view(design_doc, view.reduce_view_name) do |q|
18
- q.group(true).group_level(0)
19
- q.startkey(@orig_paginate_params.startkey).endkey(@orig_paginate_params.endkey).descending(@orig_paginate_params.descending)
20
- end
16
+ def total_doc_count(design_doc, view_name)
17
+ result = RelaxDB.view(design_doc, view_name) do |q|
18
+ q.group(true).group_level(0).reduce(true)
19
+ q.startkey(@orig_paginate_params.startkey).endkey(@orig_paginate_params.endkey).descending(@orig_paginate_params.descending)
21
20
  end
22
21
 
23
- begin
24
- result = query.call
25
- rescue
26
- # add the map reduce func if it doesn't exist
27
- DesignDocument.get(design_doc).add_map_view(view.reduce_view_name, view.map_function).
28
- add_reduce_view(view.reduce_view_name, view.reduce_function).save
29
- result = query.call
30
- end
31
-
32
22
  total_docs = RelaxDB.reduce_result(result)
33
23
  end
34
24
 
35
- def add_next_and_prev(docs, design_doc, view, view_keys)
25
+ def add_next_and_prev(docs, design_doc, view_name, view_keys)
36
26
  unless docs.empty?
37
27
  no_docs = docs.size
38
28
  offset = docs.offset
39
- orig_offset = orig_offset(Query.new(design_doc, view.view_name), view)
40
- total_doc_count = total_doc_count(design_doc, view)
29
+ orig_offset = orig_offset(design_doc, view_name)
30
+ total_doc_count = total_doc_count(design_doc, view_name)
41
31
 
42
- next_key = view_keys.map { |a| docs.last.send(a) }
43
- next_key = next_key.length == 1 ? next_key[0] : next_key
44
- next_key_docid = docs.last._id
45
- next_params = { :startkey => next_key, :startkey_docid => next_key_docid, :descending => @orig_paginate_params.descending }
46
32
  next_exists = !@paginate_params.order_inverted? ? (offset - orig_offset + no_docs < total_doc_count) : true
47
-
48
- prev_key = view_keys.map { |a| docs.first.send(a) }
49
- prev_key = prev_key.length == 1 ? prev_key[0] : prev_key
50
- prev_key_docid = docs.first._id
51
- prev_params = { :startkey => prev_key, :startkey_docid => prev_key_docid, :descending => !@orig_paginate_params.descending }
33
+ next_params = create_next(docs.last, view_keys) if next_exists
34
+
52
35
  prev_exists = @paginate_params.order_inverted? ? (offset - orig_offset + no_docs < total_doc_count) :
53
36
  (offset - orig_offset == 0 ? false : true)
37
+ prev_params = create_prev(docs.first, view_keys) if prev_exists
54
38
  else
55
39
  next_exists, prev_exists = false
56
40
  end
@@ -64,14 +48,29 @@ module RelaxDB
64
48
  end
65
49
  end
66
50
 
67
- def orig_offset(query, view)
51
+ def create_next(doc, view_keys)
52
+ next_key = view_keys.map { |a| doc.send(a) }
53
+ next_key = next_key.length == 1 ? next_key[0] : next_key
54
+ next_key_docid = doc._id
55
+ { :startkey => next_key, :startkey_docid => next_key_docid, :descending => @orig_paginate_params.descending }
56
+ end
57
+
58
+ def create_prev(doc, view_keys)
59
+ prev_key = view_keys.map { |a| doc.send(a) }
60
+ prev_key = prev_key.length == 1 ? prev_key[0] : prev_key
61
+ prev_key_docid = doc._id
62
+ prev_params = { :startkey => prev_key, :startkey_docid => prev_key_docid, :descending => !@orig_paginate_params.descending }
63
+ end
64
+
65
+ def orig_offset(design_doc, view_name)
66
+ query = Query.new(design_doc, view_name)
68
67
  if @paginate_params.order_inverted?
69
68
  query.startkey(@orig_paginate_params.endkey).descending(!@orig_paginate_params.descending)
70
69
  else
71
70
  query.startkey(@orig_paginate_params.startkey).descending(@orig_paginate_params.descending)
72
71
  end
73
- query.count(1)
74
- RelaxDB.retrieve(query.view_path, self, view.view_name, view.map_function).offset
72
+ query.reduce(false).count(1)
73
+ RelaxDB.retrieve(query.view_path).offset
75
74
  end
76
75
 
77
76
  end
data/lib/relaxdb/query.rb CHANGED
@@ -16,14 +16,18 @@ module RelaxDB
16
16
  #
17
17
  class Query
18
18
 
19
- @@params = %w(key startkey startkey_docid endkey endkey_docid count update descending skip group group_level)
19
+ @@params = %w(key startkey startkey_docid endkey endkey_docid count update descending skip group group_level reduce)
20
20
 
21
21
  @@params.each do |param|
22
- define_method(param.to_sym) do |val|
23
- instance_variable_set("@#{param}", val)
24
- # null is meaningful to CouchDB. _set allows us to know that a param has been set, even to nil
25
- instance_variable_set("@#{param}_set", true)
26
- self
22
+ define_method(param.to_sym) do |*val|
23
+ if val.empty?
24
+ instance_variable_get("@#{param}")
25
+ else
26
+ instance_variable_set("@#{param}", val[0])
27
+ # null is meaningful to CouchDB. _set allows us to know that a param has been set, even to nil
28
+ instance_variable_set("@#{param}_set", true)
29
+ self
30
+ end
27
31
  end
28
32
  end
29
33
 
@@ -54,7 +54,7 @@ module RelaxDB
54
54
  end
55
55
 
56
56
  # Used internally by RelaxDB
57
- def retrieve(view_path, design_doc, view_name, map_function)
57
+ def retrieve(view_path, design_doc=nil, view_name=nil, map_function=nil)
58
58
  begin
59
59
  resp = db.get(view_path)
60
60
  rescue => e
@@ -99,6 +99,24 @@ module RelaxDB
99
99
  obj = data["rows"][0] && data["rows"][0]["value"]
100
100
  ViewObject.create(obj)
101
101
  end
102
+
103
+ def paginate_view(page_params, design_doc, view_name, *view_keys)
104
+ paginate_params = PaginateParams.new
105
+ yield paginate_params
106
+ raise paginate_params.error_msg if paginate_params.invalid?
107
+
108
+ paginator = Paginator.new(paginate_params, page_params)
109
+
110
+ query = Query.new(design_doc, view_name)
111
+ query.merge(paginate_params)
112
+
113
+ docs = ViewResult.new(JSON.parse(db.get(query.view_path).body))
114
+ docs.reverse! if paginate_params.order_inverted?
115
+
116
+ paginator.add_next_and_prev(docs, design_doc, view_name, view_keys)
117
+
118
+ docs
119
+ end
102
120
 
103
121
  def create_from_hash(data)
104
122
  data["rows"].map { |row| create_object(row["value"]) }
@@ -10,19 +10,13 @@ module RelaxDB
10
10
  end
11
11
 
12
12
  def map_function
13
- # To guard against non existing attributes in older documents, an OR with an object literal
14
- # is inserted for each emitted key. The guard can be emitted in 0.9 trunk.
15
- # The object literal is the lowest sorting JSON category
16
-
17
- # Create the key from the attributes, wrapping it in [] if the key is composite
18
- raw = @atts.inject("") { |m,v| m << "(doc.#{v}||{}), " }
19
- refined = raw[0, raw.size-2]
20
- pure = @atts.size > 1 ? refined.sub(/^/, "[").sub(/$/, "]") : refined
13
+ key = @atts.map { |a| "doc.#{a}" }.join(", ")
14
+ key = @atts.size > 1 ? key.sub(/^/, "[").sub(/$/, "]") : key
21
15
 
22
16
  <<-QUERY
23
17
  function(doc) {
24
18
  if(doc.class == "#{@class_name}") {
25
- emit(#{pure}, doc);
19
+ emit(#{key}, doc);
26
20
  }
27
21
  }
28
22
  QUERY
@@ -37,19 +31,30 @@ module RelaxDB
37
31
  end
38
32
 
39
33
  def view_name
40
- name = "all_sorted_by#{suffix}"
41
- end
42
-
43
- def reduce_view_name
44
- "reduce_by#{suffix}"
45
- end
46
-
47
- def suffix
48
- s = @atts.inject("") do |s, att|
34
+ s = @atts.inject("all_sorted_by") do |s, att|
49
35
  s << "_#{att}_and"
50
36
  end
51
37
  s[0, s.size-4]
52
38
  end
39
+
40
+ def query(query)
41
+ # If a view contains both a map and reduce function, CouchDB will by default return
42
+ # the result of the reduce function when queried.
43
+ # This class automatically creates both map and reduce functions so it can be used by the paginator.
44
+ # In normal usage, this class will be used with map functions, hence reduce is explicitly set to false.
45
+ query.reduce(false) if query.reduce.nil?
46
+
47
+ begin
48
+ resp = RelaxDB.db.get(query.view_path)
49
+ rescue => e
50
+ design_doc = DesignDocument.get(@class_name)
51
+ design_doc.add_map_view(view_name, map_function).add_reduce_view(view_name, reduce_function).save
52
+ resp = RelaxDB.db.get(query.view_path)
53
+ end
54
+
55
+ data = JSON.parse(resp.body)
56
+ ViewResult.new(data)
57
+ end
53
58
 
54
59
  end
55
60
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paulcarey-relaxdb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Carey