couch_tomato 0.1.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,88 +1,108 @@
1
- Couch Tomato
2
- ============
1
+ #Couch Tomato
2
+ *A Ruby persistence layer for CouchDB, inspired by and forked from Couch Potato*
3
3
 
4
- ## TODO
4
+ ##Quick Start
5
+ ###Installing Couch Tomato
6
+ Couch Tomato is hosted on [gemcutter.org](http://gemcutter.org), and can be installed as follows:
5
7
 
6
- ### Documentation
8
+ sudo gem install couch_tomato --source http://gemcutter.org
7
9
 
8
- - Quick start
9
- - Data migration mechanism for managing (implicit) schema and data changes between team members and deployed systems, complete with generator and Rake tasks
10
- - Rake tasks that facilitate replication of local and remote CouchDB databases
11
- - Removed model property dirty tracking (added significant complexity and we haven't needed it...yet?)
12
- - Shoulda instead of RSpec
13
- - RR for mocks/stubs
10
+ ###Post Installation Requirements
11
+ `root` in a path refers to `Rails.root` if you are using Rails, and the root level of any Ruby project if you are not using Rails. With the couch\_tomato gem installed, enable the Thor tasks by creating a file `couch_tomato.thor` as shown below:
14
12
 
15
- ### Code
13
+ ####couch\_tomato.thor
16
14
 
17
- - default "server" to `http://localhost:5984`
15
+ couch_tomato_gem = Gem.searcher.find('couch_tomato')
16
+ Dir["#{couch_tomato_gem.full_gem_path}/lib/tasks/*.thor"].each { |ext| load ext } if couch_tomato_gem
18
17
 
18
+ `couch_tomato.thor` can be saved to `Rails.root/lib/tasks` for Rails projects or to the root level of a regular Ruby app. Thor tasks associated with Couch Tomato are available under the `ct` namespace. To setup the Couch Tomato folder structure and config file in a Rails project, run the following:
19
19
 
20
- ## Potato/Tomato
20
+ thor ct:init
21
+
22
+ The above will create a folder `couchdb` in `root`, along with `root/couchdb/migrate` and `root/couchdb/views`. The init task will also generate a sample Couch Tomato config file `couch_tomato.yml.example` in `root/config` as given below:
21
23
 
22
- We're huge fans of Couch Potato. We love it's advocacy for using Couch naturally (not trying to make it look like a SQL database). Originally a Couch Potato fork, Couch Tomato supports our own production needs.
24
+ ####couch\_tomato.yml.example
23
25
 
24
- ### Multi-Database Support
26
+ defaults: &defaults
27
+ couchdb_address: 127.0.0.1
28
+ couchdb_port: 5984
29
+ couchdb_basename: your_project_name
30
+
31
+ development:
32
+ <<: *defaults
33
+
34
+ test:
35
+ <<: *defaults
36
+
37
+ production:
38
+ <<: *defaults
39
+
40
+ Modify `couchdb_address`, `couchdb_port`, and `couchdb_basename` to correspond to the ip/address, port number, and name of your project respectively. You can optionally choose to suffix all your databases names by adding a `couchdb_suffix` field. Rename/copy `couch_tomato.yml.example` to `couch_tomato.yml`.
41
+
42
+ Finally, you will need to populate the values in CouchTomato::Config. Put
25
43
 
44
+ CouchTomato::Config.set_config_yml path
45
+
46
+ somewhere in your app (i.e. in an initializer for Rails). This will load `couch_tomato.yml` into CouchTomato::Config. If path is not specified, Couch Tomato will look for the default `root/config/couch_tomato.yml`. If you chose to not create a `couch_tomato.yml`, you can populate the fields of `CouchTomato::Config` manually. Couch Tomato is now ready to be used.
47
+
48
+ ##Using Couch Tomato
49
+ ### Multi-Database Support
26
50
  CouchDB makes it dead-simple to manage multiple databases. For large data-sets, it's very important to separate unrelated documents into separate databases. Couch Tomato assumes (but doesn't force) the use of multiple databases.
27
51
 
28
52
  class UserDb < CouchTomato::Database
29
53
  name "users"
30
- server "http://#{APP_CONFIG["couchdb_address"]}:#{APP_CONFIG["couchdb_port"]}"
54
+ ...
31
55
  end
32
56
 
33
57
  class StatDb < CouchTomato::Database
34
- name "stats"
35
- server "http://#{APP_CONFIG["couchdb_address"]}:#{APP_CONFIG["couchdb_port"]}"
58
+ ...
36
59
  end
37
60
 
38
61
  UserDb.save_doc(User.new({:name => 'Joe'}))
39
62
  5_000.times { StatDb.save_doc(Stat.new({:metric => 10_000 * rand})) }
40
63
 
41
- ### Each view determines the model for its values
64
+ A name can be specified for a specific database as shown in UserDb, otherwise, the class name is used.
42
65
 
66
+ ###Each view determines the model for its values
43
67
  Views return arbitrary hashes. Often a view's value is an entire document (or more correctly, utilize `emit(key, null)` combined with `:include_docs => true`). But, a view's value is also often completely independent of the structure of the underlying documents.
44
68
 
45
69
  Define views on the database rather than inside a model (this is arguably more Couch-like). Each views declaration stipulates whether their results should be 'raw' hashes or a particular model type.
46
70
 
47
71
  class UserDb < CouchTomato::Database
48
72
  name "users"
49
- server "http://#{APP_CONFIG["couchdb_address"]}:#{APP_CONFIG["couchdb_port"]}"
50
73
 
51
74
  view :by_created_at, User
52
75
  view :count # raw
53
76
  end
54
77
 
55
- ### Store view definitions on the file system
56
-
78
+ ###Store view definitions on the file system
57
79
  Rather than having Ruby generate JavaScript or writing JavaScript in our Ruby code as a string, define views in files on the file system:
58
80
 
59
- RAILS_ROOT/couchdb/views/users/*-map.js
60
- RAILS_ROOT/couchdb/views/users/*-reduce.js
81
+ root/couchdb/views/users/*-map.js
82
+ root/couchdb/views/users/*-reduce.js
61
83
 
62
84
  The reduce is optional. If you want to define views in a specific design document (called 'lazy'), you can do so:
63
85
 
64
- RAILS_ROOT/couchdb/views/users/lazy/*.js
86
+ root/couchdb/views/users/lazy/*.js
65
87
 
66
88
  There's a handy generator:
67
89
 
68
- script/generate view users by_created_at
69
- script/generate view users/lazy by_birthday
70
- script/generate view users by_created_on map reduce
71
-
72
- Rake tasks apply the views on the file system to Couch, skipping views that aren't dirty:
90
+ script/generate couch_view users by_created_at
91
+ script/generate couch_view users/lazy by_birthday
92
+ script/generate couch_view users by_created_on map reduce
73
93
 
74
- rake couch_tomato:push
94
+ Thor tasks apply the views on the file system to CouchDb, skipping views that aren't dirty:
75
95
 
76
- You can also view the differences between the views in Couch and those on the file system:
96
+ thor ct:push
77
97
 
78
- rake couch_tomato:diff
98
+ You can also view the differences between the views in CouchDb and those on the file system:
79
99
 
80
- ### Remove dynamically generated views
100
+ thor ct:diff
81
101
 
102
+ ###Remove dynamically generated views
82
103
  We almost always need to write JavaScript to get the view behavior we need, and, for both conceptual and implementation complexity reasons, we value having all the views contained in one place--the file system. This also simplifies deployment and collaboration workflows.
83
104
 
84
- ### Multiple design documents per database
85
-
105
+ ###Multiple design documents per database
86
106
  CouchDB supports multiple design documents per database. There's an important semantic consideration: all views in a design document are updated if any one view needs to be updated. To improve the read performance of couch views under high-volume reads and writes, you could organize views that don't need to be as timely into a separate design document named 'lazy', and always include the `stale=true` couch option in queries to views defined in the 'lazy' design document. You could then have a script that ran periodically to trigger the 'lazy' views to update.
87
107
 
88
108
  class UserDb > CouchTomato::Database
@@ -93,4 +113,113 @@ CouchDB supports multiple design documents per database. There's an important se
93
113
  view 'lazy/count_created_by_date'
94
114
  end
95
115
 
96
- We have not had a use for this, nor have we demonstrated that the claimed performance benefit actually exists (it originated from the CouchDB docs, wiki or list or some-such). But, it is instance where this approach to representing views maps fairly directly to CouchDB functionality.
116
+ ###Migrations
117
+ Couch Tomato migrations are similar to ActiveRecord migrations, however, Couch Tomato migrations modify existing fields of documents instead of a "schema". There is a handy generator available for migrators as well.
118
+
119
+ script/generate couch_migration users by_created_at
120
+
121
+ Migration come with two methods, up and down, each with a document hash. Up/down method will be run on every document in a database, with changes to the document hash committed to a database if the method does not return false. A migration can be accessed by thor ct:migrate and the -v (version) option. The version is simply the prefixed number in front of the generated view file.
122
+
123
+ ##Thor Tasks
124
+ All Thor tasks associated with Couch Tomato are available under the namespace "ct". The `-e` option specifies an environment (i.e. for Rails)
125
+
126
+ ###ct:init
127
+ The init task creates the folder structure required for managing views and migrations and a sample `couch_tomato.yml`.
128
+
129
+ Example:
130
+
131
+ thor ct:init
132
+
133
+ ###ct:push
134
+ The push tasks syncs CouchDB with the view structure present on the file system.
135
+
136
+ Example:
137
+
138
+ thor ct:push -e development
139
+
140
+ ###ct:diff
141
+ The diff tasks `git status` type diff between the filesystem view structure and the current structure in CouchDB. The diff is with respect to the file system, that is, the file system is always assumed to be the most up to date.
142
+
143
+ Example:
144
+
145
+ thor ct:diff -e development
146
+
147
+ ###ct:drop
148
+ The drop task will remove a specified database within the given environment from CouchDB. The -r option can be specified to remove via regex, and no arguments can be supplied to remove all databases.
149
+
150
+ Examples:
151
+
152
+ # Remove all databases (you will be prompted first)
153
+ thor ct:drop -e development
154
+
155
+ # Remove all databases ending "\_bak"
156
+ thor ct:drop -e development -r .*\_bak
157
+
158
+ ###ct:migrate
159
+ The migrate tasks runs migrations from your `couchdb/migrate` folder.
160
+
161
+ Examples:
162
+
163
+ # Apply all migrations
164
+ thor ct:migrate -e development
165
+
166
+ # Undo migration "20090911201227"
167
+ thor ct:migrate -e development --down -v 20090911201227
168
+
169
+ # Redo the last 5 migrations
170
+ thor ct:migrate -e development --redo -s 5
171
+
172
+ # Reset all databases using all available migrations to the "development" environment
173
+ thor ct:migrate -e development --reset
174
+
175
+ ###ct:rollback
176
+ The rollback tasks will revert to a previous migration from the current version. Specify the number of steps with the -s option.
177
+
178
+ Examples:
179
+
180
+ # Undo the previous migration
181
+ thor ct:rollback -e development
182
+
183
+ # Undo the last 5 migrations
184
+ thor ct:rollback -e development -s 5
185
+
186
+ ###ct:forward
187
+ The forward task will roll forward to the next version. Specify the number of steps with the -s option.
188
+
189
+ Example:
190
+
191
+ # Roll forward to the next migration
192
+ thor ct:forward -e development
193
+
194
+ ###ct:replicate
195
+ The replicate task facilitates the duplication of databases across application environments. The source and target server are always required for replication. Replicate operates in three different functions:
196
+
197
+ 1. If a source and destination database are provided, then the source database from the source server will be copied onto the destination database on the target server. Note that the destination database needs to already have been created.
198
+ 2. If not 1., but the the source and target servers are the same, then all databases on the common server are duplicated; the duplicate databases are postfixed with a "\_bak".
199
+ 3. If neither 1. or 2., then the assumption is that the user wants to clone all databases from the remote source server onto the specified target server.
200
+
201
+ Examples:
202
+
203
+ # Copy database "example" from source server "11.11.11.11" to "example_1" in localhost
204
+ thor ct:replicate -e development -s 11.11.11.11 -t localhost -c example -v example_1
205
+
206
+ # Back up all databases in localhost
207
+ thor ct:replicate -e development -s localhost -t localhost
208
+
209
+ # Duplicate databases from "11.11.11.11" to localhost
210
+ thor ct:replicate -e development -s 11.11.11.11 -t localhost
211
+
212
+ ###ct:touch
213
+ The touch task will initiate the building of views for a given database. Touch will query the first view of the each design doc in a db which will cause all remaining views to be built as well.
214
+
215
+ Examples:
216
+
217
+ # Build all design documents in databases "example" and "test"
218
+ thor ct:touch -e development -d example test
219
+
220
+ # Build all design documents in "example" and specify a 24 hours timeout
221
+ thor ct:touch -e development -d example -t 86400
222
+
223
+ # Build all design documents in "example" asynchronously
224
+ thor ct:touch -e development -d example --async
225
+
data/README.md_old ADDED
@@ -0,0 +1,96 @@
1
+ Couch Tomato
2
+ ============
3
+
4
+ ## TODO
5
+
6
+ ### Documentation
7
+
8
+ - Quick start
9
+ - Data migration mechanism for managing (implicit) schema and data changes between team members and deployed systems, complete with generator and Rake tasks
10
+ - Rake tasks that facilitate replication of local and remote CouchDB databases
11
+ - Removed model property dirty tracking (added significant complexity and we haven't needed it...yet?)
12
+ - Shoulda instead of RSpec
13
+ - RR for mocks/stubs
14
+
15
+ ### Code
16
+
17
+ - default "server" to `http://localhost:5984`
18
+
19
+
20
+ ## Potato/Tomato
21
+
22
+ We're huge fans of Couch Potato. We love it's advocacy for using Couch naturally (not trying to make it look like a SQL database). Originally a Couch Potato fork, Couch Tomato supports our own production needs.
23
+
24
+ ### Multi-Database Support
25
+
26
+ CouchDB makes it dead-simple to manage multiple databases. For large data-sets, it's very important to separate unrelated documents into separate databases. Couch Tomato assumes (but doesn't force) the use of multiple databases.
27
+
28
+ class UserDb < CouchTomato::Database
29
+ name "users"
30
+ server "http://#{APP_CONFIG["couchdb_address"]}:#{APP_CONFIG["couchdb_port"]}"
31
+ end
32
+
33
+ class StatDb < CouchTomato::Database
34
+ name "stats"
35
+ server "http://#{APP_CONFIG["couchdb_address"]}:#{APP_CONFIG["couchdb_port"]}"
36
+ end
37
+
38
+ UserDb.save_doc(User.new({:name => 'Joe'}))
39
+ 5_000.times { StatDb.save_doc(Stat.new({:metric => 10_000 * rand})) }
40
+
41
+ ### Each view determines the model for its values
42
+
43
+ Views return arbitrary hashes. Often a view's value is an entire document (or more correctly, utilize `emit(key, null)` combined with `:include_docs => true`). But, a view's value is also often completely independent of the structure of the underlying documents.
44
+
45
+ Define views on the database rather than inside a model (this is arguably more Couch-like). Each views declaration stipulates whether their results should be 'raw' hashes or a particular model type.
46
+
47
+ class UserDb < CouchTomato::Database
48
+ name "users"
49
+ server "http://#{APP_CONFIG["couchdb_address"]}:#{APP_CONFIG["couchdb_port"]}"
50
+
51
+ view :by_created_at, User
52
+ view :count # raw
53
+ end
54
+
55
+ ### Store view definitions on the file system
56
+
57
+ Rather than having Ruby generate JavaScript or writing JavaScript in our Ruby code as a string, define views in files on the file system:
58
+
59
+ RAILS_ROOT/couchdb/views/users/*-map.js
60
+ RAILS_ROOT/couchdb/views/users/*-reduce.js
61
+
62
+ The reduce is optional. If you want to define views in a specific design document (called 'lazy'), you can do so:
63
+
64
+ RAILS_ROOT/couchdb/views/users/lazy/*.js
65
+
66
+ There's a handy generator:
67
+
68
+ script/generate view users by_created_at
69
+ script/generate view users/lazy by_birthday
70
+ script/generate view users by_created_on map reduce
71
+
72
+ Rake tasks apply the views on the file system to Couch, skipping views that aren't dirty:
73
+
74
+ rake couch_tomato:push
75
+
76
+ You can also view the differences between the views in Couch and those on the file system:
77
+
78
+ rake couch_tomato:diff
79
+
80
+ ### Remove dynamically generated views
81
+
82
+ We almost always need to write JavaScript to get the view behavior we need, and, for both conceptual and implementation complexity reasons, we value having all the views contained in one place--the file system. This also simplifies deployment and collaboration workflows.
83
+
84
+ ### Multiple design documents per database
85
+
86
+ CouchDB supports multiple design documents per database. There's an important semantic consideration: all views in a design document are updated if any one view needs to be updated. To improve the read performance of couch views under high-volume reads and writes, you could organize views that don't need to be as timely into a separate design document named 'lazy', and always include the `stale=true` couch option in queries to views defined in the 'lazy' design document. You could then have a script that ran periodically to trigger the 'lazy' views to update.
87
+
88
+ class UserDb > CouchTomato::Database
89
+ name :users
90
+
91
+ view :by_created_at, User
92
+ view :count # raw
93
+ view 'lazy/count_created_by_date'
94
+ end
95
+
96
+ We have not had a use for this, nor have we demonstrated that the claimed performance benefit actually exists (it originated from the CouchDB docs, wiki or list or some-such). But, it is instance where this approach to representing views maps fairly directly to CouchDB functionality.
data/lib/couch_tomato.rb CHANGED
@@ -2,6 +2,7 @@ require 'couchrest'
2
2
  require 'json'
3
3
  require 'json/add/core'
4
4
  require 'json/add/rails'
5
+ require 'active_support'
5
6
 
6
7
  # require 'ostruct'
7
8
 
@@ -42,5 +43,7 @@ require File.dirname(__FILE__) + '/core_ext/symbol'
42
43
  require File.dirname(__FILE__) + '/core_ext/extract_options'
43
44
  require File.dirname(__FILE__) + '/core_ext/duplicable'
44
45
  require File.dirname(__FILE__) + '/core_ext/inheritable_attributes'
46
+ require File.dirname(__FILE__) + '/couch_tomato/config'
45
47
  require File.dirname(__FILE__) + '/couch_tomato/persistence'
46
48
  require File.dirname(__FILE__) + '/couch_tomato/js_view_source'
49
+
@@ -0,0 +1,11 @@
1
+ module CouchTomato
2
+ class Config
3
+ cattr_accessor :url
4
+ cattr_accessor :prefix
5
+ cattr_accessor :suffix
6
+
7
+ @@url = "http://127.0.0.1:5984"
8
+ @@prefix = ""
9
+ @@suffix = defined?(Rails) ? Rails.env : ""
10
+ end
11
+ end
@@ -1,73 +1,48 @@
1
- require 'couchrest'
2
- require 'pp'
1
+ require "couchrest"
3
2
 
4
3
  module CouchTomato
5
4
  class Database
6
-
7
5
  class ValidationsFailedError < ::StandardError; end
8
6
 
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
7
+ class << self
8
+ attr_accessor :url
9
+ attr_accessor :prefix
10
+ attr_accessor :suffix
11
+ attr_accessor :name
12
+ attr_accessor :views
15
13
 
16
- self.views = {}
17
- self.prefix_string = ''
14
+ attr_accessor :couchrest_db
15
+ end
16
+
17
+ def self.inherited(c)
18
+ c.url = CouchTomato::Config.url
19
+ c.prefix = CouchTomato::Config.prefix
20
+ c.suffix = CouchTomato::Config.suffix
21
+ c.name = c.to_s.underscore
22
+ c.views = {}
23
+ end
18
24
 
19
25
  def self.database
20
26
  return self.couchrest_db if self.couchrest_db
21
-
22
- self.prefix_string ||= ''
23
27
 
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)
28
+ path = "#{self.url.gsub(/\/\s*$/, "")}/#{([self.prefix.to_s, self.name, self.suffix.to_s] - [""]).join("_")}"
29
+
30
+ self.couchrest_db = CouchRest.database(path)
27
31
 
28
- self.couchrest_db = CouchRest.database("#{tmp_server}#{tmp_prefix}#{self.database_name}#{tmp_suffix}")
29
32
  begin
30
33
  self.couchrest_db.info
31
34
  rescue RestClient::ResourceNotFound
32
- raise "Database '#{tmp_prefix}#{self.database_name}#{tmp_suffix}' does not exist."
35
+ raise "Database '#{path}' does not exist."
33
36
  end
34
-
35
- self.couchrest_db
36
- end
37
37
 
38
- def self.prefix (name)
39
- self.prefix_string = name || ''
38
+ return self.couchrest_db
40
39
  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
40
 
67
41
  def self.view(name, options={})
68
- raise 'A View nemonic must be specified' if name.nil?
42
+ raise "A View nemonic must be specified" if name.nil?
43
+
69
44
  self.views[name] = {}
70
- self.views[name][:design_doc] = !options[:design_doc] ? self.database_name.to_sym : options.delete(:design_doc).to_sym
45
+ self.views[name][:design_doc] = !options[:design_doc] ? self.name.to_sym : options.delete(:design_doc).to_sym
71
46
  self.views[name][:view_name] = options.delete(:view_name) || name.to_s
72
47
  self.views[name][:model] = options.delete(:model)
73
48
  self.views[name][:couch_options] = options
@@ -149,11 +124,9 @@ module CouchTomato
149
124
  end
150
125
 
151
126
  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
127
+ puts "Database server: #{self.url || "nil"}"
128
+ puts "Database name: #{self.name || "nil"}"
129
+ puts "Views: #{self.views.inspect}"
157
130
  end
158
131
 
159
132
  def self.query_view!(name, options={})
@@ -1,5 +1,7 @@
1
1
  require 'digest/sha1'
2
+ require 'patron'
2
3
 
4
+ STDOUT.sync = true
3
5
  module CouchTomato
4
6
  class JsViewSource
5
7
  # todo: provide a 'dirty?' method that can be called in an initializer and warn the developer that view are out of sync
@@ -22,11 +24,11 @@ module CouchTomato
22
24
 
23
25
  if fs_doc['views'].empty?
24
26
  next unless fs_doc['_rev']
25
- puts "DELETE #{fs_doc['_id']}" unless silent
27
+ puts "DELETE #{database_name}/#{fs_doc['_id']}" unless silent
26
28
  db.delete_doc(fs_doc)
27
29
  else
28
30
  if changed_views?(fs_doc, db_doc)
29
- puts "UPDATE #{fs_doc['_id']}" unless silent
31
+ puts "UPDATE #{database_name}/#{fs_doc['_id']}" unless silent
30
32
  db.save_doc(fs_doc)
31
33
  end
32
34
  end
@@ -48,22 +50,36 @@ module CouchTomato
48
50
  end
49
51
 
50
52
  def self.diff
53
+ status_dict = {
54
+ :NEW_DOC => "new design:", :NEW_VIEW => "new view:",
55
+ :MOD_VIEW => "modified:", :DEL_DOC => "deleted:",
56
+ :DEL_VIEW => "deleted:", :NEW_DB => "new db:"
57
+ }
58
+ types = ["map", "reduce"]
59
+ puts "# Changes with respect to the filesystem:"
60
+
51
61
  fs_database_names.each do |database_name|
52
- db = database!(database_name)
53
-
62
+ db = database(database_name)
63
+ puts "#\t#{setw(status_dict[:NEW_DB], 14)}#{database_name}" unless is_db?(db)
64
+
54
65
  fs_docs = fs_design_docs(database_name)
55
- db_docs = db_design_docs(db)
66
+ db_docs = is_db?(db) ? db_design_docs(db) : {}
56
67
 
68
+ diff = []
57
69
  # design docs on fs but not in db
58
70
  (fs_docs.keys - db_docs.keys).each do |design_name|
59
71
  unless fs_docs[design_name]['views'].empty?
60
- puts " NEW: #{database_name}/_#{design_name}: #{fs_docs[design_name]['views'].keys.join(', ')}"
72
+ diff.push [:NEW_DOC, "#{database_name}/_#{design_name}"]
73
+ fs_docs[design_name]['views'].keys.each do |view|
74
+ (fs_docs[design_name]['views'][view].keys & types).each {|type|
75
+ diff.push [:NEW_VIEW, "#{database_name}/_#{design_name}/#{view}-#{type}"]}
76
+ end
61
77
  end
62
78
  end
63
79
 
64
80
  # design docs in db but not on fs
65
81
  (db_docs.keys - fs_docs.keys).each do |design_name|
66
- puts "REMOVED: #{database_name}/_#{design_name}"
82
+ diff.push [:DEL_DOC, "#{database_name}/_#{design_name}"] unless (design_name.to_s.include? "migrations")
67
83
  end
68
84
 
69
85
  # design docs in both db and fs
@@ -74,16 +90,16 @@ module CouchTomato
74
90
 
75
91
  unless fs_only_view_keys.empty?
76
92
  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
93
+ %w(map reduce).map {|method| fs_docs[design_name]['views'][key][method].nil? ? nil : "#{key}-#{method}"}.compact
78
94
  end.flatten
79
- puts " ADDED: #{database_name}/_#{design_name}: #{methods.join(', ')}"
95
+ methods.each {|method| diff.push [:NEW_VIEW, "#{database_name}/_#{design_name}/#{method}"] }
80
96
  end
81
97
 
82
98
  unless db_only_view_keys.empty?
83
99
  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
100
+ %w(map reduce).map {|method| db_docs[design_name]['views'][key][method] ? "#{key}-#{method}" : nil}.compact
85
101
  end
86
- puts "REMOVED: #{database_name}/_#{design_name}: #{methods.join(', ')}"
102
+ methods.each {|method| diff.push [:DEL_VIEW, "#{database_name}/_#{design_name}/#{method}"] }
87
103
  end
88
104
 
89
105
  common_view_keys.each do |common_key|
@@ -95,28 +111,36 @@ module CouchTomato
95
111
  # has either the map or reduce been added or removed
96
112
  %w(map reduce).each do |method|
97
113
  if db_view[method] && !fs_view[method]
98
- puts "REMOVED: #{database_name}/_#{design_name}:#{method}()" and next
114
+ diff.push [:DEL_VIEW, "#{database_name}/_#{design_name}/#{common_key}-#{method}"] and next
99
115
  end
100
116
 
101
117
  if fs_view[method] && !db_view[method]
102
- puts " ADDED: #{database_name}/_#{design_name}:#{method}()" and next
118
+ diff.push [:NEW_VIEW, "#{database_name}/_#{design_name}/#{common_key}-#{method}"] and next
103
119
  end
104
120
 
105
121
  if fs_view["sha1-#{method}"] != db_view["sha1-#{method}"]
106
- puts "OUTDATED: #{database_name}/_#{design_name}:#{method}()" and next
122
+ diff.push [:MOD_VIEW, "#{database_name}/_#{design_name}/#{common_key}-#{method}"] and next
107
123
  end
108
124
  end
109
125
  end
110
-
111
126
  end
112
-
127
+
128
+ diff.uniq!
129
+ diff.each do |status|
130
+ puts "#\t#{setw(status_dict[status.first], 14)}#{status.last}"
131
+ end
113
132
  end
114
133
  end
115
134
 
116
135
  private
136
+
137
+ def self.setw(str, w)
138
+ spaces = w - str.length
139
+ (spaces > 0) ? str + (" " * spaces) : str
140
+ end
117
141
 
118
142
  def self.path(db_name="")
119
- "#{Rails.root}/couchdb/views/#{db_name}" if Rails
143
+ "#{Rails.root rescue nil || "."}/couchdb/views/#{db_name}"
120
144
  end
121
145
 
122
146
  def self.fs_database_names
@@ -136,14 +160,17 @@ module CouchTomato
136
160
  # :clicks => {'by_date' => {'map' => ..., 'reduce' => ..., sha1-map => ..., sha1-reduce => ...} }
137
161
  def self.fs_design_docs(db_name)
138
162
  design_docs = {}
139
-
140
- path = "#{RAILS_ROOT}/couchdb/views/#{db_name}"
141
- Dir[path + "/**"].each do |file|
142
- throw "Invalid filename '#{File.basename(file)}': expecting '-map.js' or '-reduce.js' suffix" unless file.match(/-((map)|(reduce))\.js$/)
143
-
144
- design_name = db_name.to_sym
145
- design_docs[design_name] ||= {'_id' => "_design/#{db_name}", 'views' => {}}
146
- fs_view(design_docs[design_name], file)
163
+
164
+ path = "#{Rails.root rescue nil || "."}/couchdb/views/#{db_name}"
165
+ doc_folders = Dir["#{path}/**/"].map {|ddoc| ddoc.chop! }
166
+ doc_folders.each do |design_doc|
167
+ (Dir["#{design_doc}/**"] - doc_folders).each do |file|
168
+ throw "Invalid filename '#{File.basename(file)}': expecting '-map.js' or '-reduce.js' suffix" unless file.match(/-((map)|(reduce))\.js$/)
169
+
170
+ design_name = File.basename(design_doc)
171
+ design_docs[design_name.to_sym] ||= {'_id' => "_design/#{design_name}", 'views' => {}}
172
+ fs_view(design_docs[design_name.to_sym], file)
173
+ end
147
174
  end
148
175
 
149
176
  design_docs.each do |db, design|
@@ -172,12 +199,58 @@ module CouchTomato
172
199
  design_doc['views'][name]["sha1-#{type}"] = sha1
173
200
  design_doc
174
201
  end
202
+
203
+ def self.touch(dbs, async=false, timeout=nil)
204
+ s = Patron::Session.new
205
+ s.timeout = timeout.nil? ? 86400 : timeout.to_i
206
+ s.timeout = 1 if async
207
+
208
+ dbs.each do |db_str|
209
+ db = database(db_str)
210
+ design_docs = db_design_docs(db)
211
+
212
+ design_docs.each do |ddoc_sym, ddoc|
213
+ puts ddoc_sym.to_s
214
+ next if ddoc_sym.to_s == "migrations"
215
+ doc_id = ddoc["_id"]
216
+ view = ddoc["views"].keys.first
217
+
218
+ print "Building #{db_str}/#{doc_id}... "
219
+ begin
220
+ s.get("#{db_url(db_str)}/#{doc_id}/_view/#{view}?limit=0")
221
+ puts "finished!"
222
+ rescue Patron::TimeoutError
223
+ if async
224
+ puts "task started asynchronously."
225
+ else
226
+ puts "the view could not be built within the specified timeout (#{s.timeout} seconds). The view is still being built in the background."
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
175
232
 
176
- # todo: don't depend on "proprietary" APP_CONFIG
177
- def self.database!(database_name)
178
- CouchRest.database!("http://" + APP_CONFIG["couchdb_address"] + ":" + APP_CONFIG["couchdb_port"].to_s \
179
- + "/" + APP_CONFIG["couchdb_basename"] + "_" + database_name + "_" + RAILS_ENV)
233
+ def self.db_url(name)
234
+ "#{CouchTomato::Config.url.gsub(/\/\s*$/, "")}/#{([CouchTomato::Config.prefix.to_s, name, CouchTomato::Config.suffix.to_s] - [""]).join("_")}"
180
235
  end
181
236
 
237
+ def self.database(database_name, force=false)
238
+ raise "Database names (#{database_name}) cannot contain uppercase letters." unless database_name == database_name.downcase
239
+ url = db_url(database_name)
240
+ force ? CouchRest.database!(url) : CouchRest.database(url)
241
+ end
242
+
243
+ def self.database!(database_name)
244
+ database(database_name, true);
245
+ end
246
+
247
+ def self.is_db?(db)
248
+ begin
249
+ db.info
250
+ rescue
251
+ return false
252
+ end
253
+ return true
254
+ end
182
255
  end
183
256
  end
@@ -0,0 +1,241 @@
1
+ Ct_Yml_Example = %(defaults: &defaults
2
+ couchdb_address: 127.0.0.1
3
+ couchdb_port: 5984
4
+ couchdb_basename: your_project_name
5
+
6
+ development:
7
+ <<: *defaults
8
+
9
+ test:
10
+ <<: *defaults
11
+
12
+ production:
13
+ <<: *defaults
14
+ )
15
+
16
+ STDOUT.sync = true
17
+ class CouchTomatoApp < Thor
18
+ include Thor::Actions
19
+ namespace :ct
20
+
21
+ desc 'init', 'Sets up the required structure for Couch Tomato'
22
+ def init
23
+ load_env
24
+ project_root = ::Rails.root rescue nil || "."
25
+
26
+ ct_yml_path = "#{project_root}/config/couch_tomato.yml.example"
27
+ couch_folder = "#{project_root}/couchdb"
28
+
29
+ print "Generating a sample couch_tomato yml... "
30
+ unless File.exist?(ct_yml_path)
31
+ FileUtils.mkdir "#{project_root}/config"
32
+ File.open(ct_yml_path, 'w') {|f| f.write(Ct_Yml_Example) }
33
+ puts "#{ct_yml_path} created"
34
+ else
35
+ puts "#{ct_yml_path} already exists"
36
+ end
37
+
38
+ print "Generating the couchdb folder.......... "
39
+ puts (File.directory? couch_folder) ? "#{couch_folder} already exists" : "#{FileUtils.mkdir "#{couch_folder}"} created"
40
+ print "Generating couchdb/migrate............. "
41
+ puts (File.directory? "#{couch_folder}/migrate") ? "#{couch_folder}/migrate already exists" : "#{FileUtils.mkdir "#{couch_folder}/migrate"} created"
42
+ print "Generating couchdb/views............... "
43
+ puts (File.directory? "#{couch_folder}/views") ? "#{couch_folder}/views already exists" : "#{FileUtils.mkdir "#{couch_folder}/views"} created"
44
+ end
45
+
46
+ desc 'push', 'Inserts the views into CouchDB'
47
+ method_options %w(RAILS_ENV -e) => :string
48
+ def push
49
+ load_env(options)
50
+ CouchTomato::JsViewSource.push
51
+ end
52
+
53
+ desc 'diff', 'Compares views in DB and the File System'
54
+ method_options %w(RAILS_ENV -e) => :string
55
+ def diff
56
+ load_env(options)
57
+ CouchTomato::JsViewSource.diff
58
+ end
59
+
60
+ desc 'drop', 'Drops databases for the current RAILS_ENV; ' +
61
+ 'If no databases are specified, user will be prompted if all databases should be removed; ' +
62
+ 'If the -r option is specified, all databases matching the regex will be dropped.'
63
+ method_options %w(RAILS_ENV -e) => :string, %w(DBS -d) => :array, %w(REGEX -r) => :string
64
+ def drop
65
+ load_env(options)
66
+ dbs = options["DBS"]
67
+ rm_all = false
68
+ rm_all = (yes? "Drop all databases?") if (options['REGEX'].nil? && dbs.nil?)
69
+
70
+ regex = Regexp.new(options['REGEX'].to_s)
71
+ databases(dbs) do |db, dir|
72
+ if (rm_all || db.name == db.name[regex]) && is_db?(db)
73
+ db.delete!
74
+ puts "Dropped #{db.name}"
75
+ end
76
+ end
77
+ end
78
+
79
+ desc 'migrate', 'Runs migrations'
80
+ method_options %w(RAILS_ENV -e) => :string, %w(VERSION -v) => :string, %w(STEP -s) => :string,
81
+ :redo => :boolean, :reset => :boolean, :up => :boolean, :down => :boolean
82
+ def migrate
83
+ load_env(options)
84
+ supported_args = %w(redo reset up down)
85
+ action = supported_args & options.keys
86
+ raise "Cannot provide more than one action." if action.length > 1
87
+
88
+ action = (action.empty?) ? nil : action.first
89
+ case action
90
+ when nil
91
+ migrate_helper
92
+ #Rollbacks the database one migration and re migrate up. If you want to rollback more than one step, define STEP=x. Target specific version with VERSION=x.
93
+ when "redo"
94
+ if ENV['VERSION']
95
+ down_helper
96
+ up_helper
97
+ else
98
+ rollback_helper
99
+ migrate_helper
100
+ end
101
+ #Resets your database using your migrations for the current environment
102
+ when "reset"
103
+ invoke :drop
104
+ invoke :push
105
+ migrate_helper
106
+ #Runs the "up" for a given migration VERSION.
107
+ when "up"
108
+ up_helper
109
+ #Runs the "down" for a given migration VERSION.
110
+ when "down"
111
+ down_helper
112
+ end
113
+ end
114
+
115
+ desc 'rollback', 'Rolls back to the previous version. Specify the number of steps with STEP=n'
116
+ method_options %w(RAILS_ENV -e) => :string, %w(STEP -s) => :string
117
+ def rollback
118
+ load_env(options)
119
+ rollback_helper
120
+ end
121
+
122
+ desc 'forward', 'Rolls forward to the next version. Specify the number of steps with STEP=n'
123
+ method_options %w(RAILS_ENV -e) => :string, %w(STEP -s) => :string
124
+ def forward
125
+ load_env(options)
126
+ databases do |db, dir|
127
+ CouchTomato::Migrator.forward(db, dir, ENV['STEP'] ? ENV['STEP'].to_i : 1)
128
+ end
129
+ end
130
+
131
+ desc 'replicate', 'Replicate databases between app environments'
132
+ method_options %w(RAILS_ENV -e) => :string, %w(SRC_DB -c) => :string,
133
+ %w(DST_DB -v) => :string , %w(SRC_SERVER -s) => :string , %w(DST_SERVER -t) => :string
134
+ def replicate
135
+ load_env(options)
136
+ src_server, dst_server = servers
137
+
138
+ src_db = options['SRC_DB']
139
+ dst_db = options['DST_DB'] || (src_server == dst_server ? "#{src_db}_bak" : src_db)
140
+
141
+ replicator = CouchTomato::Replicator.new(src_server, dst_server)
142
+
143
+ if src_db
144
+ puts "== Replicating '#{src_server}/#{src_db}' to '#{dst_server}/#{dst_db}'"
145
+ replicator.replicate(src_db, dst_db)
146
+ elsif src_server == dst_server
147
+ puts "== Replicating all databases at '#{src_server}' using '_bak' suffix for replicated database names"
148
+ replicator.replicate_all('_bak')
149
+ else
150
+ puts "== Replicating all databases from '#{src_server}' to '#{dst_server}'"
151
+ replicator.replicate_all
152
+ end
153
+ end
154
+
155
+ desc 'touch', 'Initiates the building of a design document'
156
+ method_options %w(RAILS_ENV -e) => :string, %w(DBS -d) => :array, :async => :boolean, %w(TIMEOUT -t) => :numeric
157
+ def touch
158
+ load_env(options)
159
+ view_path = "couchdb/views"
160
+ valid_dbs = options["DBS"] & (Dir["couchdb/views/**"].map {|db| File.basename(db) })
161
+ CouchTomato::JsViewSource.touch(valid_dbs, options.async?, options['TIMEOUT'])
162
+ end
163
+
164
+ private
165
+ def up_helper
166
+ version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
167
+ raise 'VERSION is required' unless version
168
+
169
+ databases do |db, dir|
170
+ CouchTomato::Migrator.run(:up, db, dir, version)
171
+ end
172
+ end
173
+
174
+ def down_helper
175
+ version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
176
+ raise 'VERSION is required' unless version
177
+
178
+ databases do |db, dir|
179
+ CouchTomato::Migrator.run(:down, db, dir, version)
180
+ end
181
+ end
182
+
183
+ def rollback_helper
184
+ databases do |db, dir|
185
+ CouchTomato::Migrator.rollback(db, dir, ENV['STEP'] ? ENV['STEP'].to_i : 1)
186
+ end
187
+ end
188
+
189
+ def migrate_helper
190
+ databases do |db, dir|
191
+ CouchTomato::Migrator.migrate(db, dir, ENV['VERSION'] ? ENV['VERSION'].to_i : nil)
192
+ end
193
+ end
194
+
195
+ def load_env(options=nil)
196
+ unless (Rails rescue nil)
197
+ print "Loading enviornment... "
198
+ %w(RAILS_ENV VERSION SRC_SERVER DST_SERVER STEP).each {|var| ENV[var] =
199
+ options[var] unless options[var].nil? } unless options.nil?
200
+
201
+ begin
202
+ require(File.join(ENV['PWD'], 'config', 'boot'))
203
+ require(File.join(ENV['PWD'], 'config', 'environment'))
204
+ puts "done."
205
+ rescue LoadError
206
+ puts "could not find environment files. Assuming direct use."
207
+ ensure
208
+ CouchTomato::Config.set_config_yml unless CouchTomato::Config.loaded?
209
+ end
210
+ end
211
+ end
212
+
213
+ def servers
214
+ local_server = "http://#{CouchTomato::Config.couch_address}:#{CouchTomato::Config.couch_port}"
215
+ src_server = (ENV['SRC_SERVER'] || local_server).gsub(/\s*\/\s*$/, '')
216
+ dst_server = (ENV['DST_SERVER'] || local_server).gsub(/\s*\/\s*$/, '')
217
+
218
+ return src_server, dst_server
219
+ end
220
+
221
+ def databases(db_names=nil)
222
+ dirs = Dir['couchdb/migrate/*']
223
+
224
+ db_map = dirs.inject({}) {|map, dir| map[File.basename(dir)] = dir; map }
225
+ db_names ||= db_map.keys
226
+ db_names.each do |db_name|
227
+ db = CouchTomato::JsViewSource.database(db_name)
228
+ yield db, db_map[db_name]
229
+ end
230
+ end
231
+
232
+ def is_db?(db)
233
+ begin
234
+ db.info
235
+ rescue
236
+ return false
237
+ end
238
+ return true
239
+ end
240
+
241
+ end
data/rails/init.rb CHANGED
@@ -1,7 +1,3 @@
1
1
  # this is for rails only
2
-
3
2
  require File.dirname(__FILE__) + '/../lib/couch_tomato'
4
-
5
- # CouchTomato::Config.database_name = YAML::load(File.read(Rails.root.to_s + '/config/couchdb.yml'))[RAILS_ENV]
6
-
7
3
  RAILS_DEFAULT_LOGGER.info "** couch_tomato: initialized from #{__FILE__}"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: couch_tomato
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Plastic Trophy
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-16 00:00:00 -08:00
12
+ date: 2010-01-21 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -50,9 +50,11 @@ extensions: []
50
50
 
51
51
  extra_rdoc_files:
52
52
  - README.md
53
+ - README.md_old
53
54
  files:
54
55
  - MIT-LICENSE.txt
55
56
  - README.md
57
+ - README.md_old
56
58
  - generators/couch_migration/couch_migration_generator.rb
57
59
  - generators/couch_migration/templates/migration.rb
58
60
  - generators/couch_view/couch_view_generator.rb
@@ -68,6 +70,7 @@ files:
68
70
  - lib/core_ext/symbol.rb
69
71
  - lib/core_ext/time.rb
70
72
  - lib/couch_tomato.rb
73
+ - lib/couch_tomato/config.rb
71
74
  - lib/couch_tomato/database.rb
72
75
  - lib/couch_tomato/js_view_source.rb
73
76
  - lib/couch_tomato/migration.rb
@@ -84,6 +87,7 @@ files:
84
87
  - lib/couch_tomato/persistence/validation.rb
85
88
  - lib/couch_tomato/replicator.rb
86
89
  - lib/tasks/couch_tomato.rake
90
+ - lib/tasks/couch_tomato.thor
87
91
  - rails/init.rb
88
92
  has_rdoc: true
89
93
  homepage: http://github.com/plastictrophy/couch_tomato