paulcarey-relaxdb 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -1,3 +1,7 @@
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.
4
+
1
5
  h2. Overview
2
6
 
3
7
  RelaxDB provides a Ruby interface to CouchDB. It offers a simple idiom for specifying object relationships. The underlying objects are persisted to the mighty CouchDB. Combined with the schema free nature of CouchDB, RelaxDB's current strength lies in quick prototyping of object models.
@@ -64,6 +68,32 @@ h3. Exploring models
64
68
  </code>
65
69
  </pre>
66
70
 
71
+ h3. Paginating
72
+
73
+ <pre>
74
+ <code>
75
+ # Controller (merb-action-args used for extracting view_params)
76
+
77
+ def action(view_params={})
78
+ u_id = @user._id
79
+
80
+ @posts = Post.paginate_by(view_params, :writer_id, :created_at) do |p|
81
+ p.startkey([u_id, {}]).endkey([u_id]).descending(true).count(5)
82
+ end
83
+ render
84
+ end
85
+
86
+ # In your view
87
+
88
+ <% @posts.each do |p| %>
89
+ <%= p.contents %>
90
+ <% end %>
91
+
92
+ <%= link_to "prev", "/posts/?#{@posts.prev_query}" if @posts.prev_query %>
93
+ <%= link_to "next", "/posts/?#{@posts.next_query}" if @posts.next_query %>
94
+ </code>
95
+ </pre>
96
+
67
97
  h3. Creating views by hand
68
98
 
69
99
  <pre>
@@ -87,6 +117,24 @@ h3. Creating views by hand
87
117
  </code>
88
118
  </pre>
89
119
 
120
+ h3. Visualise
121
+
122
+ Create an object graph by simply running
123
+ <pre>
124
+ <code>
125
+ RelaxDB::GraphCreator.create
126
+ </code>
127
+ </pre>
128
+
129
+ Requires graphviz. Useful for visualising relationships between a limited number of document e.g. test fixtures. "Description and example":http://dev.strawberrydiva.com/visually_explore_couchdb/.
130
+
131
+ h3. Experimental Features
132
+
133
+ * Declarative denormalisation
134
+ ** Create a partial object graph in JSON with a single call
135
+ ** May be used to require fewer GET requests
136
+ ** View the denormalisation spec for examples
137
+
90
138
  h2. Incomplete list of limitations
91
139
 
92
140
  * Error handling is not robust
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.1.3"
7
+ GEM_VERSION = "0.2.0"
8
8
  AUTHOR = "Paul Carey"
9
9
  EMAIL = "paul.p.carey@gmail.com"
10
10
  HOMEPAGE = "http://github.com/paulcarey/relaxdb/"
data/lib/more/grapher.rb CHANGED
@@ -1,5 +1,11 @@
1
1
  module RelaxDB
2
2
 
3
+ #
4
+ # The GraphCreator uses dot to create a graphical model of an entire CouchDB database
5
+ # It probably only makes sense to run it on a database of a limited size
6
+ # The created graphs can be very useful for exploring relationships
7
+ # Run ruby scratch/grapher_demo.rb for an example
8
+ #
3
9
  class GraphCreator
4
10
 
5
11
  def self.create
data/lib/relaxdb.rb CHANGED
@@ -20,6 +20,8 @@ require 'relaxdb/document'
20
20
  require 'relaxdb/extlib'
21
21
  require 'relaxdb/has_many_proxy'
22
22
  require 'relaxdb/has_one_proxy'
23
+ require 'relaxdb/paginate_params'
24
+ require 'relaxdb/paginator'
23
25
  require 'relaxdb/query'
24
26
  require 'relaxdb/references_many_proxy'
25
27
  require 'relaxdb/relaxdb'
@@ -27,6 +29,7 @@ require 'relaxdb/server'
27
29
  require 'relaxdb/sorted_by_view'
28
30
  require 'relaxdb/uuid_generator'
29
31
  require 'relaxdb/view_object'
32
+ require 'relaxdb/view_result'
30
33
  require 'relaxdb/view_uploader'
31
34
  require 'relaxdb/views'
32
35
  require 'more/grapher.rb'
