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 +21 -3
- data/Rakefile +1 -1
- data/lib/relaxdb/all_delegator.rb +9 -9
- data/lib/relaxdb/document.rb +20 -9
- data/lib/relaxdb/paginate_params.rb +3 -1
- data/lib/relaxdb/paginator.rb +29 -30
- data/lib/relaxdb/query.rb +10 -6
- data/lib/relaxdb/relaxdb.rb +19 -1
- data/lib/relaxdb/sorted_by_view.rb +23 -18
- metadata +1 -1
data/README.textile
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
h3. What's New?
|
2
|
-
|
3
|
-
|
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
@@ -9,25 +9,25 @@ module RelaxDB
|
|
9
9
|
#
|
10
10
|
class AllDelegator < Delegator
|
11
11
|
|
12
|
-
def initialize(
|
12
|
+
def initialize(class_name)
|
13
13
|
super([])
|
14
|
-
@
|
14
|
+
@class_name = class_name
|
15
15
|
end
|
16
16
|
|
17
17
|
def __getobj__
|
18
|
-
view_path = "_view/#{@
|
19
|
-
map_function = ViewCreator.all(@
|
18
|
+
view_path = "_view/#{@class_name}/all"
|
19
|
+
map_function = ViewCreator.all(@class_name)
|
20
20
|
|
21
|
-
@all = RelaxDB.retrieve(view_path, @
|
21
|
+
@all = RelaxDB.retrieve(view_path, @class_name, "all", map_function)
|
22
22
|
end
|
23
23
|
|
24
24
|
def sorted_by(*atts)
|
25
|
-
|
25
|
+
view = SortedByView.new(@class_name, *atts)
|
26
26
|
|
27
|
-
|
28
|
-
yield
|
27
|
+
query = Query.new(@class_name, view.view_name)
|
28
|
+
yield query if block_given?
|
29
29
|
|
30
|
-
|
30
|
+
view.query(query)
|
31
31
|
end
|
32
32
|
|
33
33
|
# Note that this method leaves the corresponding DesignDoc for the associated class intact
|
data/lib/relaxdb/document.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
363
|
-
|
364
|
-
|
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
|
-
|
367
|
-
|
377
|
+
docs = view.query(query)
|
378
|
+
docs.reverse! if paginate_params.order_inverted?
|
368
379
|
|
369
|
-
paginator.add_next_and_prev(
|
380
|
+
paginator.add_next_and_prev(docs, design_doc_name, view.view_name, view_keys)
|
370
381
|
|
371
|
-
|
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)
|
data/lib/relaxdb/paginator.rb
CHANGED
@@ -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,
|
16
|
-
|
17
|
-
|
18
|
-
|
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,
|
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(
|
40
|
-
total_doc_count = total_doc_count(design_doc,
|
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
|
-
|
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
|
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
|
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
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
|
data/lib/relaxdb/relaxdb.rb
CHANGED
@@ -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
|
-
|
14
|
-
|
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(#{
|
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
|
-
|
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
|
|