paulcarey-relaxdb 0.2.0 → 0.2.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.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