@@ -24,7 +24,7 @@ module RelaxDB
24
24
 
25
25
  def save
26
26
  database = RelaxDB.db
27
- resp = database.put("#{@data['_id']}", @data.to_json)
27
+ resp = database.put(::CGI::escape(@data["_id"]), @data.to_json)
28
28
  @data["_rev"] = JSON.parse(resp.body)["rev"]
29
29
  self
30
30
  end
@@ -32,7 +32,7 @@ module RelaxDB
32
32
  def self.get(client_class)
33
33
  begin
34
34
  database = RelaxDB.db
35
- resp = database.get("_design/#{client_class}")
35
+ resp = database.get(::CGI::escape("_design/#{client_class}"))
36
36
  DesignDocument.new(client_class, JSON.parse(resp.body))
37
37
  rescue => e
38
38
  DesignDocument.new(client_class, {"_id" => "_design/#{client_class}"} )
@@ -41,7 +41,7 @@ module RelaxDB
41
41
 
42
42
  def destroy!
43
43
  # Implicitly prevent the object from being resaved by failing to update its revision
44
- RelaxDB.db.delete("#{@data["_id"]}?rev=#{@data["_rev"]}")
44
+ RelaxDB.db.delete("#{::CGI::escape(@data["_id"])}?rev=#{@data["_rev"]}")
45
45
  self
46
46
  end
47
47
 
@@ -278,6 +278,11 @@ module RelaxDB
278
278
  define_method("#{relationship}_id=") do |id|
279
279
  instance_variable_set("@#{relationship}_id".to_sym, id)
280
280
  end
281
+
282
+ # Allows belongs_to relationships to be used by the paginator
283
+ define_method("#{relationship}_id") do
284
+ instance_variable_get("@#{relationship}_id")
285
+ end
281
286
 
282
287
  end
283
288
 
@@ -346,7 +351,26 @@ module RelaxDB
346
351
  callback.is_a?(Proc) ? callback.call(self) : send(callback)
347
352
  end
348
353
  end
349
-
354
+
355
+ def self.paginate_by(page_params, *atts)
356
+ paginate_params = PaginateParams.new
357
+ yield paginate_params
358
+ raise paginate_params.error_msg if paginate_params.invalid?
359
+
360
+ paginator = Paginator.new(paginate_params, page_params)
361
+
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)
365
+
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?
368
+
369
+ paginator.add_next_and_prev(@docs, self.name, doc_view, atts)
370
+
371
+ @docs
372
+ end
373
+
350
374
  end
351
375
 
352
376
  end
