Merge commit 'candlerb/candlerb/design-doc' into candlerb-merge

* commit 'candlerb/candlerb/design-doc':
  Update handler for old versions of couchdb
  ClassProxy provides class-level methods on a dynamically chosen database.
  Remove obsolete 'move' methods
  Tidying up spec, remove unnecessary assignments to local variable
  Multiple database support for ExtendedDocument.
  Typo in comment
  Move design_doc attributes to Mixins::DesignDoc
This commit is contained in:
Matt Aimonetti 2009-03-27 10:21:56 -07:00
commit 4337e676ee
11 changed files with 347 additions and 80 deletions

View file

@ -35,11 +35,17 @@ JAVASCRIPT
end
# Dispatches to any named view.
# (using the database where this design doc was saved)
def view view_name, query={}, &block
view_on database, view_name, query, &block
end
# Dispatches to any named view in a specific database
def view_on db, view_name, query={}, &block
view_name = view_name.to_s
view_slug = "#{name}/#{view_name}"
defaults = (self['views'][view_name] && self['views'][view_name]["couchrest-defaults"]) || {}
fetch_view(view_slug, defaults.merge(query), &block)
db.view(view_slug, defaults.merge(query), &block)
end
def name
@ -64,22 +70,6 @@ JAVASCRIPT
(self['views'][view]["couchrest-defaults"]||{})
end
# def fetch_view_with_docs name, opts, raw=false, &block
# if raw
# fetch_view name, opts, &block
# else
# begin
# view = fetch_view name, opts.merge({:include_docs => true}), &block
# view['rows'].collect{|r|new(r['doc'])} if view['rows']
# rescue
# # fallback for old versions of couchdb that don't
# # have include_docs support
# view = fetch_view name, opts, &block
# view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
# end
# end
# end
def fetch_view view_name, opts, &block
database.view(view_name, opts, &block)
end

View file

@ -65,15 +65,6 @@ module CouchRest
result['ok']
end
# moves the document to a new id. If the destination id currently exists, a rev must be provided.
# <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
# hash with a '_rev' key
def move(dest)
raise ArgumentError, "doc.database required to copy" unless database
result = database.move_doc(self, dest)
result['ok']
end
# Returns the CouchDB uri for the document
def uri(append_rev = false)
return nil if new_document?

View file

@ -0,0 +1,108 @@
module CouchRest
module Mixins
module ClassProxy
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
# Return a proxy object which represents a model class on a
# chosen database instance. This allows you to DRY operations
# where a database is chosen dynamically.
#
# ==== Example:
#
# db = CouchRest::Database.new(...)
# articles = Article.on(db)
#
# articles.all { ... }
# articles.by_title { ... }
#
# u = articles.get("someid")
#
# u = articles.new(:title => "I like plankton")
# u.save # saved on the correct database
def on(database)
Proxy.new(self, database)
end
end
class Proxy #:nodoc:
def initialize(klass, database)
@klass = klass
@database = database
end
# ExtendedDocument
def new(*args)
doc = @klass.new(*args)
doc.database = @database
doc
end
def method_missing(m, *args, &block)
if has_view?(m)
query = args.shift || {}
view(m, query, *args, &block)
else
super
end
end
# Mixins::DocumentQueries
def all(opts = {}, &block)
@klass.all({:database => @database}.merge(opts), &block)
end
def first(opts = {})
@klass.first({:database => @database}.merge(opts))
end
def get(id)
@klass.get(id, @database)
end
# Mixins::Views
def has_view?(view)
@klass.has_view?(view)
end
def view(name, query={}, &block)
@klass.view(name, {:database => @database}.merge(query), &block)
end
def all_design_doc_versions
@klass.all_design_doc_versions(@database)
end
def cleanup_design_docs!
@klass.cleanup_design_docs!(@database)
end
# Mixins::DesignDoc
def design_doc
@klass.design_doc
end
def design_doc_fresh
@klass.design_doc_fresh
end
def refresh_design_doc
@klass.refresh_design_doc
end
def save_design_doc
@klass.save_design_doc_on(@database)
end
end
end
end
end

View file

