relaxdb 0.3.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 (54) hide show
  1. data/LICENSE +20 -0
  2. data/README.textile +200 -0
  3. data/Rakefile +63 -0
  4. data/docs/spec_results.html +1059 -0
  5. data/lib/more/atomic_bulk_save_support.rb +18 -0
  6. data/lib/more/grapher.rb +48 -0
  7. data/lib/relaxdb.rb +50 -0
  8. data/lib/relaxdb/all_delegator.rb +44 -0
  9. data/lib/relaxdb/belongs_to_proxy.rb +29 -0
  10. data/lib/relaxdb/design_doc.rb +57 -0
  11. data/lib/relaxdb/document.rb +600 -0
  12. data/lib/relaxdb/extlib.rb +24 -0
  13. data/lib/relaxdb/has_many_proxy.rb +101 -0
  14. data/lib/relaxdb/has_one_proxy.rb +42 -0
  15. data/lib/relaxdb/migration.rb +40 -0
  16. data/lib/relaxdb/migration_version.rb +21 -0
  17. data/lib/relaxdb/net_http_server.rb +61 -0
  18. data/lib/relaxdb/paginate_params.rb +53 -0
  19. data/lib/relaxdb/paginator.rb +88 -0
  20. data/lib/relaxdb/query.rb +76 -0
  21. data/lib/relaxdb/references_many_proxy.rb +97 -0
  22. data/lib/relaxdb/relaxdb.rb +250 -0
  23. data/lib/relaxdb/server.rb +109 -0
  24. data/lib/relaxdb/taf2_curb_server.rb +63 -0
  25. data/lib/relaxdb/uuid_generator.rb +21 -0
  26. data/lib/relaxdb/validators.rb +11 -0
  27. data/lib/relaxdb/view_object.rb +34 -0
  28. data/lib/relaxdb/view_result.rb +18 -0
  29. data/lib/relaxdb/view_uploader.rb +49 -0
  30. data/lib/relaxdb/views.rb +114 -0
  31. data/readme.rb +80 -0
  32. data/spec/belongs_to_spec.rb +124 -0
  33. data/spec/callbacks_spec.rb +80 -0
  34. data/spec/derived_properties_spec.rb +112 -0
  35. data/spec/design_doc_spec.rb +34 -0
  36. data/spec/doc_inheritable_spec.rb +100 -0
  37. data/spec/document_spec.rb +545 -0
  38. data/spec/has_many_spec.rb +202 -0
  39. data/spec/has_one_spec.rb +123 -0
  40. data/spec/migration_spec.rb +97 -0
  41. data/spec/migration_version_spec.rb +28 -0
  42. data/spec/paginate_params_spec.rb +15 -0
  43. data/spec/paginate_spec.rb +360 -0
  44. data/spec/query_spec.rb +90 -0
  45. data/spec/references_many_spec.rb +173 -0
  46. data/spec/relaxdb_spec.rb +364 -0
  47. data/spec/server_spec.rb +32 -0
  48. data/spec/spec.opts +1 -0
  49. data/spec/spec_helper.rb +65 -0
  50. data/spec/spec_models.rb +199 -0
  51. data/spec/view_by_spec.rb +76 -0
  52. data/spec/view_object_spec.rb +47 -0
  53. data/spec/view_spec.rb +23 -0
  54. metadata +137 -0