@@ -0,0 +1,52 @@
1
+ module RelaxDB
2
+
3
+ class PaginateParams
4
+
5
+ @@params = %w(key startkey startkey_docid endkey endkey_docid count update descending group)
6
+
7
+ @@params.each do |param|
8
+ define_method(param.to_sym) do |*val|
9
+ if val.empty?
10
+ instance_variable_get("@#{param}")
11
+ else
12
+ instance_variable_set("@#{param}", val[0])
13
+ # null is meaningful to CouchDB. _set allows us to know that a param has been set, even to nil
14
+ instance_variable_set("@#{param}_set", true)
15
+ self
16
+ end
17
+ end
18
+ end
19
+
20
+ def initialize
21
+ # If a client hasn't explicitly set descending, set it to the CouchDB default
22
+ @descending = false if @descending.nil?
23
+ end
24
+
25
+ def update(params)
26
+ @order_inverted = params[:descending].nil? ? false : @descending ^ params[:descending]
27
+ @descending = !@descending if @order_inverted
28
+
29
+ @endkey = @startkey if @order_inverted
30
+
31
+ @startkey = params[:startkey] || @startkey
32
+
33
+ @skip = 1 if params[:startkey]
34
+
35
+ @startkey_docid = params[:startkey_docid] if params[:startkey_docid]
36
+ @endkey_docid = params[:endkey_docid] if params[:endkey_docid]
37
+ end
38
+
39
+ def order_inverted?
40
+ @order_inverted
41
+ end
42
+
43
+ def invalid?
44
+ # Simply because allowing either to be omitted increases the complexity of the paginator
45
+ # This constraint may be removed in future, but don't hold your breath
46
+ @startkey_set && @endkey_set ? nil : "Both startkey and endkey must be set"
47
+ end
48
+ alias error_msg invalid?
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,79 @@
1
+ module RelaxDB
2
+
3
+ class Paginator
4
+
5
+ attr_reader :paginate_params
6
+
7
+ def initialize(paginate_params, page_params)
8
+ @paginate_params = paginate_params
9
+ @orig_paginate_params = @paginate_params.clone
10
+
11
+ page_params = page_params.is_a?(String) ? JSON.parse(page_params).to_mash : page_params
12
+ @paginate_params.update(page_params)
13
+ end
14
+
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
21
+ end
22
+
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
+ total_docs = RelaxDB.reduce_result(result)
33
+ end
34
+
35
+ def add_next_and_prev(docs, design_doc, view, view_keys)
36
+ unless docs.empty?
37
+ no_docs = docs.size
38
+ 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)
41
+
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
+ 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 }
52
+ prev_exists = @paginate_params.order_inverted? ? (offset - orig_offset + no_docs < total_doc_count) :
53
+ (offset - orig_offset == 0 ? false : true)
54
+ else
55
+ next_exists, prev_exists = false
56
+ end
57
+
58
+ docs.meta_class.instance_eval do
59
+ define_method(:next_params) { next_exists ? next_params : false }
60
+ define_method(:next_query) { next_exists ? "page_params=#{::CGI::escape(next_params.to_json)}" : false }
61
+
62
+ define_method(:prev_params) { prev_exists ? prev_params : false }
63
+ define_method(:prev_query) { prev_exists ? "page_params=#{::CGI::escape(prev_params.to_json)}" : false }
64
+ end
65
+ end
66
+
67
+ def orig_offset(query, view)
68
+ if @paginate_params.order_inverted?
69
+ query.startkey(@orig_paginate_params.endkey).descending(!@orig_paginate_params.descending)
70
+ else
71
+ query.startkey(@orig_paginate_params.startkey).descending(@orig_paginate_params.descending)
72
+ end
73
+ query.count(1)
74
+ RelaxDB.retrieve(query.view_path, self, view.view_name, view.map_function).offset
75
+ end
76
+
77
+ end
78
+
79
+ end
data/lib/relaxdb/query.rb CHANGED
@@ -16,16 +16,17 @@ 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)
19
+ @@params = %w(key startkey startkey_docid endkey endkey_docid count update descending skip group group_level)
20
20
 
21
21
  @@params.each do |param|
22
22
  define_method(param.to_sym) do |val|
23
- val ||= ""
24
- instance_variable_set("@#{param}".to_sym, 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)
25
26
  self
26
27
  end
27
28
  end
28
-
29
+
29
30
  def initialize(design_doc, view_name)
30
31
  @design_doc = design_doc
31
32
  @view_name = view_name
@@ -36,12 +37,24 @@ module RelaxDB
36
37
 
37
38
  query = ""
38
39
  @@params.each do |param|
39
- val = instance_variable_get("@#{param}")
40
- query << "&#{param}=#{::CGI::escape(val.to_json)}" if val
40
+ val_set = instance_variable_get("@#{param}_set")
41
+ if val_set
42
+ val = instance_variable_get("@#{param}")
43
+ val = val.to_json unless ["startkey_docid", "endkey_docid"].include?(param)
44
+ query << "&#{param}=#{::CGI::escape(val)}"
45
+ end
41
46
  end
42
47
 
43
48
  uri << query.sub(/^&/, "?")
44
49
  end
50
+
51
+ def merge(paginate_params)
52
+ paginate_params.instance_variables.each do |pp|
53
+ val = paginate_params.instance_variable_get(pp)
54
+ method_name = pp[1, pp.length]
55
+ send(method_name, val) if methods.include? method_name
56
+ end
57
+ end
45
58
 
46
59
  end
47
60
 
@@ -12,6 +12,10 @@ module RelaxDB
12
12
  @@db
13
13
  end
14
14
 
15
+ def logger
16
+ @@db.logger
17
+ end
18
+
15
19
  # Creates the named database if it doesn't already exist
