namelessjon-couchrest 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +46 -0
  3. data/Rakefile +69 -0
  4. data/THANKS.md +21 -0
  5. data/couchrest.gemspec +111 -0
  6. data/examples/word_count/markov +38 -0
  7. data/examples/word_count/views/books/chunked-map.js +3 -0
  8. data/examples/word_count/views/books/united-map.js +1 -0
  9. data/examples/word_count/views/markov/chain-map.js +6 -0
  10. data/examples/word_count/views/markov/chain-reduce.js +7 -0
  11. data/examples/word_count/views/word_count/count-map.js +6 -0
  12. data/examples/word_count/views/word_count/count-reduce.js +3 -0
  13. data/examples/word_count/word_count.rb +46 -0
  14. data/examples/word_count/word_count_query.rb +40 -0
  15. data/examples/word_count/word_count_views.rb +26 -0
  16. data/history.txt +145 -0
  17. data/lib/couchrest/commands/generate.rb +71 -0
  18. data/lib/couchrest/commands/push.rb +103 -0
  19. data/lib/couchrest/database.rb +373 -0
  20. data/lib/couchrest/design.rb +80 -0
  21. data/lib/couchrest/document.rb +89 -0
  22. data/lib/couchrest/helper/attachments.rb +29 -0
  23. data/lib/couchrest/helper/pager.rb +103 -0
  24. data/lib/couchrest/helper/streamer.rb +51 -0
  25. data/lib/couchrest/helper/upgrade.rb +52 -0
  26. data/lib/couchrest/json_response.rb +14 -0
  27. data/lib/couchrest/middlewares/logger.rb +263 -0
  28. data/lib/couchrest/monkeypatches.rb +42 -0
  29. data/lib/couchrest/response.rb +35 -0
  30. data/lib/couchrest/rest_api.rb +62 -0
  31. data/lib/couchrest/server.rb +90 -0
  32. data/lib/couchrest/support/inheritable_attributes.rb +107 -0
  33. data/lib/couchrest.rb +127 -0
  34. data/spec/couchrest/couchrest_spec.rb +202 -0
  35. data/spec/couchrest/database_spec.rb +870 -0
  36. data/spec/couchrest/design_spec.rb +158 -0
  37. data/spec/couchrest/document_spec.rb +279 -0
  38. data/spec/couchrest/helpers/pager_spec.rb +123 -0
  39. data/spec/couchrest/helpers/streamer_spec.rb +52 -0
  40. data/spec/couchrest/server_spec.rb +35 -0
  41. data/spec/fixtures/attachments/README +3 -0
  42. data/spec/fixtures/attachments/couchdb.png +0 -0
  43. data/spec/fixtures/attachments/test.html +11 -0
  44. data/spec/fixtures/views/lib.js +3 -0
  45. data/spec/fixtures/views/test_view/lib.js +3 -0
  46. data/spec/fixtures/views/test_view/only-map.js +4 -0
  47. data/spec/fixtures/views/test_view/test-map.js +3 -0
  48. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  49. data/spec/spec.opts +5 -0
  50. data/spec/spec_helper.rb +44 -0
  51. data/utils/remap.rb +27 -0
  52. data/utils/subset.rb +30 -0
  53. metadata +179 -0
