couch_tomato 0.1.0
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/MIT-LICENSE.txt +19 -0
- data/README.md +96 -0
- data/init.rb +3 -0
- data/lib/core_ext/date.rb +10 -0
- data/lib/core_ext/duplicable.rb +43 -0
- data/lib/core_ext/extract_options.rb +14 -0
- data/lib/core_ext/inheritable_attributes.rb +222 -0
- data/lib/core_ext/object.rb +5 -0
- data/lib/core_ext/string.rb +19 -0
- data/lib/core_ext/symbol.rb +15 -0
- data/lib/core_ext/time.rb +12 -0
- data/lib/couch_tomato/database.rb +279 -0
- data/lib/couch_tomato/js_view_source.rb +182 -0
- data/lib/couch_tomato/migration.rb +52 -0
- data/lib/couch_tomato/migrator.rb +235 -0
- data/lib/couch_tomato/persistence/base.rb +62 -0
- data/lib/couch_tomato/persistence/belongs_to_property.rb +58 -0
- data/lib/couch_tomato/persistence/callbacks.rb +60 -0
- data/lib/couch_tomato/persistence/dirty_attributes.rb +27 -0
- data/lib/couch_tomato/persistence/json.rb +48 -0
- data/lib/couch_tomato/persistence/magic_timestamps.rb +15 -0
- data/lib/couch_tomato/persistence/properties.rb +58 -0
- data/lib/couch_tomato/persistence/simple_property.rb +97 -0
- data/lib/couch_tomato/persistence/validation.rb +18 -0
- data/lib/couch_tomato/persistence.rb +85 -0
- data/lib/couch_tomato/replicator.rb +50 -0
- data/lib/couch_tomato.rb +46 -0
- data/lib/tasks/couch_tomato.rake +128 -0
- data/rails/init.rb +7 -0
- data/spec/callbacks_spec.rb +271 -0
- data/spec/comment.rb +8 -0
- data/spec/create_spec.rb +22 -0
- data/spec/custom_view_spec.rb +134 -0
- data/spec/destroy_spec.rb +29 -0
- data/spec/fixtures/address.rb +9 -0
- data/spec/fixtures/person.rb +6 -0
- data/spec/property_spec.rb +103 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/unit/attributes_spec.rb +26 -0
- data/spec/unit/callbacks_spec.rb +33 -0
- data/spec/unit/create_spec.rb +58 -0
- data/spec/unit/customs_views_spec.rb +15 -0
- data/spec/unit/database_spec.rb +38 -0
- data/spec/unit/dirty_attributes_spec.rb +113 -0
- data/spec/unit/string_spec.rb +13 -0
- data/spec/unit/view_query_spec.rb +9 -0
- data/spec/update_spec.rb +40 -0
- data/test/test_helper.rb +63 -0
- data/test/unit/database_test.rb +285 -0
- data/test/unit/js_view_test.rb +362 -0
- data/test/unit/property_test.rb +193 -0
- metadata +133 -0
@@ -0,0 +1,279 @@
|
|
1
|
+
require 'couchrest'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
module CouchTomato
|
5
|
+
class Database
|
6
|
+
|
7
|
+
class ValidationsFailedError < ::StandardError; end
|
8
|
+
|
9
|
+
# Database
|
10
|
+
class_inheritable_accessor :prefix_string
|
11
|
+
class_inheritable_accessor :database_name
|
12
|
+
class_inheritable_accessor :database_server
|
13
|
+
class_inheritable_accessor :couchrest_db
|
14
|
+
class_inheritable_accessor :views
|
15
|
+
|
16
|
+
self.views = {}
|
17
|
+
self.prefix_string = ''
|
18
|
+
|
19
|
+
def self.database
|
20
|
+
return self.couchrest_db if self.couchrest_db
|
21
|
+
|
22
|
+
self.prefix_string ||= ''
|
23
|
+
|
24
|
+
tmp_prefix = self.prefix_string + '_' unless self.prefix_string.empty?
|
25
|
+
tmp_server = self.database_server + '/' unless self.database_server.match(/\/$/)
|
26
|
+
tmp_suffix = '_' + Rails.env if defined?(Rails)
|
27
|
+
|
28
|
+
self.couchrest_db = CouchRest.database("#{tmp_server}#{tmp_prefix}#{self.database_name}#{tmp_suffix}")
|
29
|
+
begin
|
30
|
+
self.couchrest_db.info
|
31
|
+
rescue RestClient::ResourceNotFound
|
32
|
+
raise "Database '#{tmp_prefix}#{self.database_name}#{tmp_suffix}' does not exist."
|
33
|
+
end
|
34
|
+
|
35
|
+
self.couchrest_db
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.prefix (name)
|
39
|
+
self.prefix_string = name || ''
|
40
|
+
end
|
41
|
+
def self.name (name)
|
42
|
+
raise 'You need to provide a database name' if name.nil?
|
43
|
+
self.database_name = (name.class == Symbol)? name.to_s : name
|
44
|
+
end
|
45
|
+
|
46
|
+
# TODO: specify db=>host mapping in yaml, and allow to differ per environment
|
47
|
+
def self.server (route='http://127.0.0.1:5984/')
|
48
|
+
self.database_server = route
|
49
|
+
|
50
|
+
# self.prefix_string ||= ''
|
51
|
+
#
|
52
|
+
# tmp_prefix = self.prefix_string + '_' unless self.prefix_string.empty?
|
53
|
+
# tmp_server = self.database_server + '/' unless self.database_server.match(/\/$/)
|
54
|
+
# tmp_suffix = '_' + Rails.env if defined?(Rails)
|
55
|
+
#
|
56
|
+
#
|
57
|
+
# self.couchrest_db ||= CouchRest.database("#{tmp_server}#{tmp_prefix}#{self.database_name}#{tmp_suffix}")
|
58
|
+
# begin
|
59
|
+
# self.couchrest_db.info
|
60
|
+
# rescue RestClient::ResourceNotFound
|
61
|
+
# raise "Database '#{tmp_prefix}#{self.database_name}#{tmp_suffix}' does not exist."
|
62
|
+
# end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
def self.view(name, options={})
|
68
|
+
raise 'A View nemonic must be specified' if name.nil?
|
69
|
+
self.views[name] = {}
|
70
|
+
self.views[name][:design_doc] = !options[:design_doc] ? self.database_name.to_sym : options.delete(:design_doc).to_sym
|
71
|
+
self.views[name][:view_name] = options.delete(:view_name) || name.to_s
|
72
|
+
self.views[name][:model] = options.delete(:model)
|
73
|
+
self.views[name][:couch_options] = options
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.save_document(document)
|
77
|
+
# TODO: Need to place some protected block here to respond to an exception in case trying to save
|
78
|
+
# a :raw document with an old _rev number
|
79
|
+
|
80
|
+
# return self.couchrest_db.save_doc(document) unless document.respond_to?(:dirty?)
|
81
|
+
|
82
|
+
# return true if document.respond_to?(:dirty?) && !document.dirty?
|
83
|
+
|
84
|
+
if document.new?
|
85
|
+
self.create_document document
|
86
|
+
else
|
87
|
+
self.update_document document
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.save_document!(document)
|
92
|
+
save_document(document) || raise(ValidationsFailedError.new(document.errors.full_messages))
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.bulk_save(documents)
|
96
|
+
doc_hashes = []
|
97
|
+
|
98
|
+
documents.each do |document|
|
99
|
+
document.run_callbacks :before_validation_on_save
|
100
|
+
document.run_callbacks(document.new? ? :before_validation_on_create : :before_validation_on_update)
|
101
|
+
return unless document.valid?
|
102
|
+
document.run_callbacks :before_save
|
103
|
+
document.run_callbacks(document.new? ? :before_create : :before_update)
|
104
|
+
|
105
|
+
doc_hashes << document.to_hash
|
106
|
+
end
|
107
|
+
|
108
|
+
res = self.couchrest_db.bulk_save(doc_hashes)
|
109
|
+
|
110
|
+
documents.each_with_index do |document, index|
|
111
|
+
is_new = document.new?
|
112
|
+
document._id = res[index]['id'] if is_new
|
113
|
+
document._rev = res[index]['rev']
|
114
|
+
document.run_callbacks :after_save
|
115
|
+
document.run_callbacks(is_new ? :after_create : :after_update)
|
116
|
+
end
|
117
|
+
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.destroy_document(document)
|
122
|
+
document.run_callbacks :before_destroy
|
123
|
+
document._deleted = true
|
124
|
+
self.database.delete_doc document.to_hash
|
125
|
+
document.run_callbacks :after_destroy
|
126
|
+
document._id = nil
|
127
|
+
document._rev = nil
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.load_document(id, options ={})
|
131
|
+
raise "Can't load a document without an id (got nil)" if id.nil?
|
132
|
+
|
133
|
+
begin
|
134
|
+
# json = self.couchrest_db.get(id)
|
135
|
+
# instance = Class.const_get(json['ruby_class']).json_create json
|
136
|
+
# # instance.database = self
|
137
|
+
# instance
|
138
|
+
json = self.database.get(id)
|
139
|
+
if options[:model] == :raw || !json['ruby_class']
|
140
|
+
{}.merge(json)
|
141
|
+
else
|
142
|
+
klass = class_from_string(json['ruby_class'])
|
143
|
+
instance = klass.json_create json
|
144
|
+
instance
|
145
|
+
end
|
146
|
+
rescue(RestClient::ResourceNotFound) #'Document not found'
|
147
|
+
nil
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.inspect
|
152
|
+
super
|
153
|
+
puts 'Database name: ' + (self.database_name || 'nil')
|
154
|
+
puts 'Database server: ' + (self.database_server || 'nil')
|
155
|
+
puts 'Views:'
|
156
|
+
pp self.views
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.query_view!(name, options={})
|
160
|
+
view = self.views[name]
|
161
|
+
raise 'View does not exist' unless view
|
162
|
+
|
163
|
+
begin
|
164
|
+
tmp_couch_opts = view[:couch_options] || {}
|
165
|
+
pr_options = options.merge(tmp_couch_opts)
|
166
|
+
results = self.query_view(name, pr_options)
|
167
|
+
self.process_results(name, results, pr_options)
|
168
|
+
rescue RestClient::ResourceNotFound# => e
|
169
|
+
raise
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class << self
|
174
|
+
alias_method :save, :save_document
|
175
|
+
alias_method :save_doc, :save_document
|
176
|
+
|
177
|
+
alias_method :save!, :save_document!
|
178
|
+
alias_method :save_doc!, :save_document!
|
179
|
+
|
180
|
+
alias_method :destroy, :destroy_document
|
181
|
+
alias_method :destroy_doc, :destroy_document
|
182
|
+
|
183
|
+
alias_method :load, :load_document
|
184
|
+
alias_method :load_doc, :load_document
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def self.create_document(document)
|
190
|
+
# document.database = self
|
191
|
+
document.run_callbacks :before_validation_on_save
|
192
|
+
document.run_callbacks :before_validation_on_create
|
193
|
+
return unless document.valid?
|
194
|
+
document.run_callbacks :before_save
|
195
|
+
document.run_callbacks :before_create
|
196
|
+
res = self.database.save_doc document.to_hash
|
197
|
+
document._rev = res['rev']
|
198
|
+
document._id = res['id']
|
199
|
+
document.run_callbacks :after_save
|
200
|
+
document.run_callbacks :after_create
|
201
|
+
true
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.update_document(document)
|
205
|
+
document.run_callbacks :before_validation_on_save
|
206
|
+
document.run_callbacks :before_validation_on_update
|
207
|
+
return unless document.valid?
|
208
|
+
document.run_callbacks :before_save
|
209
|
+
document.run_callbacks :before_update
|
210
|
+
res = self.database.save_doc document.to_hash
|
211
|
+
document._rev = res['rev']
|
212
|
+
document.run_callbacks :after_save
|
213
|
+
document.run_callbacks :after_update
|
214
|
+
true
|
215
|
+
end
|
216
|
+
|
217
|
+
def self.query_view(name,parameters)
|
218
|
+
self.database.view "#{self.views[name][:design_doc]}/#{self.views[name][:view_name]}", parameters
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.process_results(name, results, options ={})
|
222
|
+
# view = self.views[name]
|
223
|
+
options = self.views[name].merge(options)
|
224
|
+
|
225
|
+
couch_opts = options[:couch_options] || {}
|
226
|
+
return_raw = couch_opts[:reduce] || options[:model] == :raw
|
227
|
+
|
228
|
+
first_result = results['rows'][0]
|
229
|
+
|
230
|
+
if (first_result && first_result['doc'] && first_result['value'])
|
231
|
+
raise "View results contain doc and value keys. Don't pass `:include_docs => true` to a view that does not `emit(some_key,null)`"
|
232
|
+
end
|
233
|
+
|
234
|
+
field_to_read = first_result && first_result['doc'] ? 'doc' : 'value'
|
235
|
+
|
236
|
+
# TODO: This code looks like C (REVIEW)
|
237
|
+
results['rows'].map do |row|
|
238
|
+
next row[field_to_read] if return_raw
|
239
|
+
|
240
|
+
model = options[:model]
|
241
|
+
if model
|
242
|
+
model = model.kind_of?(String) ? class_from_string(model) : model
|
243
|
+
meta = {'id' => row['id']}.merge({'key' => row['key']})
|
244
|
+
model.json_create(row[field_to_read], meta)
|
245
|
+
else
|
246
|
+
if row[field_to_read]['ruby_class'].nil?
|
247
|
+
row[field_to_read]
|
248
|
+
else
|
249
|
+
meta = {'id' => row['id']}.merge({'key' => row['key']})
|
250
|
+
# Class.const_get(row[field_to_read]['ruby_class']).json_create(row[field_to_read], meta)
|
251
|
+
klass = class_from_string(row[field_to_read]['ruby_class'])
|
252
|
+
klass.json_create(row[field_to_read], meta)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end # results do
|
256
|
+
end
|
257
|
+
|
258
|
+
private
|
259
|
+
|
260
|
+
def self.class_from_string(string)
|
261
|
+
string.to_s.split('::').inject(Object){|a, m| a = a.const_get(m.to_sym)}
|
262
|
+
end
|
263
|
+
|
264
|
+
end # class
|
265
|
+
end # module
|
266
|
+
|
267
|
+
# if return_raw
|
268
|
+
# row[field_to_read]
|
269
|
+
# elsif options[:model].nil?
|
270
|
+
# if row[field_to_read]['ruby_class'].nil?
|
271
|
+
# row[field_to_read]
|
272
|
+
# else
|
273
|
+
# meta = {'id' => row['id']}.merge({'key' => row['key']})
|
274
|
+
# Class.const_get(row[field_to_read]['ruby_class']).json_create(row[field_to_read], meta)
|
275
|
+
# end
|
276
|
+
# else
|
277
|
+
# meta = {'id' => row['id']}.merge({'key' => row['key']})
|
278
|
+
# options[:model].json_create(row[field_to_read], meta)
|
279
|
+
# end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
module CouchTomato
|
4
|
+
class JsViewSource
|
5
|
+
# todo: provide a 'dirty?' method that can be called in an initializer and warn the developer that view are out of sync
|
6
|
+
# todo: provide more granular information about which views are being modified
|
7
|
+
# todo: limitation (bug?) where if you remove a database's views entirely from the file system, view's will not be removed from the database as may be expected
|
8
|
+
def self.push(silent=false)
|
9
|
+
fs_database_names.each do |database_name|
|
10
|
+
db = database!(database_name)
|
11
|
+
|
12
|
+
fs_docs = fs_design_docs(database_name)
|
13
|
+
db_docs = db_design_docs(db)
|
14
|
+
|
15
|
+
fs_docs.each do |design_name, fs_doc|
|
16
|
+
db_doc = db_docs[design_name]
|
17
|
+
|
18
|
+
if db_doc
|
19
|
+
fs_doc['_id'] = db_doc['_id']
|
20
|
+
fs_doc['_rev'] = db_doc['_rev']
|
21
|
+
end
|
22
|
+
|
23
|
+
if fs_doc['views'].empty?
|
24
|
+
next unless fs_doc['_rev']
|
25
|
+
puts "DELETE #{fs_doc['_id']}" unless silent
|
26
|
+
db.delete_doc(fs_doc)
|
27
|
+
else
|
28
|
+
if changed_views?(fs_doc, db_doc)
|
29
|
+
puts "UPDATE #{fs_doc['_id']}" unless silent
|
30
|
+
db.save_doc(fs_doc)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.changed_views?(fs_doc, db_doc)
|
38
|
+
return true if db_doc.nil?
|
39
|
+
|
40
|
+
fs_doc['views'].each do |name, fs_view|
|
41
|
+
db_view = db_doc['views'][name]
|
42
|
+
%w(map reduce).each do |method|
|
43
|
+
return true if db_view.nil? || db_view["sha1-#{method}"] != fs_view["sha1-#{method}"]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
return false
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.diff
|
51
|
+
fs_database_names.each do |database_name|
|
52
|
+
db = database!(database_name)
|
53
|
+
|
54
|
+
fs_docs = fs_design_docs(database_name)
|
55
|
+
db_docs = db_design_docs(db)
|
56
|
+
|
57
|
+
# design docs on fs but not in db
|
58
|
+
(fs_docs.keys - db_docs.keys).each do |design_name|
|
59
|
+
unless fs_docs[design_name]['views'].empty?
|
60
|
+
puts " NEW: #{database_name}/_#{design_name}: #{fs_docs[design_name]['views'].keys.join(', ')}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# design docs in db but not on fs
|
65
|
+
(db_docs.keys - fs_docs.keys).each do |design_name|
|
66
|
+
puts "REMOVED: #{database_name}/_#{design_name}"
|
67
|
+
end
|
68
|
+
|
69
|
+
# design docs in both db and fs
|
70
|
+
(fs_docs.keys & db_docs.keys).each do |design_name|
|
71
|
+
common_view_keys = (fs_docs[design_name]['views'].keys & db_docs[design_name]['views'].keys)
|
72
|
+
fs_only_view_keys = fs_docs[design_name]['views'].keys - common_view_keys
|
73
|
+
db_only_view_keys = db_docs[design_name]['views'].keys - common_view_keys
|
74
|
+
|
75
|
+
unless fs_only_view_keys.empty?
|
76
|
+
methods = fs_only_view_keys.map do |key|
|
77
|
+
%w(map reduce).map {|method| fs_docs[design_name]['views'][key][method].nil? ? nil : "#{key}.#{method}()"}.compact
|
78
|
+
end.flatten
|
79
|
+
puts " ADDED: #{database_name}/_#{design_name}: #{methods.join(', ')}"
|
80
|
+
end
|
81
|
+
|
82
|
+
unless db_only_view_keys.empty?
|
83
|
+
methods = db_only_view_keys.map do |key|
|
84
|
+
%w(map reduce).map {|method| db_docs[design_name]['views'][key][method] ? "#{key}.#{method}()" : nil}.compact
|
85
|
+
end
|
86
|
+
puts "REMOVED: #{database_name}/_#{design_name}: #{methods.join(', ')}"
|
87
|
+
end
|
88
|
+
|
89
|
+
common_view_keys.each do |common_key|
|
90
|
+
# are the sha's the same?
|
91
|
+
# map reduce sha1
|
92
|
+
fs_view = fs_docs[design_name]['views'][common_key]
|
93
|
+
db_view = db_docs[design_name]['views'][common_key]
|
94
|
+
|
95
|
+
# has either the map or reduce been added or removed
|
96
|
+
%w(map reduce).each do |method|
|
97
|
+
if db_view[method] && !fs_view[method]
|
98
|
+
puts "REMOVED: #{database_name}/_#{design_name}:#{method}()" and next
|
99
|
+
end
|
100
|
+
|
101
|
+
if fs_view[method] && !db_view[method]
|
102
|
+
puts " ADDED: #{database_name}/_#{design_name}:#{method}()" and next
|
103
|
+
end
|
104
|
+
|
105
|
+
if fs_view["sha1-#{method}"] != db_view["sha1-#{method}"]
|
106
|
+
puts "OUTDATED: #{database_name}/_#{design_name}:#{method}()" and next
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def self.path(db_name="")
|
118
|
+
"#{Rails.root}/db/views/#{db_name}" if Rails
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.fs_database_names
|
122
|
+
Dir[path + "/**"].map {|path| path.split('/').last}
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.db_design_docs(db)
|
126
|
+
design_docs = db.get("_all_docs", {:startkey => "_design/", :endkey => "_design0", :include_docs => true})['rows']
|
127
|
+
design_docs.inject({}) do |res, row|
|
128
|
+
doc = row['doc']
|
129
|
+
design_name = doc['_id'].split('/').last
|
130
|
+
res[design_name.to_sym] = doc#['views']
|
131
|
+
res
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# :clicks => {'by_date' => {'map' => ..., 'reduce' => ..., sha1-map => ..., sha1-reduce => ...} }
|
136
|
+
def self.fs_design_docs(db_name)
|
137
|
+
design_docs = {}
|
138
|
+
|
139
|
+
path = "#{RAILS_ROOT}/couchdb/views/#{db_name}"
|
140
|
+
Dir[path + "/**"].each do |file|
|
141
|
+
throw "Invalid filename '#{File.basename(file)}': expecting '-map.js' or '-reduce.js' suffix" unless file.match(/-((map)|(reduce))\.js$/)
|
142
|
+
|
143
|
+
design_name = db_name.to_sym
|
144
|
+
design_docs[design_name] ||= {'_id' => "_design/#{db_name}", 'views' => {}}
|
145
|
+
fs_view(design_docs[design_name], file)
|
146
|
+
end
|
147
|
+
|
148
|
+
design_docs.each do |db, design|
|
149
|
+
design["views"].each do |view, functions|
|
150
|
+
if !functions["reduce"].nil?
|
151
|
+
raise "#{view}-reduce was found without a corresponding map function." if functions["map"].nil?
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
design_docs
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.fs_view(design_doc, view_path)
|
160
|
+
filename = view_path.split('/').last.split('.').first
|
161
|
+
name, type = filename.split('-')
|
162
|
+
|
163
|
+
file = open(view_path)
|
164
|
+
content = file.read
|
165
|
+
file.close
|
166
|
+
|
167
|
+
sha1 = Digest::SHA1.hexdigest(content)
|
168
|
+
|
169
|
+
design_doc['views'][name] ||= {}
|
170
|
+
design_doc['views'][name][type] = content
|
171
|
+
design_doc['views'][name]["sha1-#{type}"] = sha1
|
172
|
+
design_doc
|
173
|
+
end
|
174
|
+
|
175
|
+
# todo: don't depend on "proprietary" APP_CONFIG
|
176
|
+
def self.database!(database_name)
|
177
|
+
CouchRest.database!("http://" + APP_CONFIG["couchdb_address"] + ":" + APP_CONFIG["couchdb_port"].to_s \
|
178
|
+
+ "/" + APP_CONFIG["couchdb_basename"] + "_" + database_name + "_" + RAILS_ENV)
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module CouchTomato
|
2
|
+
class Migration
|
3
|
+
class << self
|
4
|
+
# Execute this migration in the named direction
|
5
|
+
def migrate(direction, db)
|
6
|
+
return unless respond_to?(direction)
|
7
|
+
|
8
|
+
case direction
|
9
|
+
when :up then announce "migrating"
|
10
|
+
when :down then announce "reverting"
|
11
|
+
end
|
12
|
+
|
13
|
+
time = Benchmark.measure do
|
14
|
+
docs = db.get('_all_docs', :include_docs => true)['rows']
|
15
|
+
docs.each do |doc|
|
16
|
+
next if /^_design/ =~ doc['id']
|
17
|
+
send(direction, doc['doc'])
|
18
|
+
db.save_doc(doc['doc'])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
case direction
|
23
|
+
when :up then announce "migrated (%.4fs)" % time.real; write
|
24
|
+
when :down then announce "reverted (%.4fs)" % time.real; write
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def write(text="")
|
29
|
+
puts(text)
|
30
|
+
end
|
31
|
+
|
32
|
+
def announce(message)
|
33
|
+
text = "#{@version} #{name}: #{message}"
|
34
|
+
length = [0, 75 - text.length].max
|
35
|
+
write "== %s %s" % [text, "=" * length]
|
36
|
+
end
|
37
|
+
|
38
|
+
def say(message, subitem=false)
|
39
|
+
write "#{subitem ? " ->" : "--"} #{message}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def say_with_time(message)
|
43
|
+
say(message)
|
44
|
+
result = nil
|
45
|
+
time = Benchmark.measure { result = yield }
|
46
|
+
say "%.4fs" % time.real, :subitem
|
47
|
+
say("#{result} rows", :subitem) if result.is_a?(Integer)
|
48
|
+
result
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|