relaxdb 0.3.5 → 0.5

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.
Files changed (49) hide show
  1. data/README.textile +21 -23
  2. data/Rakefile +2 -7
  3. data/docs/spec_results.html +5 -5
  4. data/lib/more/grapher.rb +1 -1
  5. data/lib/relaxdb.rb +3 -5
  6. data/lib/relaxdb/all_delegator.rb +19 -13
  7. data/lib/relaxdb/document.rb +150 -218
  8. data/lib/relaxdb/extlib.rb +7 -1
  9. data/lib/relaxdb/migration.rb +11 -8
  10. data/lib/relaxdb/net_http_server.rb +19 -1
  11. data/lib/relaxdb/paginator.rb +30 -11
  12. data/lib/relaxdb/query.rb +1 -1
  13. data/lib/relaxdb/{belongs_to_proxy.rb → references_proxy.rb} +3 -3
  14. data/lib/relaxdb/relaxdb.rb +87 -7
  15. data/lib/relaxdb/server.rb +8 -2
  16. data/lib/relaxdb/taf2_curb_server.rb +2 -1
  17. data/lib/relaxdb/uuid_generator.rb +38 -2
  18. data/lib/relaxdb/view_by_delegator.rb +34 -0
  19. data/lib/relaxdb/view_object.rb +1 -1
  20. data/lib/relaxdb/view_uploader.rb +16 -2
  21. data/lib/relaxdb/views.rb +23 -55
  22. data/readme.rb +3 -3
  23. data/spec/all_delegator_spec.rb +52 -0
  24. data/spec/callbacks_spec.rb +4 -4
  25. data/spec/derived_properties_spec.rb +4 -4
  26. data/spec/design_doc_spec.rb +2 -2
  27. data/spec/doc_inheritable_spec.rb +2 -2
  28. data/spec/document_spec.rb +47 -25
  29. data/spec/migration_spec.rb +12 -10
  30. data/spec/qpaginate_spec.rb +88 -0
  31. data/spec/query_spec.rb +2 -2
  32. data/spec/references_proxy_spec.rb +94 -0
  33. data/spec/relaxdb_spec.rb +29 -21
  34. data/spec/server_spec.rb +4 -3
  35. data/spec/spec_helper.rb +1 -0
  36. data/spec/spec_models.rb +48 -57
  37. data/spec/uuid_generator_spec.rb +34 -0
  38. data/spec/view_by_spec.rb +62 -54
  39. data/spec/view_docs_by_spec.rb +85 -0
  40. metadata +38 -27
  41. data/lib/more/atomic_bulk_save_support.rb +0 -18
  42. data/lib/relaxdb/has_many_proxy.rb +0 -101
  43. data/lib/relaxdb/has_one_proxy.rb +0 -42
  44. data/lib/relaxdb/references_many_proxy.rb +0 -97
  45. data/spec/belongs_to_spec.rb +0 -124
  46. data/spec/has_many_spec.rb +0 -202
  47. data/spec/has_one_spec.rb +0 -123
  48. data/spec/references_many_spec.rb +0 -173
  49. data/spec/view_spec.rb +0 -23
@@ -16,9 +16,15 @@ class Time
16
16
  # Ensure that all Times are stored as UTC
17
17
  # Times in the following format may be passed directly to
18
18
  # Date.new in a JavaScript runtime
19
- def to_json
19
+ def to_json(options={})
20
20
  utc
21
21
  %Q("#{strftime "%Y/%m/%d %H:%M:%S +0000"}")
22
22
  end
23
23
 
24
24
  end
25
+
26
+ class Hash
27
+ def to_obj
28
+ RelaxDB.create_obj_from_doc(self)
29
+ end
30
+ end
@@ -1,18 +1,21 @@
1
1
  module RelaxDB
2
2
 
3
3
  class Migration
4
+
5
+ def self.load_batch klass, startkey, limit
6
+ skip = startkey.nil? ? 0 : 1
7
+ result = RelaxDB.rf_view "#{klass}_all", :startkey => startkey,
8
+ :raw => true, :limit => limit, :skip => skip
9
+ ids = result["rows"].map { |h| h["id"] }
10
+ RelaxDB.load! ids
11
+ end
4
12
 
5
13
  def self.run klass, limit = 1000