@@ -0,0 +1,97 @@
1
+ module RelaxDB
2
+
3
+ class ReferencesManyProxy
4
+
5
+ include Enumerable
6
+
7
+ def initialize(client, relationship, opts)
8
+ @client = client
9
+ @relationship = relationship
10
+
11
+ @target_class = opts[:class]
12
+ @relationship_as_viewed_by_target = opts[:known_as].to_s
13
+
14
+ @peers = resolve
15
+ end
16
+
17
+ def <<(obj, reciprocal_invocation=false)
18
+ return false if peer_ids.include? obj._id
19
+
20
+ @peers << obj if @peers
21
+ peer_ids << obj._id
22
+
23
+ unless reciprocal_invocation
24
+ # Set the other side of the relationship, ensuring this method isn't called again
25
+ obj.send(@relationship_as_viewed_by_target).send(:<<, @client, true)
26
+
27
+ # Bulk save to ensure relationship is persisted on both sides
28
+ RelaxDB.bulk_save! @client, obj
29
+ end
30
+
31
+ self
32
+ end
33
+
34
+ def clear
35
+ @peers.each do |peer|
36
+ peer.send(@relationship_as_viewed_by_target).send(:delete_from_self, @client)
37
+ end
38
+
39
+ # Important to resolve in the database before in memory, although an examination of the
40
+ # contents of the bulk_save will look wrong as this object will still list all its peers
41
+ RelaxDB.bulk_save(@client, *@peers)
42
+
43
+ peer_ids.clear
44
+ @peers.clear
45
+ end
46
+
47
+ def delete(obj)
48
+ deleted = obj.send(@relationship_as_viewed_by_target).send(:delete_from_self, @client)
49
+ if deleted
50
+ delete_from_self(obj)
51
+ RelaxDB.bulk_save(@client, obj)
52
+ end
53
+ deleted
54
+ end
55
+
56
+ def delete_from_self(obj)
57
+ @peers.delete(obj)
58
+ peer_ids.delete(obj._id)
59
+ end
60
+
61
+ def empty?
62
+ peer_ids.empty?
63
+ end
64
+
65
+ def size
66
+ peer_ids.size
67
+ end
68
+
69
+ def [](*args)
70
+ @peers[*args]
71
+ end
72
+
73
+ def each(&blk)
74
+ @peers.each(&blk)
75
+ end
76
+
77
+ def inspect
78
+ @client.instance_variable_get("@#{@relationship}".to_sym).inspect
79
+ end
80
+
81
+ def peer_ids
82
+ @client.instance_variable_get("@#{@relationship}".to_sym)
83
+ end
84
+
85
+ alias to_id_a peer_ids
86
+
87
+ private
88
+
89
+ # Resolves the actual ids into real objects via a single GET to CouchDB
90
+ def resolve
91
+ view_name = "#{@client.class}_#{@relationship}"
92
+ @peers = RelaxDB.view(view_name, :key => @client._id)
93
+ end
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,250 @@
1
+ module RelaxDB
2
+
3
+ class NotFound < StandardError; end
4
+ class DocumentNotSaved < StandardError; end
5
+ class UpdateConflict < DocumentNotSaved; end
6
+ class ValidationFailure < DocumentNotSaved; end
7
+
8
+ @@db = nil
9
+
10
+ class <<self
11
+
12
+ def configure(config)
13
+ @@db = CouchDB.new config
14
+
15
+ raise "A design_doc must be provided" unless config[:design_doc]
16
+ @dd = config[:design_doc]
17
+ end
18
+
19
+ # This is a temporary method that helps the transition as RelaxDB moves to a single
20
+ # design doc per application.
21
+ def dd
22
+ @dd
23
+ end
24
+
25
+ def enable_view_creation default=true
26
+ @create_views = default
27
+ end
28
+
29
+ # Set in configuration and consulted by view_by, has_many, has_one, references_many and all
30
+ # Views will be added to CouchDB iff this is true
31
+ def create_views?
32
+ @create_views
33
+ end
34
+
35
+ def db
36
+ @@db
37
+ end
38
+
39
+ def logger
40
+ @@db.logger
41
+ end
42
+
43
+ # Creates the named database if it doesn't already exist
44
+ def use_db(name)
45
+ db.use_db name
46
+ end
47
+
48
+ def db_exists?(name)
49
+ db.db_exists? name
50
+ end
51
+
52
+ def db_info
53
+ data = JSON.parse db.get.body
54
+ create_object data
55
+ end
56
+
57
+ def delete_db(name)
58
+ db.delete_db name
59
+ end
60
+
61
+ def list_dbs
62
+ db.list_dbs
63
+ end
64
+
65
+ def replicate_db(source, target)
66
+ db.replicate_db source, target
67
+ end
68
+
69
+ def bulk_save!(*objs)
70
+ if objs[0].equal? :all_or_nothing
71
+ objs.shift
72
+ all_or_nothing = true
73
+ end
74
+
75
+ pre_save_success = objs.inject(true) { |s, o| s &= o.pre_save }
76
+ raise ValidationFailure, objs.inspect unless pre_save_success
77
+
78
+ docs = {}
79
+ objs.each { |o| docs[o._id] = o }
80
+
81
+ data = { "docs" => objs }
82
+ data[:all_or_nothing] = true if all_or_nothing
83
+ resp = db.post("_bulk_docs", data.to_json )
84
+ data = JSON.parse(resp.body)
85
+
86
+ conflicted = []
87
+ data.each do |new_rev|
88
+ obj = docs[ new_rev["id"] ]
89
+ if new_rev["rev"]
90
+ obj._rev = new_rev["rev"]
91
+ obj.post_save
92
+ else
93
+ conflicted << obj._id
94
+ obj.conflicted
95
+ end
96
+ end
97
+
98
+ raise UpdateConflict, conflicted.inspect unless conflicted.empty?
99
+ objs
100
+ end
101
+
102
+ def bulk_save(*objs)
103
+ begin
104
+ bulk_save!(*objs)
105
+ rescue ValidationFailure, UpdateConflict
106
+ false
107
+ end
108
+ end
109
+
110
+ def reload(obj)
111
+ load(obj._id)
112
+ end
113
+
114
+ #
115
+ # Examples:
116
+ # RelaxDB.load "foo", :conflicts => true
117
+ # RelaxDB.load "foo", :revs => true
118
+ # RelaxDB.load ["foo", "bar"]
119
+ #
120
+ def load(ids, atts={})
121
+ # RelaxDB.logger.debug(caller.inject("#{db.name}/#{ids}\n") { |a, i| a += "#{i}\n" })
122
+
123
+ if ids.is_a? Array
124
+ resp = db.post("_all_docs?include_docs=true", {:keys => ids}.to_json)
125
+ data = JSON.parse(resp.body)
126
+ data["rows"].map { |row| row["doc"] ? create_object(row["doc"]) : nil }
127
+ else
128
+ begin
129
+ qs = atts.map{ |k, v| "#{k}=#{v}" }.join("&")
130
+ qs = atts.empty? ? ids : "#{ids}?#{qs}"
131
+ resp = db.get qs
132
+ data = JSON.parse resp.body
133
+ create_object data
134
+ rescue HTTP_404
135
+ nil
136
+ end
137
+ end
138
+ end
139
+
140
+ def load!(ids)
141
+ res = load(ids)
142
+
143
+ raise NotFound, ids if res == nil
144
+ raise NotFound, ids if res.respond_to?(:include?) && res.include?(nil)
145
+
146
+ res
147
+ end
148
+
149
+ #
150
+ # CouchDB defaults reduce to true when a reduce func is present.
151
+ # RelaxDB used to indiscriminately set reduce=false, allowing clients to override
152
+ # if desired. However, as of CouchDB 0.10, such behaviour results in
153
+ # {"error":"query_parse_error","reason":"Invalid URL parameter `reduce` for map view."}
154
+ # View https://issues.apache.org/jira/browse/COUCHDB-383#action_12722350
155
+ #
156
+ # This method is an internal workaround for this change to CouchDB and may
157
+ # be removed if a future change allows for a better solution e.g. map=true
158
+ # or a _map endpoint
159
+ #
160
+ def rf_view view_name, params
161
+ params[:reduce] = false
162
+ view view_name, params
163
+ end
164
+
165
+ def view(view_name, params = {})
166
+ q = Query.new(view_name, params)
167
+
168
+ resp = q.keys ? db.post(q.view_path, q.keys) : db.get(q.view_path)
169
+ hash = JSON.parse(resp.body)
170
+
171
+ if q.raw then hash
172
+ elsif q.reduce then reduce_result hash
173
+ else ViewResult.new hash
174
+ end
175
+ end
176
+
177
+ # Should be invoked on the result of a join view
178
+ # Merges all rows based on merge_key and returns an array of ViewOject
179
+ def merge(data, merge_key)
180
+ merged = {}
181
+ data["rows"].each do |row|
182
+ value = row["value"]
183
+ merged[value[merge_key]] ||= {}
184
+ merged[value[merge_key]].merge!(value)
185
+ end
186
+
187
+ merged.values.map { |v| ViewObject.create(v) }
188
+ end
189
+
190
+ def reduce_result(data)
191
+ res = create_from_hash data
192
+ res.size == 0 ? nil :
193
+ res.size == 1 ? res[0] : res
194
+ end
195
+
196
+ def paginate_view(view_name, atts)
197
+ page_params = atts.delete :page_params
198
+ view_keys = atts.delete :attributes
199
+
200
+ paginate_params = PaginateParams.new atts
201
+ raise paginate_params.error_msg if paginate_params.invalid?
202
+
203
+ paginator = Paginator.new(paginate_params, page_params)
204
+
205
+ atts[:reduce] = false
206
+ query = Query.new(view_name, atts)
207
+ query.merge(paginate_params)
208
+
209
+ docs = ViewResult.new(JSON.parse(db.get(query.view_path).body))
210
+ docs.reverse! if paginate_params.order_inverted?
211
+
212
+ paginator.add_next_and_prev(docs, view_name, view_keys)
213
+
214
+ docs
215
+ end
216
+
217
+ def create_from_hash(data)
218
+ data["rows"].map { |row| create_object(row["value"]) }
219
+ end
220
+
221
+ def create_object(data)
222
+ klass = data.is_a?(Hash) && data.delete("relaxdb_class")
223
+ if klass
224
+ k = klass.split("::").inject(Object) { |x, y| x.const_get y }
225
+ k.new data
226
+ else
227
+ # data is a scalar or not of a known class
228
+ ViewObject.create data
229
+ end
230
+ end
231
+
232
+ # Convenience methods - should be in a diffent module?
233
+
234
+ def get(uri=nil)
235
+ JSON.parse(db.get(uri).body)
236
+ end
237
+
238
+ def pp_get(uri=nil)
239
+ resp = db.get(uri)
240
+ pp(JSON.parse(resp.body))
241
+ end
242
+
243
+ def pp_post(uri=nil, json=nil)
244
+ resp = db.post(uri, json)
245
+ pp(JSON.parse(resp.body))
246
+ end
247
+
248
+ end
249
+
250
+ end
@@ -0,0 +1,109 @@
1
+ module RelaxDB
2
+
3
+ class HTTP_404 < StandardError; end
4
+ class HTTP_409 < StandardError; end
5
+ class HTTP_412 < StandardError; end
6
+
7
+ class CouchDB
8
+
9
+ attr_reader :logger
10
+
11
+ # Used for test instrumentation only i.e. to assert that
12
+ # an expected number of requests have been issued
13
+ attr_accessor :get_count, :put_count, :post_count
14
+
15
+ def initialize(config)
16
+ @get_count, @post_count, @put_count = 0, 0, 0
17
+ @server = RelaxDB::Server.new(config[:host], config[:port])
18
+ @logger = config[:logger] ? config[:logger] : Logger.new(Tempfile.new('relaxdb.log'))
19
+ end
20
+
21
+ def use_db(name)
22
+ create_db_if_non_existant(name)
23
+ @db = name
24
+ end
25
+
26
+ def db_exists?(name)
27
+ @server.get("/#{name}") rescue false
28
+ end
29
+
30
+ # URL encode slashes e.g. RelaxDB.delete_db "foo%2Fbar"
31
+ def delete_db(name)
32
+ @logger.info("Deleting database #{name}")
33
+ @server.delete("/#{name}")
34
+ end
35
+
36
+ def list_dbs
37
+ JSON.parse(@server.get("/_all_dbs").body)
38
+ end
39
+
40
+ def replicate_db(source, target)
41
+ @logger.info("Replicating from #{source} to #{target}")
42
+ create_db_if_non_existant target
43
+ # Manual JSON encoding to allow for dbs containing a '/'
44
+ data = %Q({"source":"#{source}","target":"#{target}"})
45
+ @server.post("/_replicate", data)
46
+ end
47
+
48
+ def delete(path=nil)
49
+ @logger.info("DELETE /#{@db}/#{unesc(path)}")
50
+ @server.delete("/#{@db}/#{path}")
51
+ end
52
+
53
+ def get(path=nil)
54
+ @get_count += 1
55
+ @logger.info("GET /#{@db}/#{unesc(path)}")
56
+ @server.get("/#{@db}/#{path}")
57
+ end
58
+
59
+ def post(path=nil, json=nil)
60
+ @post_count += 1
61
+ @logger.info("POST /#{@db}/#{unesc(path)} #{json}")
62
+ @server.post("/#{@db}/#{path}", json)
63
+ end
64
+
65
+ def put(path=nil, json=nil)
66
+ @put_count += 1
67
+ @logger.info("PUT /#{@db}/#{unesc(path)} #{json}")
68
+ @server.put("/#{@db}/#{path}", json)
69
+ # @server.put("/#{@db}/#{path}?batch=ok", json)
70
+ end
71
+
72
+ def unesc(path)
73
+ # path
74
+ path ? ::CGI::unescape(path) : ""
75
+ end
76
+
77
+ def uri
78
+ "#@server" / @db
79
+ end
80
+
81
+ def name
82
+ @db
83
+ end
84
+
85
+ def name=(name)
86
+ @db = name
87
+ end
88
+
89
+ def req_count
90
+ get_count + put_count + post_count
91
+ end
92
+
93
+ def reset_req_count
94
+ @get_count = @put_count = @post_count = 0
95
+ end
96
+
97
+ private
98
+
99
+ def create_db_if_non_existant(name)
100
+ begin
101
+ @server.get("/#{name}")
102
+ rescue
103
+ @server.put("/#{name}", "")
104
+ end
105
+ end
106
+
107
+ end
108
+
109
+ end