16
20
  def use_db(name)
17
21
  db.use_db(name)
@@ -59,7 +63,7 @@ module RelaxDB
59
63
  end
60
64
 
61
65
  data = JSON.parse(resp.body)
62
- create_from_hash(data)
66
+ ViewResult.new(data)
63
67
  end
64
68
 
65
69
  # Requests the given view from CouchDB and returns a hash.
@@ -100,7 +100,8 @@ module RelaxDB
100
100
  end
101
101
 
102
102
  def unesc(path)
103
- path ? ::CGI::unescape(path) : ""
103
+ # path
104
+ path ? ::CGI::unescape(path) : ""
104
105
  end
105
106
 
106
107
  def uri
@@ -111,6 +112,10 @@ module RelaxDB
111
112
  @db
112
113
  end
113
114
 
115
+ def logger
116
+ @logger
117
+ end
118
+
114
119
  private
115
120
 
116
121
  def create_db_if_non_existant(name)
@@ -9,18 +9,9 @@ module RelaxDB
9
9
  @atts = atts
10
10
  end
11
11
 
12
- def view_name
13
- name = "all_sorted_by"
14
-
15
- @atts.each do |att|
16
- name += "_#{att}_and"
17
- end
18
- name[0, name.size-4]
19
- end
20
-
21
12
  def map_function
22
13
  # To guard against non existing attributes in older documents, an OR with an object literal
23
- # is inserted for each emitted key
14
+ # is inserted for each emitted key. The guard can be emitted in 0.9 trunk.
24
15
  # The object literal is the lowest sorting JSON category
25
16
 
26
17
  # Create the key from the attributes, wrapping it in [] if the key is composite
@@ -36,6 +27,29 @@ module RelaxDB
36
27
  }
37
28
  QUERY
38
29
  end
30
+
31
+ def reduce_function
32
+ <<-QUERY
33
+ function(keys, values, rereduce) {
34
+ return values.length;
35
+ }
36
+ QUERY
37
+ end
38
+
39
+ 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|
49
+ s << "_#{att}_and"
50
+ end
51
+ s[0, s.size-4]
52
+ end
39
53
 
40
54
  end
41
55
 
@@ -16,7 +16,7 @@ describe RelaxDB::DesignDocument do
16
16
 
17
17
  it "should create a corresponding document in CouchDB" do
18
18
  RelaxDB::DesignDocument.get("foo").save
19
- RelaxDB.load("_design/foo").should_not be_nil
19
+ RelaxDB.load("_design%2Ffoo").should_not be_nil
20
20
  end
21
21
 
22
22
  end
@@ -26,7 +26,7 @@ describe RelaxDB::DesignDocument do
26
26
  it "should delete the corresponding document from CouchDB" do
27
27
  dd = RelaxDB::DesignDocument.get("foo").save
28
28
  dd.destroy!
29
- lambda { RelaxDB.load("_design/foo") }.should raise_error
29
+ lambda { RelaxDB.load("_design%2Ffoo") }.should raise_error
30
30
  end
31
31
 
32
32
  end
@@ -78,6 +78,32 @@ describe RelaxDB::Document do
78
78
 
79
79
  end
80
80
 
81
+ describe "user defined property reader" do
82
+
83
+ it "should not effect normal operation" do
84
+ o = BespokeReader.new(:val => 101).save
85
+ o = RelaxDB.load o._id
86
+ o.val.should == 106
87
+ end
88
+
89
+ it "should not modify internal state" do
90
+ o = BespokeReader.new(:val => 101).save
91
+ o = RelaxDB.load o._id
92
+ o.instance_variable_get(:@val).should == 101
93
+ end
94
+
95
+ end
96
+
97
+ describe "user defined property writer" do
98
+
99
+ it "should not be used" do
100
+ o = BespokeWriter.new(:val => 101).save
101
+ o = RelaxDB.load o._id
102
+ o.val.should == 81
103
+ end
104
+
105
+ end
106
+
81
107
  describe "loaded objects" do
82
108
 
83
109
  it "should contain state as when saved" do
@@ -113,7 +139,6 @@ describe RelaxDB::Document do
113
139
  end