data/history.txt ADDED
@@ -0,0 +1,145 @@
1
+ == Next Version
2
+
3
+ * Major enhancements
4
+
5
+ * Minor enhancements
6
+ * rest-client version mismatch between couchrest.rb & gemspec
7
+
8
+ == 1.0.0
9
+
10
+ * Major enhancements
11
+ * Moved ExtendedDocument and friends into own library, couchrest_extended_document. (Sam Lown)
12
+ * Removed HttpAbstraction component for direct interface with RestClient. (Sam Lown)
13
+ * Changed version to more conventional format starting from 1.0.0 to avoid ambiguity issues with order. (Sam Lown)
14
+
15
+ == 0.38
16
+
17
+ * Major enhancements
18
+ * Add create_target option to Database#replicate_to and #replicate_from. http://github.com/couchrest/couchrest/issues/#issue/26 (Alexander Uvarov)
19
+ * Removing unused core extensions and moving extlib_inhertiable_* methods to use couchrest_inheritable_*
20
+ to avoid conflicts with Rails. (Geoff Buesing)
21
+
22
+ * Minor enhancements
23
+ * Support for CouchDB 1.0
24
+ * Added Document#id= support (issue detected by Rory Franklin with RSpec model stubs)
25
+ * Fixing issues with CouchDB 1.0 and RestClient
26
+
27
+ == 0.37
28
+
29
+ * Minor enhancements
30
+ * Added gemspec (needed for Bundler install) (Tapajós)
31
+
32
+ == 0.36
33
+
34
+ * Major enhancements
35
+ * Adds support for continuous replication (sauy7)
36
+ * Automatic Type Casting (Alexander Uvarov, Sam Lown, Tim Heighes, Will Leinweber)
37
+ * Added a search method to CouchRest:Database to search the documents in a given database. (Dave Farkas, Arnaud Berthomier, John Wood)
38
+
39
+ * Minor enhancements
40
+ * Provide a description of the timeout error (John Wood)
41
+
42
+ == 0.35
43
+
44
+ * Major enhancements
45
+ * CouchRest::ExtendedDocument allow chaining the inherit class callback (Kenneth Kalmer) - http://github.com/couchrest/couchrest/issues#issue/8
46
+
47
+ * Minor enhancements
48
+ * Fix attachment bug (Johannes Jörg Schmidt)
49
+ * Fix create database exception bug (Damien Mathieu)
50
+ * Compatible with restclient >= 1.4.0 new responses (Julien Kirch)
51
+ * Bug fix: Attribute protection no longer strips attributes coming from the database (Will Leinweber)
52
+ * Bug fix: Remove double CGI escape when PUTting an attachment (nzoschke)
53
+ * Bug fix: Changing Class proxy to set database on result sets (Peter Gumeson)
54
+ * Bug fix: Updated time regexp (Nolan Darilek)
55
+ * Added an update_doc method to database to handle conflicts during atomic updates. (Pierre Larochelle)
56
+ * Bug fix: http://github.com/couchrest/couchrest/issues/#issue/2 (Luke Burton)
57
+
58
+ == 0.34
59
+
60
+ * Major enhancements
61
+
62
+ * Added support for https database URIs. (Mathias Meyer)
63
+ * Changing some validations to be compatible with activemodel. (Marcos Tapajós)
64
+ * Adds attribute protection to properties. (Will Leinweber)
65
+ * Improved CouchRest::Database#save_doc, added "batch" mode to significantly speed up saves at cost of lower durability gurantees. (Igal Koshevoy)
66
+ * Added CouchRest::Database#bulk_save_doc and #batch_save_doc as human-friendlier wrappers around #save_doc. (Igal Koshevoy)
67
+
68
+ * Minor enhancements
69
+
70
+ * Fix content_type handling for attachments
71
+ * Fixed a bug in the pagination code that caused it to paginate over records outside of the scope of the view parameters.(John Wood)
72
+ * Removed amount_pages calculation for the pagination collection, since it cannot be reliably calculated without a view.(John Wood)
73
+ * Bug fix: http://github.com/couchrest/couchrest/issues/#issue/2 (Luke Burton)
74
+ * Bug fix: http://github.com/couchrest/couchrest/issues/#issue/1 (Marcos Tapajós)
75
+ * Removed the Database class deprecation notices (Matt Aimonetti)
76
+ * Adding support to :cast_as => 'Date'. (Marcos Tapajós)
77
+ * Improve documentation (Marcos Tapajós)
78
+ * Streamer fixes (Julien Sanchez)
79
+ * Fix Save on Document & ExtendedDocument crashed if bulk (Julien Sanchez)
80
+ * Fix Initialization of ExtendentDocument model shouldn't failed on a nil value in argument (deepj)
81
+ * Change to use Jeweler and Gemcutter (Marcos Tapajós)
82
+
83
+ == 0.33
84
+
85
+ * Major enhancements
86
+
87
+ * Added a new Rack logger middleware letting you log/save requests/queries (Matt Aimonetti)
88
+
89
+ * Minor enhancements
90
+
91
+ * Added #amount_pages to a paginated result array (Matt Aimonetti)
92
+ * Ruby 1.9.2 compatible (Matt Aimonetti)
93
+ * Added a property? method for property cast as :boolean (John Wood)
94
+ * Added an option to force the deletion of a attachments (bypass 409s) (Matt Aimonetti)
95
+ * Created a new abstraction layer for the REST API (Matt Aimonetti)
96
+ * Bug fix: made ExtendedDocument#all compatible with Couch 0.10 (tc)
97
+
98
+ == 0.32
99
+
100
+ * Major enhancements
101
+
102
+ * ExtendedDocument.get doesn't raise an exception anymore. If no documents are found nil is returned.
103
+ * ExtendedDocument.get! works the say #get used to work and will raise an exception if a document isn't found.
104
+
105
+ * Minor enhancements
106
+
107
+ * Bug fix: Model.all(:keys => [1,2]) was not working (Matt Aimonetti)
108
+ * Added ValidationErrors#count in order to play nicely with Rails (Peter Wagenet)
109
+ * Bug fix: class proxy design doc refresh (Daniel Kirsh)
110
+ * Bug fix: the count method on the proxy collection was missing (Daniel Kirsch)
111
+ * Added #amount_pages to a paginated collection. (Matt Aimonetti)
112
+
113
+ == 0.31
114
+
115
+ * Major enhancements
116
+
117
+ * Created an abstraction HTTP layer to support different http adapters (Matt Aimonetti)
118
+ * Added ExtendedDocument.create({}) and #create!({}) so you don't have to do Model.new.create (Matt Aimonetti)
119
+
120
+ * Minor enhancements
121
+
122
+ * Added an init.rb file for easy usage as a Rails plugin (Aaron Quint)
123
+ * Bug fix: pagination shouldn't die on empty results (Arnaud Berthomier)
124
+ * Optimized ExtendedDocument.count to run about 3x faster (Matt Aimonetti)
125
+ * Added Float casting (Ryan Felton & Matt Aimonetti)
126
+
127
+ == 0.30
128
+
129
+ * Major enhancements
130
+
131
+ * Added support for pagination (John Wood)
132
+ * Improved performance when initializing documents with timestamps (Matt Aimonetti)
133
+
134
+ * Minor enhancements
135
+
136
+ * Extended the API to retrieve an attachment URI (Matt Aimonetti)
137
+ * Bug fix: default value should be able to be set as false (Alexander Uvarov)
138
+ * Bug fix: validates_is_numeric should be able to properly validate a Float instance (Rob Kaufman)
139
+ * Bug fix: fixed the Timeout implementation (Seth Falcon)
140
+
141
+
142
+ ---
143
+
144
+ Unfortunately, before 0.30 we did not keep a track of the modifications made to CouchRest.
145
+ You can see the full commit history on GitHub: http://github.com/couchrest/couchrest/commits/master/
@@ -0,0 +1,71 @@
1
+ require 'fileutils'
2
+
3
+ module CouchRest
4
+ module Commands
5
+ module Generate
6
+
7
+ def self.run(options)
8
+ directory = options[:directory]
9
+ design_names = options[:trailing_args]
10
+
11
+ FileUtils.mkdir_p(directory)
12
+ filename = File.join(directory, "lib.js")
13
+ self.write(filename, <<-FUNC)
14
+ // Put global functions here.
15
+ // Include in your views with
16
+ //
17
+ // //include-lib
18
+ FUNC
19
+
20
+ design_names.each do |design_name|
21
+ subdirectory = File.join(directory, design_name)
22
+ FileUtils.mkdir_p(subdirectory)
23
+ filename = File.join(subdirectory, "sample-map.js")
24
+ self.write(filename, <<-FUNC)
25
+ function(doc) {
26
+ // Keys is first letter of _id
27
+ emit(doc._id[0], doc);
28
+ }
29
+ FUNC
30
+
31
+ filename = File.join(subdirectory, "sample-reduce.js")
32
+ self.write(filename, <<-FUNC)
33
+ function(keys, values) {
34
+ // Count the number of keys starting with this letter
35
+ return values.length;
36
+ }
37
+ FUNC
38
+
39
+ filename = File.join(subdirectory, "lib.js")
40
+ self.write(filename, <<-FUNC)
41
+ // Put functions specific to '#{design_name}' here.
42
+ // Include in your views with
43
+ //
44
+ // //include-lib
45
+ FUNC
46
+ end
47
+ end
48
+
49
+ def self.help
50
+ helpstring = <<-GEN
51
+
52
+ Usage: couchview generate directory design1 design2 design3 ...
53
+
54
+ Couchview will create directories and example views for the design documents you specify.
55
+
56
+ GEN
57
+ helpstring.gsub(/^ /, '')
58
+ end
59
+
60
+ def self.write(filename, contents)
61
+ puts "Writing #{filename}"
62
+ File.open(filename, "w") do |f|
63
+ # Remove leading spaces
64
+ contents.gsub!(/^ ( )?/, '')
65
+ f.write contents
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,103 @@
1
+ module CouchRest
2
+
3
+ module Commands
4
+
5
+ module Push
6
+
7
+ def self.run(options)
8
+ directory = options[:directory]
9
+ database = options[:trailing_args].first
10
+
11
+ fm = CouchRest::FileManager.new(database)
12
+ fm.loud = options[:loud]
13
+
14
+ if options[:loud]
15
+ puts "Pushing views from directory #{directory} to database #{fm.db}"
16
+ end
17
+
18
+ fm.push_views(directory)
19
+ end
20
+
21
+ def self.help
22
+ helpstring = <<-GEN
23
+
24
+ == Pushing views with Couchview ==
25
+
26
+ Usage: couchview push directory dbname
27
+
28
+ Couchview expects a specific filesystem layout for your CouchDB views (see
29
+ example below). It also supports advanced features like inlining of library
30
+ code (so you can keep DRY) as well as avoiding unnecessary document
31
+ modification.
32
+
33
+ Couchview also solves a problem with CouchDB's view API, which only provides
34
+ access to the final reduce side of any views which have both a map and a
35
+ reduce function defined. The intermediate map results are often useful for
36
+ development and production. CouchDB is smart enough to reuse map indexes for
37
+ functions duplicated across views within the same design document.
38
+
39
+ For views with a reduce function defined, Couchview creates both a reduce view
40
+ and a map-only view, so that you can browse and query the map side as well as
41
+ the reduction, with no performance penalty.
42
+
43
+ == Example ==
44
+
45
+ couchview push foo-project/bar-views baz-database
46
+
47
+ This will push the views defined in foo-project/bar-views into a database
48
+ called baz-database. Couchview expects the views to be defined in files with
49
+ names like:
50
+
51
+ foo-project/bar-views/my-design/viewname-map.js
52
+ foo-project/bar-views/my-design/viewname-reduce.js
53
+ foo-project/bar-views/my-design/noreduce-map.js
54
+
55
+ Pushed to => http://127.0.0.1:5984/baz-database/_design/my-design
56
+
57
+ And the design document:
58
+ {
59
+ "views" : {
60
+ "viewname-map" : {
61
+ "map" : "### contents of view-name-map.js ###"
62
+ },
63
+ "viewname-reduce" : {
64
+ "map" : "### contents of view-name-map.js ###",
65
+ "reduce" : "### contents of view-name-reduce.js ###"
66
+ },
67
+ "noreduce-map" : {
68
+ "map" : "### contents of noreduce-map.js ###"
69
+ }
70
+ }
71
+ }
72
+
73
+ Couchview will create a design document for each subdirectory of the views
74
+ directory specified on the command line.
75
+
76
+ == Library Inlining ==
77
+
78
+ Couchview can optionally inline library code into your views so you only have
79
+ to maintain it in one place. It looks for any files named lib.* in your
80
+ design-doc directory (for doc specific libs) and in the parent views directory
81
+ (for project global libs). These libraries are only inserted into views which
82
+ include the text
83
+
84
+ // !include lib
85
+
86
+ or
87
+
88
+ # !include lib
89
+
90
+ Couchview is a result of scratching my own itch. I'd be happy to make it more
91
+ general, so please contact me at jchris@grabb.it if you'd like to see anything
92
+ added or changed.
93
+
94
+ GEN
95
+ helpstring.gsub(/^ /, '')
96
+ end
97
+
98
+ end
99
+
100
+
101
+ end
102
+
103
+ end
@@ -0,0 +1,373 @@
1
+ require 'cgi'
2
+ require 'couchrest/document'
3
+ require 'couchrest/design'
4
+ require 'couchrest/helper/streamer'
5
+
6
+ module CouchRest
7
+ class Database
8
+ attr_reader :server, :host, :name, :root, :uri
9
+ attr_accessor :bulk_save_cache_limit
10
+
11
+ # Create a CouchRest::Database adapter for the supplied CouchRest::Server
12
+ # and database name.
13
+ #
14
+ # ==== Parameters
15
+ # server<CouchRest::Server>:: database host
16
+ # name<String>:: database name
17
+ #
18
+ def initialize(server, name)
19
+ @name = name
20
+ @server = server
21
+ @host = server.uri
22
+ @uri = "/#{name.gsub('/','%2F')}"
23
+ @root = host + uri
24
+ @streamer = Streamer.new(self)
25
+ @bulk_save_cache = []
26
+ @bulk_save_cache_limit = 500 # must be smaller than the uuid count
27
+ end
28
+
29
+ # returns the database's uri
30
+ def to_s
31
+ @root
32
+ end
33
+
34
+ # GET the database info from CouchDB
35
+ def info
36
+ CouchRest.get @root
37
+ end
38
+
39
+ # Query the <tt>_all_docs</tt> view. Accepts all the same arguments as view.
40
+ def documents(params = {})
41
+ keys = params.delete(:keys)
42
+ url = CouchRest.paramify_url "#{@root}/_all_docs", params
43
+ if keys
44
+ CouchRest.post(url, {:keys => keys})
45
+ else
46
+ CouchRest.get url
47
+ end
48
+ end
49
+
50
+ # Query a CouchDB-Lucene search view
51
+ def search(name, params={})
52
+ # -> http://localhost:5984/yourdb/_fti/YourDesign/by_name?include_docs=true&q=plop*'
53
+ url = CouchRest.paramify_url "#{root}/_fti/#{name}", params
54
+ CouchRest.get url
55
+ end
56
+
57
+ # load a set of documents by passing an array of ids
58
+ def get_bulk(ids)
59
+ documents(:keys => ids, :include_docs => true)
60
+ end
61
+ alias :bulk_load :get_bulk
62
+
63
+ # POST a temporary view function to CouchDB for querying. This is not
64
+ # recommended, as you don't get any performance benefit from CouchDB's
65
+ # materialized views. Can be quite slow on large databases.
66
+ def slow_view(funcs, params = {})
67
+ keys = params.delete(:keys)
68
+ funcs = funcs.merge({:keys => keys}) if keys
69
+ url = CouchRest.paramify_url "#{@root}/_temp_view", params
70
+ JSON.parse(RestClient.post(url, funcs.to_json, CouchRest.default_headers))
71
+ end
72
+
73
+ # backwards compatibility is a plus
74
+ alias :temp_view :slow_view
75
+
76
+ # Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts
77
+ # paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
78
+ def view(name, params = {}, &block)
79
+ keys = params.delete(:keys)
80
+ name = name.split('/') # I think this will always be length == 2, but maybe not...
81
+ dname = name.shift
82
+ vname = name.join('/')
83
+ url = CouchRest.paramify_url "#{@root}/_design/#{dname}/_view/#{vname}", params
84
+ if keys
85
+ CouchRest.post(url, {:keys => keys})
86
+ else
87
+ if block_given?
88
+ @streamer.view("_design/#{dname}/_view/#{vname}", params, &block)
89
+ else
90
+ CouchRest.get url
91
+ end
92
+ end
93
+ end
94
+
95
+ # GET a document from CouchDB, by id. Returns a Ruby Hash.
96
+ def get(id, params = {})
97
+ slug = escape_docid(id)
98
+ url = CouchRest.paramify_url("#{@root}/#{slug}", params)
99
+ result = CouchRest.get(url)
100
+ return result unless result.is_a?(Hash)
101
+ doc = if /^_design/ =~ result["_id"]
102
+ Design.new(result)
103
+ else
104
+ Document.new(result)
105
+ end
106
+ doc.database = self
107
+ doc
108
+ end
109
+
110
+ # GET an attachment directly from CouchDB
111
+ def fetch_attachment(doc, name)
112
+ uri = url_for_attachment(doc, name)
113
+ RestClient.get uri, CouchRest.default_headers
114
+ end
115
+
116
+ # PUT an attachment directly to CouchDB
117
+ def put_attachment(doc, name, file, options = {})
118
+ uri = url_for_attachment(doc, name)
119
+ JSON.parse(RestClient.put(uri, file, CouchRest.default_headers.merge(options)))
120
+ end
121
+
122
+ # DELETE an attachment directly from CouchDB
123
+ def delete_attachment(doc, name, force=false)
124
+ uri = url_for_attachment(doc, name)
125
+ # this needs a rev
126
+ begin
127
+ CouchRest.delete(uri)
128
+ rescue Exception => error
129
+ if force
130
+ # get over a 409
131
+ doc = get(doc['_id'])
132
+ uri = url_for_attachment(doc, name)
133
+ CouchRest.delete(uri)
134
+ else
135
+ error
136
+ end
137
+ end
138
+ end
139
+
140
+ # Save a document to CouchDB. This will use the <tt>_id</tt> field from
141
+ # the document as the id for PUT, or request a new UUID from CouchDB, if
142
+ # no <tt>_id</tt> is present on the document. IDs are attached to
143
+ # documents on the client side because POST has the curious property of
144
+ # being automatically retried by proxies in the event of network
145
+ # segmentation and lost responses.
146
+ #
147
+ # If <tt>bulk</tt> is true (false by default) the document is cached for bulk-saving later.
148
+ # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
149
+ #
150
+ # If <tt>batch</tt> is true (false by default) the document is saved in
151
+ # batch mode, "used to achieve higher throughput at the cost of lower
152
+ # guarantees. When [...] sent using this option, it is not immediately
153
+ # written to disk. Instead it is stored in memory on a per-user basis for a
154
+ # second or so (or the number of docs in memory reaches a certain point).
155
+ # After the threshold has passed, the docs are committed to disk. Instead
156
+ # of waiting for the doc to be written to disk before responding, CouchDB
157
+ # sends an HTTP 202 Accepted response immediately. batch=ok is not suitable
158
+ # for crucial data, but it ideal for applications like logging which can
159
+ # accept the risk that a small proportion of updates could be lost due to a
160
+ # crash."
161
+ def save_doc(doc, bulk = false, batch = false)
162
+ if doc['_attachments']
163
+ doc['_attachments'] = encode_attachments(doc['_attachments'])
164
+ end
165
+ if bulk
166
+ @bulk_save_cache << doc
167
+ bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
168
+ return {"ok" => true} # Compatibility with Document#save
169
+ elsif !bulk && @bulk_save_cache.length > 0
170
+ bulk_save
171
+ end
172
+ result = if doc['_id']
173
+ slug = escape_docid(doc['_id'])
174
+ begin
175
+ uri = "#{@root}/#{slug}"
176
+ uri << "?batch=ok" if batch
177
+ CouchRest.put uri, doc
178
+ rescue RestClient::ResourceNotFound
179
+ p "resource not found when saving even tho an id was passed"
180
+ slug = doc['_id'] = @server.next_uuid
181
+ CouchRest.put "#{@root}/#{slug}", doc
182
+ end
183
+ else
184
+ begin
185
+ slug = doc['_id'] = @server.next_uuid
186
+ CouchRest.put "#{@root}/#{slug}", doc
187
+ rescue #old version of couchdb
188
+ CouchRest.post @root, doc
189
+ end
190
+ end
191
+ if result['ok']
192
+ doc['_id'] = result['id']
193
+ doc['_rev'] = result['rev']
194
+ doc.database = self if doc.respond_to?(:database=)
195
+ end
196
+ result
197
+ end
198
+
199
+ # Save a document to CouchDB in bulk mode. See #save_doc's +bulk+ argument.
200
+ def bulk_save_doc(doc)
201
+ save_doc(doc, true)
202
+ end
203
+
204
+ # Save a document to CouchDB in batch mode. See #save_doc's +batch+ argument.
205
+ def batch_save_doc(doc)
206
+ save_doc(doc, false, true)
207
+ end
208
+
209
+ # POST an array of documents to CouchDB. If any of the documents are
210
+ # missing ids, supply one from the uuid cache.
211
+ #
212
+ # If called with no arguments, bulk saves the cache of documents to be bulk saved.
213
+ def bulk_save(docs = nil, use_uuids = true)
214
+ if docs.nil?
215
+ docs = @bulk_save_cache
216
+ @bulk_save_cache = []
217
+ end
218
+ if (use_uuids)
219
+ ids, noids = docs.partition{|d|d['_id']}
220
+ uuid_count = [noids.length, @server.uuid_batch_count].max
221
+ noids.each do |doc|
222
+ nextid = @server.next_uuid(uuid_count) rescue nil
223
+ doc['_id'] = nextid if nextid
224
+ end
225
+ end
226
+ CouchRest.post "#{@root}/_bulk_docs", {:docs => docs}
227
+ end
228
+ alias :bulk_delete :bulk_save
229
+
230
+ # DELETE the document from CouchDB that has the given <tt>_id</tt> and
231
+ # <tt>_rev</tt>.
232
+ #
233
+ # If <tt>bulk</tt> is true (false by default) the deletion is recorded for bulk-saving (bulk-deletion :) later.
234
+ # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
235
+ def delete_doc(doc, bulk = false)
236
+ raise ArgumentError, "_id and _rev required for deleting" unless doc['_id'] && doc['_rev']
237
+ if bulk
238
+ @bulk_save_cache << { '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true }
239
+ return bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
240
+ return { "ok" => true } # Mimic the non-deferred version
241
+ end
242
+ slug = escape_docid(doc['_id'])
243
+ CouchRest.delete "#{@root}/#{slug}?rev=#{doc['_rev']}"
244
+ end
245
+
246
+ # COPY an existing document to a new id. If the destination id currently exists, a rev must be provided.
247
+ # <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
248
+ # hash with a '_rev' key
249
+ def copy_doc(doc, dest)
250
+ raise ArgumentError, "_id is required for copying" unless doc['_id']
251
+ slug = escape_docid(doc['_id'])
252
+ destination = if dest.respond_to?(:has_key?) && dest['_id'] && dest['_rev']
253
+ "#{dest['_id']}?rev=#{dest['_rev']}"
254
+ else
255
+ dest
256
+ end
257
+ CouchRest.copy "#{@root}/#{slug}", destination
258
+ end
259
+
260
+ # Updates the given doc by yielding the current state of the doc
261
+ # and trying to update update_limit times. Returns the new doc
262
+ # if the doc was successfully updated without hitting the limit
263
+ def update_doc(doc_id, params = {}, update_limit=10)
264
+ resp = {'ok' => false}
265
+ new_doc = nil
266
+ last_fail = nil
267
+
268
+ until resp['ok'] or update_limit <= 0
269
+ doc = self.get(doc_id, params) # grab the doc
270
+ new_doc = yield doc # give it to the caller to be updated
271
+ begin
272
+ resp = self.save_doc new_doc # try to PUT the updated doc into the db
273
+ rescue RestClient::RequestFailed => e
274
+ if e.http_code == 409 # Update collision
275
+ update_limit -= 1
276
+ last_fail = e
277
+ else # some other error
278
+ raise e
279
+ end
280
+ end
281
+ end
282
+
283
+ raise last_fail unless resp['ok']
284
+ new_doc
285
+ end
286
+
287
+ # Compact the database, removing old document revisions and optimizing space use.
288
+ def compact!
289
+ CouchRest.post "#{@root}/_compact"
290
+ end
291
+
292
+ # Create the database
293
+ def create!
294
+ bool = server.create_db(@name) rescue false
295
+ bool && true
296
+ end
297
+
298
+ # Delete and re create the database
299
+ def recreate!
300
+ delete!
301
+ create!
302
+ rescue RestClient::ResourceNotFound
303
+ ensure
304
+ create!
305
+ end
306
+
307
+ # Replicates via "pulling" from another database to this database. Makes no attempt to deal with conflicts.
308
+ def replicate_from(other_db, continuous = false, create_target = false)
309
+ replicate(other_db, continuous, :target => name, :create_target => create_target)
310
+ end
311
+
312
+ # Replicates via "pushing" to another database. Makes no attempt to deal with conflicts.
313
+ def replicate_to(other_db, continuous = false, create_target = false)
314
+ replicate(other_db, continuous, :source => name, :create_target => create_target)
315
+ end
316
+
317
+ # DELETE the database itself. This is not undoable and could be rather
318
+ # catastrophic. Use with care!
319
+ def delete!
320
+ CouchRest.delete @root
321
+ end
322
+
323
+ private
324
+
325
+ def replicate(other_db, continuous, options)
326
+ raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchRest::Database)
327
+ raise ArgumentError, "must provide a target or source option" unless (options.key?(:target) || options.key?(:source))
328
+ payload = options
329
+ if options.has_key?(:target)
330
+ payload[:source] = other_db.root
331
+ else
332
+ payload[:target] = other_db.root
333
+ end
334
+ payload[:continuous] = continuous
335
+ CouchRest.post "#{@host}/_replicate", payload
336
+ end
337
+
338
+ def uri_for_attachment(doc, name)
339
+ if doc.is_a?(String)
340
+ puts "CouchRest::Database#fetch_attachment will eventually require a doc as the first argument, not a doc.id"
341
+ docid = doc
342
+ rev = nil
343
+ else
344
+ docid = doc['_id']
345
+ rev = doc['_rev']
346
+ end
347
+ docid = escape_docid(docid)
348
+ name = CGI.escape(name)
349
+ rev = "?rev=#{doc['_rev']}" if rev
350
+ "/#{docid}/#{name}#{rev}"
351
+ end
352
+
353
+ def url_for_attachment(doc, name)
354
+ @root + uri_for_attachment(doc, name)
355
+ end
356
+
357
+ def escape_docid id
358
+ /^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
359
+ end
360
+
361
+ def encode_attachments(attachments)
362
+ attachments.each do |k,v|
363
+ next if v['stub']
364
+ v['data'] = base64(v['data'])
365
+ end
366
+ attachments
367
+ end
368
+
369
+ def base64(data)
370
+ [data].pack('m')
371
+ end
372
+ end
373
+ end