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 +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
|
|