114
140
 
115
141
  it "will result in undefined behaviour when invoked on unsaved objects" do
116
- Photo.new.destroy!
117
142
  lambda { Atom.new.destroy! }.should raise_error
118
143
  end
119
144
 
data/spec/query_spec.rb CHANGED
@@ -24,21 +24,45 @@ describe RelaxDB::Query do
24
24
  end
25
25
 
26
26
  it "should contain URL and JSON encoded key when the key has been set" do
27
- q = RelaxDB::Query.new("Zenith", "mount")
27
+ q = RelaxDB::Query.new("", "")
28
28
  q.key("olympus")
29
- q.view_path.should == "_view/Zenith/mount?key=%22olympus%22"
29
+ q.view_path.should == "_view//?key=%22olympus%22"
30
30
  end
31
31
 
32
32
  it "should honour startkey, endkey and count" do
33
- q = RelaxDB::Query.new("Zenith", "all_sorted_by_name_and_height")
33
+ q = RelaxDB::Query.new("", "")
34
34
  q.startkey(["olympus"]).endkey(["vesuvius", 3600]).count(100)
35
- q.view_path.should == "_view/Zenith/all_sorted_by_name_and_height?startkey=%5B%22olympus%22%5D&endkey=%5B%22vesuvius%22%2C3600%5D&count=100"
35
+ q.view_path.should == "_view//?startkey=%5B%22olympus%22%5D&endkey=%5B%22vesuvius%22%2C3600%5D&count=100"
36
36
  end
37
37
 
38
- it "should specify the key as the empty string if key was set to nil" do
38
+ it "should specify a null key if key was set to nil" do
39
39
  q = RelaxDB::Query.new("", "")
40
40
  q.key(nil)
41
- q.view_path.should == "_view//?key=%22%22"
41
+ q.view_path.should == "_view//?key=null"
42
+ end
43
+
44
+ it "should specify a null startkey if startkey was set to nil" do
45
+ q = RelaxDB::Query.new("", "")
46
+ q.startkey(nil)
47
+ q.view_path.should == "_view//?startkey=null"
48
+ end
49
+
50
+ it "should specify a null endkey if endkey was set to nil" do
51
+ q = RelaxDB::Query.new("", "")
52
+ q.endkey(nil)
53
+ q.view_path.should == "_view//?endkey=null"
54
+ end
55
+
56
+ it "should not JSON encode the startkey_docid" do
57
+ q = RelaxDB::Query.new("", "")
58
+ q.startkey_docid("foo")
59
+ q.view_path.should == "_view//?startkey_docid=foo"
60
+ end
61
+
62
+ it "should not JSON encode the endkey_docid" do
63
+ q = RelaxDB::Query.new("", "")
64
+ q.endkey_docid("foo")
65
+ q.view_path.should == "_view//?endkey_docid=foo"
42
66
  end
43
67
 
44
68
  end
data/spec/spec_models.rb CHANGED
@@ -12,6 +12,23 @@ class Primitives < RelaxDB::Document
12
12
 
13
13
  end
14
14
 
15
+ class BespokeReader < RelaxDB::Document
16
+ property :val
17
+ def val; @val + 5; end
18
+ end
19
+
20
+ class BespokeWriter < RelaxDB::Document
21
+ property :val
22
+ def val=(v); @val = v - 10; end
23
+ end
24
+
25
+ class Letter < RelaxDB::Document
26
+
27
+ property :letter
28
+ property :number
29
+
30
+ end
31
+
15
32
  class Invite < RelaxDB::Document
16
33
 
17
34
  property :message
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.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Carey
@@ -60,6 +60,8 @@ files:
60
60
  - lib/relaxdb/extlib.rb
61
61
  - lib/relaxdb/has_many_proxy.rb
62
62
  - lib/relaxdb/has_one_proxy.rb
63
+ - lib/relaxdb/paginate_params.rb
64
+ - lib/relaxdb/paginator.rb
63
65
  - lib/relaxdb/query.rb
64
66
  - lib/relaxdb/references_many_proxy.rb
65
67
  - lib/relaxdb/relaxdb.rb