@ -6,6 +6,9 @@ module CouchRest
def self.included(base)
base.extend(ClassMethods)
base.send(:extlib_inheritable_accessor, :design_doc)
base.send(:extlib_inheritable_accessor, :design_doc_slug_cache)
base.send(:extlib_inheritable_accessor, :design_doc_fresh)
end
module ClassMethods
@ -16,7 +19,7 @@ module CouchRest
def design_doc_slug
return design_doc_slug_cache if (design_doc_slug_cache && design_doc_fresh)
funcs = []
design_doc ||= Design.new(default_design_doc)
self.design_doc ||= Design.new(default_design_doc)
design_doc['views'].each do |name, view|
funcs << "#{name}/#{view['map']}#{view['reduce']}"
end
@ -40,21 +43,42 @@ module CouchRest
end
def refresh_design_doc
did = design_doc_id
saved = database.get(did) rescue nil
design_doc['_id'] = design_doc_id
design_doc.delete('_rev')
#design_doc.database = nil
self.design_doc_fresh = true
end
# Save the design doc onto the default database, and update the
# design_doc attribute
def save_design_doc
refresh_design_doc unless design_doc_fresh
self.design_doc = update_design_doc(design_doc)
end
# Save the design doc onto a target database in a thread-safe way,
# not modifying the model's design_doc
def save_design_doc_on(db)
update_design_doc(Design.new(design_doc), db)
end
private
# Writes out a design_doc to a given database, returning the
# updated design doc
def update_design_doc(design_doc, db = database)
saved = db.get(design_doc['_id']) rescue nil
if saved
design_doc['views'].each do |name, view|
saved['views'][name] = view
end
database.save_doc(saved)
self.design_doc = saved
db.save_doc(saved)
saved
else
design_doc['_id'] = did
design_doc.delete('_rev')
design_doc.database = database
design_doc.database = db
design_doc.save
design_doc
end
self.design_doc_fresh = true
end
end # module ClassMethods

View file

@ -36,8 +36,8 @@ module CouchRest
end
# Load a document from the database by id
def get(id)
doc = database.get id
def get(id, db = database)
doc = db.get id
new(doc)
end

View file

@ -3,4 +3,5 @@ require File.join(File.dirname(__FILE__), 'document_queries')
require File.join(File.dirname(__FILE__), 'views')
require File.join(File.dirname(__FILE__), 'design_doc')
require File.join(File.dirname(__FILE__), 'validation')
require File.join(File.dirname(__FILE__), 'extended_attachments')
require File.join(File.dirname(__FILE__), 'extended_attachments')
require File.join(File.dirname(__FILE__), 'class_proxy')

View file

