ceritium-relaxdb 0.2.8
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.textile +185 -0
- data/Rakefile +58 -0
- data/docs/spec_results.html +604 -0
- data/lib/more/atomic_bulk_save_support.rb +18 -0
- data/lib/more/grapher.rb +48 -0
- data/lib/relaxdb.rb +40 -0
- data/lib/relaxdb/all_delegator.rb +68 -0
- data/lib/relaxdb/belongs_to_proxy.rb +29 -0
- data/lib/relaxdb/design_doc.rb +55 -0
- data/lib/relaxdb/document.rb +531 -0
- data/lib/relaxdb/extlib.rb +15 -0
- data/lib/relaxdb/has_many_proxy.rb +104 -0
- data/lib/relaxdb/has_one_proxy.rb +45 -0
- data/lib/relaxdb/paginate_params.rb +54 -0
- data/lib/relaxdb/paginator.rb +78 -0
- data/lib/relaxdb/query.rb +75 -0
- data/lib/relaxdb/references_many_proxy.rb +101 -0
- data/lib/relaxdb/relaxdb.rb +208 -0
- data/lib/relaxdb/server.rb +156 -0
- data/lib/relaxdb/sorted_by_view.rb +65 -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 +47 -0
- data/lib/relaxdb/views.rb +54 -0
- data/spec/belongs_to_spec.rb +129 -0
- data/spec/callbacks_spec.rb +80 -0
- data/spec/derived_properties_spec.rb +117 -0
- data/spec/design_doc_spec.rb +34 -0
- data/spec/document_spec.rb +556 -0
- data/spec/has_many_spec.rb +176 -0
- data/spec/has_one_spec.rb +128 -0
- data/spec/query_spec.rb +80 -0
- data/spec/references_many_spec.rb +178 -0
- data/spec/relaxdb_spec.rb +226 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/spec_models.rb +151 -0
- data/spec/view_object_spec.rb +47 -0
- metadata +123 -0
@@ -0,0 +1,208 @@
|
|
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
|
+
end
|
15
|
+
|
16
|
+
def db
|
17
|
+
@@db
|
18
|
+
end
|
19
|
+
|
20
|
+
def logger
|
21
|
+
@@db.logger
|
22
|
+
end
|
23
|
+
|
24
|
+
# Creates the named database if it doesn't already exist
|
25
|
+
def use_db(name)
|
26
|
+
db.use_db name
|
27
|
+
end
|
28
|
+
|
29
|
+
def db_exists?(name)
|
30
|
+
db.db_exists? name
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete_db(name)
|
34
|
+
db.delete_db name
|
35
|
+
end
|
36
|
+
|
37
|
+
def list_dbs
|
38
|
+
db.list_dbs
|
39
|
+
end
|
40
|
+
|
41
|
+
def replicate_db(source, target)
|
42
|
+
db.replicate_db source, target
|
43
|
+
end
|
44
|
+
|
45
|
+
def bulk_save!(*objs)
|
46
|
+
pre_save_success = objs.inject(true) { |s, o| s &= o.pre_save }
|
47
|
+
raise ValidationFailure, objs unless pre_save_success
|
48
|
+
|
49
|
+
docs = {}
|
50
|
+
objs.each { |o| docs[o._id] = o }
|
51
|
+
|
52
|
+
begin
|
53
|
+
resp = db.post("_bulk_docs", { "docs" => objs }.to_json )
|
54
|
+
data = JSON.parse(resp.body)
|
55
|
+
|
56
|
+
data["new_revs"].each do |new_rev|
|
57
|
+
obj = docs[ new_rev["id"] ]
|
58
|
+
obj._rev = new_rev["rev"]
|
59
|
+
obj.post_save
|
60
|
+
end
|
61
|
+
rescue HTTP_409
|
62
|
+
raise UpdateConflict, objs
|
63
|
+
end
|
64
|
+
|
65
|
+
objs
|
66
|
+
end
|
67
|
+
|
68
|
+
def bulk_save(*objs)
|
69
|
+
begin
|
70
|
+
bulk_save!(*objs)
|
71
|
+
rescue ValidationFailure, UpdateConflict
|
72
|
+
false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def reload(obj)
|
77
|
+
load(obj._id)
|
78
|
+
end
|
79
|
+
|
80
|
+
def load(ids)
|
81
|
+
# RelaxDB.logger.debug(caller.inject("#{db.name}/#{ids}\n") { |a, i| a += "#{i}\n" })
|
82
|
+
|
83
|
+
if ids.is_a? Array
|
84
|
+
resp = db.post("_all_docs?include_docs=true", {:keys => ids}.to_json)
|
85
|
+
data = JSON.parse(resp.body)
|
86
|
+
data["rows"].map { |row| row["doc"] ? create_object(row["doc"]) : nil }
|
87
|
+
else
|
88
|
+
begin
|
89
|
+
resp = db.get(ids)
|
90
|
+
data = JSON.parse(resp.body)
|
91
|
+
create_object(data)
|
92
|
+
rescue HTTP_404
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def load!(ids)
|
99
|
+
res = load(ids)
|
100
|
+
|
101
|
+
raise NotFound, ids if res == nil
|
102
|
+
raise NotFound, ids if res.respond_to?(:include?) && res.include?(nil)
|
103
|
+
|
104
|
+
res
|
105
|
+
end
|
106
|
+
|
107
|
+
# Used internally by RelaxDB
|
108
|
+
def retrieve(view_path, design_doc=nil, view_name=nil, map_func=nil, reduce_func=nil)
|
109
|
+
begin
|
110
|
+
resp = db.get(view_path)
|
111
|
+
rescue => e
|
112
|
+
dd = DesignDocument.get(design_doc).add_map_view(view_name, map_func)
|
113
|
+
dd.add_reduce_view(view_name, reduce_func) if reduce_func
|
114
|
+
dd.save
|
115
|
+
resp = db.get(view_path)
|
116
|
+
end
|
117
|
+
|
118
|
+
data = JSON.parse(resp.body)
|
119
|
+
ViewResult.new(data)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Requests the given view from CouchDB and returns a hash.
|
123
|
+
# This method should typically be wrapped in one of merge, instantiate, or reduce_result.
|
124
|
+
def view(design_doc, view_name)
|
125
|
+
q = Query.new(design_doc, view_name)
|
126
|
+
yield q if block_given?
|
127
|
+
|
128
|
+
resp = q.keys ? db.post(q.view_path, q.keys) : db.get(q.view_path)
|
129
|
+
JSON.parse(resp.body)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Should be invoked on the result of a join view
|
133
|
+
# Merges all rows based on merge_key and returns an array of ViewOject
|
134
|
+
def merge(data, merge_key)
|
135
|
+
merged = {}
|
136
|
+
data["rows"].each do |row|
|
137
|
+
value = row["value"]
|
138
|
+
merged[value[merge_key]] ||= {}
|
139
|
+
merged[value[merge_key]].merge!(value)
|
140
|
+
end
|
141
|
+
|
142
|
+
merged.values.map { |v| ViewObject.create(v) }
|
143
|
+
end
|
144
|
+
|
145
|
+
# Creates RelaxDB::Document objects from the result
|
146
|
+
def instantiate(data)
|
147
|
+
create_from_hash(data)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns a scalar, an object, or an Array of objects
|
151
|
+
def reduce_result(data)
|
152
|
+
obj = data["rows"][0] && data["rows"][0]["value"]
|
153
|
+
ViewObject.create(obj)
|
154
|
+
end
|
155
|
+
|
156
|
+
def paginate_view(page_params, design_doc, view_name, *view_keys)
|
157
|
+
paginate_params = PaginateParams.new
|
158
|
+
yield paginate_params
|
159
|
+
raise paginate_params.error_msg if paginate_params.invalid?
|
160
|
+
|
161
|
+
paginator = Paginator.new(paginate_params, page_params)
|
162
|
+
|
163
|
+
query = Query.new(design_doc, view_name)
|
164
|
+
query.merge(paginate_params)
|
165
|
+
|
166
|
+
docs = ViewResult.new(JSON.parse(db.get(query.view_path).body))
|
167
|
+
docs.reverse! if paginate_params.order_inverted?
|
168
|
+
|
169
|
+
paginator.add_next_and_prev(docs, design_doc, view_name, view_keys)
|
170
|
+
|
171
|
+
docs
|
172
|
+
end
|
173
|
+
|
174
|
+
def create_from_hash(data)
|
175
|
+
data["rows"].map { |row| create_object(row["value"]) }
|
176
|
+
end
|
177
|
+
|
178
|
+
def create_object(data)
|
179
|
+
# revise use of string 'class' - it's a reserved word in JavaScript
|
180
|
+
klass = data ? data.delete("class") : nil
|
181
|
+
if klass
|
182
|
+
k = Module.const_get(klass)
|
183
|
+
k.new(data)
|
184
|
+
else
|
185
|
+
# data is not of a known class
|
186
|
+
ViewObject.create(data)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Convenience methods - should be in a diffent module?
|
191
|
+
|
192
|
+
def get(uri=nil)
|
193
|
+
JSON.parse(db.get(uri).body)
|
194
|
+
end
|
195
|
+
|
196
|
+
def pp_get(uri=nil)
|
197
|
+
resp = db.get(uri)
|
198
|
+
pp(JSON.parse(resp.body))
|
199
|
+
end
|
200
|
+
|
201
|
+
def pp_post(uri=nil, json=nil)
|
202
|
+
resp = db.post(uri, json)
|
203
|
+
pp(JSON.parse(resp.body))
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
@@ -0,0 +1,156 @@
|
|
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 Server
|
8
|
+
|
9
|
+
def initialize(host, port)
|
10
|
+
@host = host
|
11
|
+
@port = port
|
12
|
+
end
|
13
|
+
|
14
|
+
def delete(uri)
|
15
|
+
request(Net::HTTP::Delete.new(uri))
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(uri)
|
19
|
+
request(Net::HTTP::Get.new(uri))
|
20
|
+
end
|
21
|
+
|
22
|
+
def put(uri, json)
|
23
|
+
req = Net::HTTP::Put.new(uri)
|
24
|
+
req["content-type"] = "application/json"
|
25
|
+
req.body = json
|
26
|
+
request(req)
|
27
|
+
end
|
28
|
+
|
29
|
+
def post(uri, json)
|
30
|
+
req = Net::HTTP::Post.new(uri)
|
31
|
+
req["content-type"] = "application/json"
|
32
|
+
req.body = json
|
33
|
+
request(req)
|
34
|
+
end
|
35
|
+
|
36
|
+
def request(req)
|
37
|
+
res = Net::HTTP.start(@host, @port) {|http|
|
38
|
+
http.request(req)
|
39
|
+
}
|
40
|
+
if (not res.kind_of?(Net::HTTPSuccess))
|
41
|
+
handle_error(req, res)
|
42
|
+
end
|
43
|
+
res
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
"http://#{@host}:#{@port}/"
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def handle_error(req, res)
|
53
|
+
msg = "#{res.code}:#{res.message}\nMETHOD:#{req.method}\nURI:#{req.path}\n#{res.body}"
|
54
|
+
begin
|
55
|
+
klass = RelaxDB.const_get("HTTP_#{res.code}")
|
56
|
+
e = klass.new(msg)
|
57
|
+
rescue
|
58
|
+
e = RuntimeError.new(msg)
|
59
|
+
end
|
60
|
+
|
61
|
+
raise e
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class CouchDB
|
66
|
+
|
67
|
+
attr_reader :logger
|
68
|
+
|
69
|
+
# Used for test instrumentation only i.e. to assert that
|
70
|
+
# an expected number of requests have been issued
|
71
|
+
attr_accessor :get_count, :put_count
|
72
|
+
|
73
|
+
def initialize(config)
|
74
|
+
@get_count, @put_count = 0, 0
|
75
|
+
@server = RelaxDB::Server.new(config[:host], config[:port])
|
76
|
+
@logger = config[:logger] ? config[:logger] : Logger.new(Tempfile.new('couchdb.log'))
|
77
|
+
end
|
78
|
+
|
79
|
+
def use_db(name)
|
80
|
+
create_db_if_non_existant(name)
|
81
|
+
@db = name
|
82
|
+
end
|
83
|
+
|
84
|
+
def db_exists?(name)
|
85
|
+
@server.get("/#{name}") rescue false
|
86
|
+
end
|
87
|
+
|
88
|
+
def delete_db(name)
|
89
|
+
@logger.info("Deleting database #{name}")
|
90
|
+
@server.delete("/#{name}")
|
91
|
+
end
|
92
|
+
|
93
|
+
def list_dbs
|
94
|
+
JSON.parse(@server.get("/_all_dbs").body)
|
95
|
+
end
|
96
|
+
|
97
|
+
def replicate_db(source, target)
|
98
|
+
@logger.info("Replicating from #{source} to #{target}")
|
99
|
+
create_db_if_non_existant target
|
100
|
+
data = { "source" => source, "target" => target}
|
101
|
+
@server.post("/_replicate", data.to_json)
|
102
|
+
end
|
103
|
+
|
104
|
+
def delete(path=nil)
|
105
|
+
@logger.info("DELETE /#{@db}/#{unesc(path)}")
|
106
|
+
@server.delete("/#{@db}/#{path}")
|
107
|
+
end
|
108
|
+
|
109
|
+
# *ignored allows methods to invoke get or post indifferently via send
|
110
|
+
def get(path=nil, *ignored)
|
111
|
+
@get_count += 1
|
112
|
+
@logger.info("GET /#{@db}/#{unesc(path)}")
|
113
|
+
@server.get("/#{@db}/#{path}")
|
114
|
+
end
|
115
|
+
|
116
|
+
def post(path=nil, json=nil)
|
117
|
+
@logger.info("POST /#{@db}/#{unesc(path)} #{json}")
|
118
|
+
@server.post("/#{@db}/#{path}", json)
|
119
|
+
end
|
120
|
+
|
121
|
+
def put(path=nil, json=nil)
|
122
|
+
@put_count += 1
|
123
|
+
@logger.info("PUT /#{@db}/#{unesc(path)} #{json}")
|
124
|
+
@server.put("/#{@db}/#{path}", json)
|
125
|
+
end
|
126
|
+
|
127
|
+
def unesc(path)
|
128
|
+
# path
|
129
|
+
path ? ::CGI::unescape(path) : ""
|
130
|
+
end
|
131
|
+
|
132
|
+
def uri
|
133
|
+
"#@server" / @db
|
134
|
+
end
|
135
|
+
|
136
|
+
def name
|
137
|
+
@db
|
138
|
+
end
|
139
|
+
|
140
|
+
def name=(name)
|
141
|
+
@db = name
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def create_db_if_non_existant(name)
|
147
|
+
begin
|
148
|
+
@server.get("/#{name}")
|
149
|
+
rescue
|
150
|
+
@server.put("/#{name}", "")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
# Represents a CouchDB view, which is implicitly sorted by key
|
4
|
+
# The view name is determined by sort attributes
|
5
|
+
class SortedByView
|
6
|
+
|
7
|
+
def initialize(class_name, *atts)
|
8
|
+
@class_name = class_name
|
9
|
+
@atts = atts
|
10
|
+
end
|
11
|
+
|
12
|
+
def map_function
|
13
|
+
key = @atts.map { |a| "doc.#{a}" }.join(", ")
|
14
|
+
key = @atts.size > 1 ? key.sub(/^/, "[").sub(/$/, "]") : key
|
15
|
+
|
16
|
+
<<-QUERY
|
17
|
+
function(doc) {
|
18
|
+
if(doc.class == "#{@class_name}") {
|
19
|
+
emit(#{key}, doc);
|
20
|
+
}
|
21
|
+
}
|
22
|
+
QUERY
|
23
|
+
end
|
24
|
+
|
25
|
+
def reduce_function
|
26
|
+
<<-QUERY
|
27
|
+
function(keys, values, rereduce) {
|
28
|
+
if (rereduce) {
|
29
|
+
return sum(values);
|
30
|
+
} else {
|
31
|
+
return values.length;
|
32
|
+
}
|
33
|
+
}
|
34
|
+
QUERY
|
35
|
+
end
|
36
|
+
|
37
|
+
def view_name
|
38
|
+
"all_sorted_by_" << @atts.join("_and_")
|
39
|
+
end
|
40
|
+
|
41
|
+
def query(query)
|
42
|
+
# If a view contains both a map and reduce function, CouchDB will by default return
|
43
|
+
# the result of the reduce function when queried.
|
44
|
+
# This class automatically creates both map and reduce functions so it can be used by the paginator.
|
45
|
+
# In normal usage, this class will be used with map functions, hence reduce is explicitly set
|
46
|
+
# to false if it hasn't already been set.
|
47
|
+
query.reduce(false) if query.reduce.nil?
|
48
|
+
|
49
|
+
method = query.keys ? :post : :get
|
50
|
+
|
51
|
+
begin
|
52
|
+
resp = RelaxDB.db.send(method, query.view_path, query.keys)
|
53
|
+
rescue => e
|
54
|
+
design_doc = DesignDocument.get(@class_name)
|
55
|
+
design_doc.add_map_view(view_name, map_function).add_reduce_view(view_name, reduce_function).save
|
56
|
+
resp = RelaxDB.db.send(method, query.view_path, query.keys)
|
57
|
+
end
|
58
|
+
|
59
|
+
data = JSON.parse(resp.body)
|
60
|
+
ViewResult.new(data)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class UuidGenerator
|
4
|
+
|
5
|
+
def self.uuid
|
6
|
+
unless @length
|
7
|
+
@uuid ||= UUID.new
|
8
|
+
@uuid.generate
|
9
|
+
else
|
10
|
+
rand.to_s[2, @length]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Convenience that helps relationship debuggging and model exploration
|
15
|
+
def self.id_length=(length)
|
16
|
+
@length = length
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|