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.
- data/LICENSE +20 -0
- data/README.textile +200 -0
- data/Rakefile +63 -0
- data/docs/spec_results.html +1059 -0
- data/lib/more/atomic_bulk_save_support.rb +18 -0
- data/lib/more/grapher.rb +48 -0
- data/lib/relaxdb.rb +50 -0
- data/lib/relaxdb/all_delegator.rb +44 -0
- data/lib/relaxdb/belongs_to_proxy.rb +29 -0
- data/lib/relaxdb/design_doc.rb +57 -0
- data/lib/relaxdb/document.rb +600 -0
- data/lib/relaxdb/extlib.rb +24 -0
- data/lib/relaxdb/has_many_proxy.rb +101 -0
- data/lib/relaxdb/has_one_proxy.rb +42 -0
- data/lib/relaxdb/migration.rb +40 -0
- data/lib/relaxdb/migration_version.rb +21 -0
- data/lib/relaxdb/net_http_server.rb +61 -0
- data/lib/relaxdb/paginate_params.rb +53 -0
- data/lib/relaxdb/paginator.rb +88 -0
- data/lib/relaxdb/query.rb +76 -0
- data/lib/relaxdb/references_many_proxy.rb +97 -0
- data/lib/relaxdb/relaxdb.rb +250 -0
- data/lib/relaxdb/server.rb +109 -0
- data/lib/relaxdb/taf2_curb_server.rb +63 -0
- data/lib/relaxdb/uuid_generator.rb +21 -0
- data/lib/relaxdb/validators.rb +11 -0
- data/lib/relaxdb/view_object.rb +34 -0
- data/lib/relaxdb/view_result.rb +18 -0
- data/lib/relaxdb/view_uploader.rb +49 -0
- data/lib/relaxdb/views.rb +114 -0
- data/readme.rb +80 -0
- data/spec/belongs_to_spec.rb +124 -0
- data/spec/callbacks_spec.rb +80 -0
- data/spec/derived_properties_spec.rb +112 -0
- data/spec/design_doc_spec.rb +34 -0
- data/spec/doc_inheritable_spec.rb +100 -0
- data/spec/document_spec.rb +545 -0
- data/spec/has_many_spec.rb +202 -0
- data/spec/has_one_spec.rb +123 -0
- data/spec/migration_spec.rb +97 -0
- data/spec/migration_version_spec.rb +28 -0
- data/spec/paginate_params_spec.rb +15 -0
- data/spec/paginate_spec.rb +360 -0
- data/spec/query_spec.rb +90 -0
- data/spec/references_many_spec.rb +173 -0
- data/spec/relaxdb_spec.rb +364 -0
- data/spec/server_spec.rb +32 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +65 -0
- data/spec/spec_models.rb +199 -0
- data/spec/view_by_spec.rb +76 -0
- data/spec/view_object_spec.rb +47 -0
- data/spec/view_spec.rb +23 -0
- 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
|