@ -4,9 +4,6 @@ module CouchRest
def self.included(base)
base.extend(ClassMethods)
base.send(:extlib_inheritable_accessor, :design_doc)
base.send(:extlib_inheritable_accessor, :design_doc_slug_cache)
base.send(:extlib_inheritable_accessor, :design_doc_fresh)
end
module ClassMethods
@ -57,6 +54,10 @@ module CouchRest
# themselves. By default <tt>Post.by_date</tt> will return the
# documents included in the generated view.
#
# Calling with :database => [instance of CouchRest::Database] will
# send the query to a specific database, otherwise it will go to
# the model's default database (use_database)
#
# CouchRest::Database#view options can be applied at view definition
# time as defaults, and they will be curried and used at view query
# time. Or they can be overridden at query time.
@ -70,7 +71,7 @@ module CouchRest
# that model won't be available until generation is complete. This can
# take some time with large databases. Strategies are in the works.
#
# To understand the capabilities of this view system more compeletly,
# To understand the capabilities of this view system more completely,
# it is recommended that you read the RSpec file at
# <tt>spec/core/model_spec.rb</tt>.
@ -100,12 +101,13 @@ module CouchRest
refresh_design_doc
end
query[:raw] = true if query[:reduce]
db = query.delete(:database) || database
raw = query.delete(:raw)
fetch_view_with_docs(name, query, raw, &block)
fetch_view_with_docs(db, name, query, raw, &block)
end
def all_design_doc_versions
database.documents :startkey => "_design/#{self.to_s}-",
def all_design_doc_versions(db = database)
db.documents :startkey => "_design/#{self.to_s}-",
:endkey => "_design/#{self.to_s}-\u9999"
end
@ -114,11 +116,11 @@ module CouchRest
# and consistently using the latest code, is the way to clear out old design
# docs. Running it to early could mean that live code has to regenerate
# potentially large indexes.
def cleanup_design_docs!
ddocs = all_design_doc_versions
def cleanup_design_docs!(db = database)
ddocs = all_design_doc_versions(db)
ddocs["rows"].each do |row|
if (row['id'] != design_doc_id)
database.delete_doc({
db.delete_doc({
"_id" => row['id'],
"_rev" => row['value']['rev']
})
@ -128,30 +130,31 @@ module CouchRest
private
def fetch_view_with_docs(name, opts, raw=false, &block)
def fetch_view_with_docs(db, name, opts, raw=false, &block)
if raw || (opts.has_key?(:include_docs) && opts[:include_docs] == false)
fetch_view(name, opts, &block)
fetch_view(db, name, opts, &block)
else
begin
view = fetch_view name, opts.merge({:include_docs => true}), &block
view = fetch_view db, name, opts.merge({:include_docs => true}), &block
view['rows'].collect{|r|new(r['doc'])} if view['rows']
rescue
# fallback for old versions of couchdb that don't
# have include_docs support
view = fetch_view(name, opts, &block)
view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
view = fetch_view(db, name, opts, &block)
view['rows'].collect{|r|new(db.get(r['id']))} if view['rows']
end
end
end
def fetch_view view_name, opts, &block
def fetch_view(db, view_name, opts, &block)
raise "A view needs a database to operate on (specify :database option, or use_database in the #{self.class} class)" unless db
retryable = true
begin
design_doc.view(view_name, opts, &block)
# the design doc could have been deleted by a rouge process
design_doc.view_on(db, view_name, opts, &block)
# the design doc may not have been saved yet on this database
rescue RestClient::ResourceNotFound => e
if retryable
refresh_design_doc
save_design_doc_on(db)
retryable = false
retry
else

View file

@ -56,12 +56,6 @@ module RestClient
:url => url,
:headers => headers)
end
def self.move(url, headers={})
Request.execute(:method => :move,
:url => url,
:headers => headers)
end
# class Request
#

View file

@ -11,6 +11,7 @@ module CouchRest
include CouchRest::Mixins::Views
include CouchRest::Mixins::DesignDoc
include CouchRest::Mixins::ExtendedAttachments
include CouchRest::Mixins::ClassProxy
def self.inherited(subklass)
subklass.send(:include, CouchRest::Mixins::Properties)
@ -77,10 +78,10 @@ module CouchRest
end
# Temp solution to make the view_by methods available
def self.method_missing(m, *args)
def self.method_missing(m, *args, &block)
if has_view?(m)
query = args.shift || {}
view(m, query, *args)
view(m, query, *args, &block)
else
super
end

View file

@ -14,7 +14,7 @@ describe CouchRest::Design do
describe "with an unsaved view" do
before(:each) do
@des = CouchRest::Design.new
method = @des.view_by :name
@des.view_by :name
end
it "should accept a name" do
@des.name = "mytest"
@ -31,7 +31,7 @@ describe CouchRest::Design do
describe "saving" do
before(:each) do
@des = CouchRest::Design.new
method = @des.view_by :name
@des.view_by :name
@des.database = reset_test_db!
end
it "should fail without a name" do
@ -49,7 +49,7 @@ describe CouchRest::Design do
@db.bulk_save([{"name" => "x"},{"name" => "y"}])
@des = CouchRest::Design.new
@des.database = @db
method = @des.view_by :name
@des.view_by :name
end
it "should by queryable when it's saved" do
@des.name = "mydesign"
@ -57,6 +57,13 @@ describe CouchRest::Design do
res = @des.view :by_name
res["rows"][0]["key"].should == "x"
end
it "should be queryable on specified database" do
@des.name = "mydesign"
@des.save
@des.database = nil
res = @des.view_on @db, :by_name
res["rows"][0]["key"].should == "x"
end
end
describe "from a saved document" do
@ -92,7 +99,7 @@ describe CouchRest::Design do
@db = reset_test_db!
@des = CouchRest::Design.new
@des.name = "test"
method = @des.view_by :name, :descending => true
@des.view_by :name, :descending => true
@des.database = @db
@des.save
@db.bulk_save([{"name" => "a"},{"name" => "z"}])
@ -116,7 +123,7 @@ describe CouchRest::Design do
@db = reset_test_db!
@des = CouchRest::Design.new
@des.name = "test"
method = @des.view_by :name, :age
@des.view_by :name, :age
@des.database = @db
@des.save
@db.bulk_save([{"name" => "a", "age" => 2},

View file

@ -3,6 +3,14 @@ require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course')
describe "ExtendedDocument views" do
class Unattached < CouchRest::ExtendedDocument
# Note: no use_database here
property :title
property :questions
property :professor
view_by :title
end
describe "a model with simple views and a default param" do
before(:all) do
@ -75,12 +83,18 @@ describe "ExtendedDocument views" do
end
it "should yield" do
courses = []
rs = Course.by_title # remove me
Course.view(:by_title) do |course|
courses << course
end
courses[0]["doc"]["title"].should =='aaa'
end
it "should yield with by_key method" do
courses = []
Course.by_title do |course|
courses << course
end
courses[0]["doc"]["title"].should =='aaa'
end
end
@ -103,6 +117,144 @@ describe "ExtendedDocument views" do
end
end
describe "a model class not tied to a database" do
before(:all) do
reset_test_db!
@db = TEST_SERVER.default_database
%w{aaa bbb ddd eee}.each do |title|
u = Unattached.new(:title => title)
u.database = @db
u.save
@first_id ||= u.id
end
end
it "should barf on all if no database given" do
lambda{Unattached.all}.should raise_error
end
it "should query all" do
rs = Unattached.all :database=>@db
rs.length.should == 4
end
it "should barf on query if no database given" do
lambda{Unattached.view :by_title}.should raise_error
end
it "should make the design doc upon first query" do
Unattached.by_title :database=>@db
doc = Unattached.design_doc
doc['views']['all']['map'].should include('Unattached')
end
it "should merge query params" do
rs = Unattached.by_title :database=>@db, :startkey=>"bbb", :endkey=>"eee"
rs.length.should == 3
end
it "should query via view" do
view = Unattached.view :by_title, :database=>@db
designed = Unattached.by_title :database=>@db
view.should == designed
end
it "should yield" do
things = []
Unattached.view(:by_title, :database=>@db) do |thing|
things << thing
end
things[0]["doc"]["title"].should =='aaa'
end
it "should yield with by_key method" do
things = []
Unattached.by_title(:database=>@db) do |thing|
things << thing
end
things[0]["doc"]["title"].should =='aaa'
end
it "should barf on get if no database given" do
lambda{Unattached.get("aaa")}.should raise_error
end
it "should get from specific database" do
u = Unattached.get(@first_id, @db)
u.title.should == "aaa"
end
it "should barf on first if no database given" do
lambda{Unattached.first}.should raise_error
end
it "should get first" do
u = Unattached.first :database=>@db
u.title.should =~ /\A...\z/
end
it "should barf on all_design_doc_versions if no database given" do
lambda{Unattached.all_design_doc_versions}.should raise_error
end
it "should clean up design docs left around on specific database" do
Unattached.by_title :database=>@db
Unattached.all_design_doc_versions(@db)["rows"].length.should == 1
Unattached.view_by :questions
Unattached.by_questions :database=>@db
Unattached.all_design_doc_versions(@db)["rows"].length.should == 2
Unattached.cleanup_design_docs!(@db)
Unattached.all_design_doc_versions(@db)["rows"].length.should == 1
end
end
describe "class proxy" do
before(:all) do
reset_test_db!
@us = Unattached.on(TEST_SERVER.default_database)
%w{aaa bbb ddd eee}.each do |title|
u = @us.new(:title => title)
u.save
@first_id ||= u.id
end
end
it "should query all" do
rs = @us.all
rs.length.should == 4
end
it "should make the design doc upon first query" do
@us.by_title
doc = @us.design_doc
doc['views']['all']['map'].should include('Unattached')
end
it "should merge query params" do
rs = @us.by_title :startkey=>"bbb", :endkey=>"eee"
rs.length.should == 3
end
it "should query via view" do
view = @us.view :by_title
designed = @us.by_title
view.should == designed
end
it "should yield" do
things = []
@us.view(:by_title) do |thing|
things << thing
end
things[0]["doc"]["title"].should =='aaa'
end
it "should yield with by_key method" do
things = []
@us.by_title do |thing|
things << thing
end
things[0]["doc"]["title"].should =='aaa'
end
it "should get from specific database" do
u = @us.get(@first_id)
u.title.should == "aaa"
end
it "should get first" do
u = @us.first
u.title.should =~ /\A...\z/
end
it "should clean up design docs left around on specific database" do
@us.by_title
@us.all_design_doc_versions["rows"].length.should == 1
Unattached.view_by :professor
@us.by_professor
@us.all_design_doc_versions["rows"].length.should == 2
@us.cleanup_design_docs!
@us.all_design_doc_versions["rows"].length.should == 1
end
end
describe "a model with a compound key view" do
before(:all) do
Article.design_doc_fresh = false
@ -177,14 +329,11 @@ describe "ExtendedDocument views" do
newdocs["rows"].length.should == @design_docs["rows"].length
end
it "should create a new version of the design document on view access" do
old_design_doc = Article.database.documents(:key => @design_docs["rows"].first["key"], :include_docs => true)["rows"][0]["doc"]
ddocs = Article.all_design_doc_versions["rows"].length
Article.view_by :updated_at
Article.by_updated_at
newdocs = Article.database.documents({:startkey => "_design/", :endkey => "_design/\u9999"})
doc = Article.database.documents(:key => @design_docs["rows"].first["key"], :include_docs => true)["rows"][0]["doc"]
doc["_rev"].should_not == old_design_doc["_rev"]
doc["views"].keys.should include("by_updated_at")
Article.all_design_doc_versions["rows"].length.should == ddocs + 1
Article.design_doc["views"].keys.should include("by_updated_at")
end
end
@ -196,12 +345,11 @@ describe "ExtendedDocument views" do
Article.by_field
end
it "should clean them up" do
ddocs = Article.all_design_doc_versions
Article.view_by :stream
Article.by_stream
Article.all_design_doc_versions["rows"].length.should > 1
Article.cleanup_design_docs!
ddocs = Article.all_design_doc_versions
ddocs["rows"].length.should == 1
Article.all_design_doc_versions["rows"].length.should == 1
end
end
end