6
- query = lambda do |page_params|
7
- RelaxDB.paginate_view "#{klass}_all", :startkey => nil, :endkey => {}, :attributes => [:_id],
8
- :page_params => page_params, :limit => limit
9
- end
10
-
11
- objs = query.call({})
14
+ objs = load_batch klass, nil, limit
12
15
  until objs.empty?
13
- migrated = objs.map { |o| yield o }.flatten.reject { |o| o.nil? }
16
+ migrated = objs.map { |o| yield o }.flatten.compact
14
17
  RelaxDB.bulk_save! *migrated
15
- objs = objs.next_params ? query.call(objs.next_params) : []
18
+ objs = load_batch klass, objs[-1]._id, limit
16
19
  end
17
20
  end
18
21
 
@@ -5,6 +5,7 @@ module RelaxDB
5
5
  def initialize(host, port)
6
6
  @host = host
7
7
  @port = port
8
+ @cx = Net::HTTP.start(@host, @port)
8
9
  end
9
10
 
10
11
  def delete(uri)
@@ -30,9 +31,26 @@ module RelaxDB
30
31
  end
31
32
 
32
33
  def request(req)
33
- res = Net::HTTP.start(@host, @port) {|http|
34
+ res = Net::HTTP.start(@host, @port) { |http|
34
35
  http.request(req)
35
36
  }
37
+
38
+ #
39
+ # When using this chunk of code, as opposed to the one above, CouchDB
40
+ # started throwing emfile errors. I can't imagine they're related,
41
+ # but worth noting nonetheless. On second thoughts, I think they are!
42
+ #
43
+ # The tests don't close the cx after they run - just open new ones, this
44
+ # could be causing CouchDB to run out of handles.
45
+ #
46
+ # begin
47
+ # # @cx = Net::HTTP.start(@host, @port) unless @cx.active?
48
+ # res = @cx.request(req)
49
+ # rescue
50
+ # @cx = Net::HTTP.start(@host, @port)
51
+ # res = @cx.request(req)
52
+ # end
53
+
36
54
  if (not res.kind_of?(Net::HTTPSuccess))
37
55
  handle_error(req, res)
38
56
  end
@@ -8,9 +8,23 @@ module RelaxDB
8
8
  @paginate_params = paginate_params
9
9
  @orig_paginate_params = @paginate_params.clone
10
10
 
11
- page_params = page_params.is_a?(String) ? JSON.parse(page_params).to_mash : page_params
11
+ @page_params = page_params.is_a?(String) ? JSON.parse(page_params).to_mash : page_params
12
12
  # Where the magic happens - the original params are updated with the page specific params
13
- @paginate_params.update(page_params)
13
+ @paginate_params.update(@page_params)
14
+ end
15
+
16
+ def add_q_next_and_prev docs, view_name, view_keys
17
+ if docs.empty?
18
+ next_exists = prev_exists = false
19
+ else
20
+ next_exists = docs.size >= @paginate_params.limit
21
+ next_params = next_exists ? create_next(docs.last, view_keys) : false
22
+
23
+ prev_exists = !@page_params.empty?
24
+ prev_params = prev_exists ? create_prev(docs.first, view_keys) : false
25
+ end
26
+
27
+ add_next_and_prev_meths docs, next_params, prev_params
14
28
  end
15
29
 
16
30
  def total_doc_count(view_name)
@@ -31,22 +45,16 @@ module RelaxDB
31
45
  total_doc_count = total_doc_count(view_name)
32
46
 
33
47
  next_exists = !@paginate_params.order_inverted? ? (offset - orig_offset + no_docs < total_doc_count) : true
34
- next_params = create_next(docs.last, view_keys) if next_exists
48
+ next_params = next_exists ? create_next(docs.last, view_keys) : false
35
49
 
36
50
  prev_exists = @paginate_params.order_inverted? ? (offset - orig_offset + no_docs < total_doc_count) :
37
51
  (offset - orig_offset == 0 ? false : true)
38
- prev_params = create_prev(docs.first, view_keys) if prev_exists
52
+ prev_params = prev_exists ? create_prev(docs.first, view_keys) : false
39
53
  else
40
54
  next_exists = prev_exists = false
41
55
  end
42
56
 
43
- docs.meta_class.instance_eval do
44
- define_method(:next_params) { next_exists ? next_params : false }
45
- define_method(:next_query) { next_exists ? "page_params=#{::CGI::escape(next_params.to_json)}" : false }
46
-
47
- define_method(:prev_params) { prev_exists ? prev_params : false }
48
- define_method(:prev_query) { prev_exists ? "page_params=#{::CGI::escape(prev_params.to_json)}" : false }
49
- end
57
+ add_next_and_prev_meths docs, next_params, prev_params
50
58
  end
51
59
 
52
60
  def create_next(doc, view_keys)
@@ -83,6 +91,17 @@ module RelaxDB
83
91
  RelaxDB.rf_view(view_name, params).offset
84
92
  end
85
93
 
94
+ def add_next_and_prev_meths docs, next_params, prev_params
95
+ docs.meta_class.instance_eval do
96
+ define_method(:next_params) { next_params }
97
+ define_method(:next_query) { next_params ? "page_params=#{::CGI::escape(next_params.to_json)}" : false }
98
+
99
+ define_method(:prev_params) { prev_params }
100
+ define_method(:prev_query) { prev_params ? "page_params=#{::CGI::escape(prev_params.to_json)}" : false }
101
+ end
102
+ end
103
+
104
+
86
105
  end
87
106
 
88
107
  end
data/lib/relaxdb/query.rb CHANGED
@@ -67,7 +67,7 @@ module RelaxDB
67
67
  paginate_params.instance_variables.each do |pp|
68
68
  val = paginate_params.instance_variable_get(pp)
69
69
  method_name = pp[1, pp.length]
70
- send(method_name, val) if methods.include? method_name
70
+ send(method_name, val) if @@params.include? method_name
71
71
  end
72
72
  end
73
73
 
@@ -1,6 +1,6 @@
1
1
  module RelaxDB
2
2
 
3
- class BelongsToProxy
3
+ class ReferencesProxy
4
4
 
5
5
  attr_reader :target
6
6
 
@@ -13,13 +13,13 @@ module RelaxDB
13
13
  def target
14
14
  return @target if @target
15
15
 
16
- id = @client.instance_variable_get("@#{@relationship}_id")
16
+ id = @client.data["#{@relationship}_id"]
17
17
  @target = RelaxDB.load(id) if id
18
18
  end
19
19
 
20
20
  def target=(new_target)
21
21
  id = new_target ? new_target._id : nil
22
- @client.instance_variable_set("@#{@relationship}_id", id)
22
+ @client.data["#{@relationship}_id"] = id
23
23
 
24
24
  @target = new_target
25
25
  end
@@ -23,10 +23,11 @@ module RelaxDB
23
23
  end
24
24
 
25
25
  def enable_view_creation default=true
26
+ View.reset
26
27
  @create_views = default
27
28
  end
28
29
 
29
- # Set in configuration and consulted by view_by, has_many, has_one, references_many and all
30
+ # Set in configuration and consulted by view_docs_by, has_many, has_one, references_many and all
30
31
  # Views will be added to CouchDB iff this is true
31
32
  def create_views?
32
33
  @create_views
@@ -121,16 +122,18 @@ module RelaxDB
121
122
  # RelaxDB.logger.debug(caller.inject("#{db.name}/#{ids}\n") { |a, i| a += "#{i}\n" })
122
123
 
123
124
  if ids.is_a? Array
125
+ return [] if ids.empty?
126
+
124
127
  resp = db.post("_all_docs?include_docs=true", {:keys => ids}.to_json)
125
128
  data = JSON.parse(resp.body)
126
- data["rows"].map { |row| row["doc"] ? create_object(row["doc"]) : nil }
129
+ data["rows"].map { |row| row["doc"] ? create_obj_from_doc(row["doc"]) : nil }
127
130
  else
128
131
  begin
129
132
  qs = atts.map{ |k, v| "#{k}=#{v}" }.join("&")
130
133
  qs = atts.empty? ? ids : "#{ids}?#{qs}"
131
134
  resp = db.get qs
132
135
  data = JSON.parse resp.body
133
- create_object data
136
+ create_obj_from_doc data
134
137
  rescue HTTP_404
135
138
  nil
136
139
  end
@@ -146,6 +149,26 @@ module RelaxDB
146
149
  res
147
150
  end
148
151
 
152
+ #
153
+ # Queries a view that doesn't emit the underlying doc as a val.
154
+ #
155
+ def docs view_name, params
156
+ params[:raw] = true
157
+ params[:include_docs] = true
158
+ hash = view view_name, params
159
+ hash["rows"].map { |row| row["doc"] ? create_obj_from_doc(row["doc"]) : nil }
160
+ end
161
+
162
+ def doc_ids view_name, params
163
+ params[:raw] = true
164
+ result = view view_name, params
165
+ ids_from_view result
166
+ end
167
+
168
+ def ids_from_view raw_result
169
+ raw_result["rows"].map { |h| h["id"] }
170
+ end
171
+
149
172
  #
150
173
  # CouchDB defaults reduce to true when a reduce func is present.
151
174
  # RelaxDB used to indiscriminately set reduce=false, allowing clients to override
@@ -195,7 +218,8 @@ module RelaxDB
195
218
 
196
219
  def paginate_view(view_name, atts)
197
220
  page_params = atts.delete :page_params
198
- view_keys = atts.delete :attributes
221
+ view_keys = atts.delete :attributes
222
+ qpaginate = atts.delete :qpaginate
199
223
 
200
224
  paginate_params = PaginateParams.new atts
201
225
  raise paginate_params.error_msg if paginate_params.invalid?
@@ -209,17 +233,67 @@ module RelaxDB
209
233
  docs = ViewResult.new(JSON.parse(db.get(query.view_path).body))
210
234
  docs.reverse! if paginate_params.order_inverted?
211
235
 
212
- paginator.add_next_and_prev(docs, view_name, view_keys)
236
+ if qpaginate
237
+ paginator.add_q_next_and_prev docs, view_name, view_keys
238
+ else
239
+ paginator.add_next_and_prev(docs, view_name, view_keys)
240
+ end
213
241
 
214
242
  docs
215
243
  end
216
-
244
+
245
+ #
246
+ # The paginate_view method only populates next and prev links if they exist.
247
+ # However, its implementation uses three queries to determine if the links
248
+ # should exist.
249
+ #
250
+ # It may be possible to determine if the links should exist with just
251
+ # a single query (by using a 1 doc offset on either side of the docs to be
252
+ # displayed).
253
+ #
254
+ # This method makes a small change to paginate_view, dispensing with knowledge
255
+ # of when to create prev and next links but only requiring a single query.
256
+ #
257
+ # Indiscrimate display of prev links makes this method more suited
258
+ # to forward navigation only.
259
+ #
260
+ def qpaginate_view view_name, atts
261
+ atts[:qpaginate] = true
262
+ paginate_view view_name, atts
263
+ end
264
+
265
+ #
266
+ # Paginates over views that don't emit the underlying doc as a val
267
+ # using the same idiom as qpaginate_view
268
+ #
269
+ def qpaginate_docs view_name, atts
270
+ page_params = atts.delete :page_params
271
+ view_keys = atts.delete :attributes
272
+
273
+ paginate_params = PaginateParams.new atts
274
+ raise paginate_params.error_msg if paginate_params.invalid?
275
+ paginator = Paginator.new(paginate_params, page_params)
276
+
277
+ atts[:raw] = true
278
+ query = Query.new(view_name, atts)
279
+ query.merge(paginate_params)
280
+
281
+ result = JSON.parse(db.get(query.view_path).body)
282
+ doc_ids = ids_from_view result
283
+ doc_ids.reverse! if paginate_params.order_inverted?
284
+ docs = RelaxDB.load! doc_ids
285
+
286
+ paginator.add_q_next_and_prev docs, view_name, view_keys
287
+
288
+ docs
289
+ end
290
+
217
291
  def create_from_hash(data)
218
292
  data["rows"].map { |row| create_object(row["value"]) }
219
293
  end
220
294
 
221
295
  def create_object(data)
222
- klass = data.is_a?(Hash) && data.delete("relaxdb_class")
296
+ klass = data.is_a?(Hash) && data["relaxdb_class"]
223
297
  if klass
224
298
  k = klass.split("::").inject(Object) { |x, y| x.const_get y }
225
299
  k.new data
@@ -228,6 +302,12 @@ module RelaxDB
228
302
  ViewObject.create data
229
303
  end
230
304
  end
305
+
306
+ def create_obj_from_doc(data)
307
+ klass = data["relaxdb_class"]
308
+ k = klass.split("::").inject(Object) { |x, y| x.const_get y }
309
+ k.new data
310
+ end
231
311
 
232
312
  # Convenience methods - should be in a diffent module?
233
313
 
@@ -6,7 +6,7 @@ module RelaxDB
6
6
 
7
7
  class CouchDB
8
8
 
9
- attr_reader :logger
9
+ attr_reader :logger, :server
10
10
 
11
11
  # Used for test instrumentation only i.e. to assert that
12
12
  # an expected number of requests have been issued
@@ -66,7 +66,13 @@ module RelaxDB
66
66
  @put_count += 1
67
67
  @logger.info("PUT /#{@db}/#{unesc(path)} #{json}")
68
68
  @server.put("/#{@db}/#{path}", json)
69
- # @server.put("/#{@db}/#{path}?batch=ok", json)
69
+ end
70
+
71
+ def uuids(count=1)
72
+ @get_count += 1
73
+ uri = "/_uuids?count=#{count}"
74
+ @logger.info "GET #{uri}"
75
+ @server.get uri
70
76
  end
71
77
 
72
78
  def unesc(path)
@@ -8,6 +8,8 @@ module RelaxDB
8
8
  end
9
9
  end
10
10
 
11
+ attr_reader :host, :port
12
+
11
13
  def initialize(host, port)
12
14
  @host, @port = host, port
13
15
  end
@@ -23,7 +25,6 @@ module RelaxDB
23
25
  def put(uri, json)
24
26
  request(uri, 'put') do |c|
25
27
  c.headers['content-type'] = 'application/json'
26
- c.headers['X-Couch-Full-Commit'] = "true"
27
28
  c.http_put json
28
29
  end
29
30
  end
@@ -1,20 +1,56 @@
1
1
  module RelaxDB
2
2
 
3
+ #
4
+ # UUIDs have a significant impact on CouchDB file size and performance
5
+ # See https://issues.apache.org/jira/browse/COUCHDB-465 and
6
+ # http://mail-archives.apache.org/mod_mbox/couchdb-dev/201001.mbox/%3chi57et$19n$1@ger.gmane.org%3e
7
+ #
8
+ # This approach uses the default CouchDB UUID generator (sequential) and
9
+ # reduces its size by converting to base 36. Converting to base 36 results
10
+ # in a UUID 7 chars shorter than hex.
11
+ #
12
+ # The default size of 200 is arbitrary. Brian Candler's UUID generator in
13
+ # couchtiny may also be of interest.
14
+ #
15
+ #
3
16
  class UuidGenerator
17
+
18
+ @uuids = []
19
+ @count = 200
4
20
 
5
21
  def self.uuid
6
22
  unless @length
7
- @uuid ||= UUID.new
8
- @uuid.generate
23
+ uuid = @uuids.pop
24
+ if uuid.nil?
25
+ refill
26
+ uuid = @uuids.pop
27
+ end
28
+ uuid.hex.to_s(36)
9
29
  else
10
30
  rand.to_s[2, @length]
11
31
  end
12
32
  end
33
+
34
+ def self.refill
35
+ resp = RelaxDB.db.uuids(@count)
36
+ @uuids = JSON.parse(resp.body)["uuids"]
37
+ end
38
+
39
+ def self.count=(c)
40
+ @count = c
41
+ end
13
42
 
14
43
  # Convenience that helps relationship debuggging and model exploration
15
44
  def self.id_length=(length)
16
45
  @length = length
17
46
  end
47
+
48
+ # To be invoked by tests, or clients after temp changes
49
+ def self.reset
50
+ @uuids = []
51
+ @count = 200
52
+ @length = nil
53
+ end
18
54
 
19
55
  end
20
56
 
@@ -0,0 +1,34 @@
1
+ module RelaxDB
2
+
3
+ class ViewByDelegator < Delegator
4
+
5
+ def initialize(view_name, opts)
6
+ super([])
7
+ @view_name = view_name
8
+ @opts = opts
9
+ end
10
+
11
+ def __getobj__
12
+ unless @ids
13
+ @opts[:raw] = true
14
+ @ids = RelaxDB.doc_ids @view_name, @opts
15
+ end
16
+ @ids
17
+ end
18
+
19
+ def __setobj__ obj
20
+ # Intentionally empty
21
+ end
22
+
23
+ def load!
24
+ if @ids
25
+ RelaxDB.load! @ids
26
+ else
27
+ @opts[:include_docs] = true
28
+ RelaxDB.docs @view_name, @opts
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ end