Multiple database support for ExtendedDocument.
New optional parameters are available to select the database: Mixins::DocumentQueries * get <id>, <db> * all :database => <db> * first :database => <db> Mixins::Views * view <name>, :database => <db> * all_design_doc_versions <db> * cleanup_design_docs! <db> Mixins::DesignDoc * refresh_design_doc now only updates the design_doc _id and removes _rev * call save_design_doc to save and update the design_doc * call save_design_doc_on <db> to save the design doc on a given database without modifying the model's design_doc object Design (core/design.rb) * new method view_on <db>, ... Bug fixes: * design_doc_slug in mixins/design_doc.rb was using an empty document to calculate the slug each time * method_missing in core/extended_document.rb now passes a block through
This commit is contained in:
parent
f9278a4ca6
commit
ec7848b783
|
@ -35,11 +35,17 @@ JAVASCRIPT
|
||||||
end
|
end
|
||||||
|
|
||||||
# Dispatches to any named view.
|
# Dispatches to any named view.
|
||||||
|
# (using the database where this design doc was saved)
|
||||||
def view view_name, query={}, &block
|
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_name = view_name.to_s
|
||||||
view_slug = "#{name}/#{view_name}"
|
view_slug = "#{name}/#{view_name}"
|
||||||
defaults = (self['views'][view_name] && self['views'][view_name]["couchrest-defaults"]) || {}
|
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
|
end
|
||||||
|
|
||||||
def name
|
def name
|
||||||
|
@ -64,22 +70,6 @@ JAVASCRIPT
|
||||||
(self['views'][view]["couchrest-defaults"]||{})
|
(self['views'][view]["couchrest-defaults"]||{})
|
||||||
end
|
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
|
def fetch_view view_name, opts, &block
|
||||||
database.view(view_name, opts, &block)
|
database.view(view_name, opts, &block)
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,7 +19,7 @@ module CouchRest
|
||||||
def design_doc_slug
|
def design_doc_slug
|
||||||
return design_doc_slug_cache if (design_doc_slug_cache && design_doc_fresh)
|
return design_doc_slug_cache if (design_doc_slug_cache && design_doc_fresh)
|
||||||
funcs = []
|
funcs = []
|
||||||
design_doc ||= Design.new(default_design_doc)
|
self.design_doc ||= Design.new(default_design_doc)
|
||||||
design_doc['views'].each do |name, view|
|
design_doc['views'].each do |name, view|
|
||||||
funcs << "#{name}/#{view['map']}#{view['reduce']}"
|
funcs << "#{name}/#{view['map']}#{view['reduce']}"
|
||||||
end
|
end
|
||||||
|
@ -43,21 +43,42 @@ module CouchRest
|
||||||
end
|
end
|
||||||
|
|
||||||
def refresh_design_doc
|
def refresh_design_doc
|
||||||
did = design_doc_id
|
design_doc['_id'] = design_doc_id
|
||||||
saved = database.get(did) rescue nil
|
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
|
if saved
|
||||||
design_doc['views'].each do |name, view|
|
design_doc['views'].each do |name, view|
|
||||||
saved['views'][name] = view
|
saved['views'][name] = view
|
||||||
end
|
end
|
||||||
database.save_doc(saved)
|
db.save_doc(saved)
|
||||||
self.design_doc = saved
|
saved
|
||||||
else
|
else
|
||||||
design_doc['_id'] = did
|
design_doc.database = db
|
||||||
design_doc.delete('_rev')
|
|
||||||
design_doc.database = database
|
|
||||||
design_doc.save
|
design_doc.save
|
||||||
|
design_doc
|
||||||
end
|
end
|
||||||
self.design_doc_fresh = true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end # module ClassMethods
|
end # module ClassMethods
|
||||||
|
|
|
@ -36,8 +36,8 @@ module CouchRest
|
||||||
end
|
end
|
||||||
|
|
||||||
# Load a document from the database by id
|
# Load a document from the database by id
|
||||||
def get(id)
|
def get(id, db = database)
|
||||||
doc = database.get id
|
doc = db.get id
|
||||||
new(doc)
|
new(doc)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,10 @@ module CouchRest
|
||||||
# themselves. By default <tt>Post.by_date</tt> will return the
|
# themselves. By default <tt>Post.by_date</tt> will return the
|
||||||
# documents included in the generated view.
|
# 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
|
# CouchRest::Database#view options can be applied at view definition
|
||||||
# time as defaults, and they will be curried and used at view query
|
# time as defaults, and they will be curried and used at view query
|
||||||
# time. Or they can be overridden at query time.
|
# time. Or they can be overridden at query time.
|
||||||
|
@ -67,7 +71,7 @@ module CouchRest
|
||||||
# that model won't be available until generation is complete. This can
|
# that model won't be available until generation is complete. This can
|
||||||
# take some time with large databases. Strategies are in the works.
|
# 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
|
# it is recommended that you read the RSpec file at
|
||||||
# <tt>spec/core/model_spec.rb</tt>.
|
# <tt>spec/core/model_spec.rb</tt>.
|
||||||
|
|
||||||
|
@ -97,12 +101,13 @@ module CouchRest
|
||||||
refresh_design_doc
|
refresh_design_doc
|
||||||
end
|
end
|
||||||
query[:raw] = true if query[:reduce]
|
query[:raw] = true if query[:reduce]
|
||||||
|
db = query.delete(:database) || database
|
||||||
raw = query.delete(:raw)
|
raw = query.delete(:raw)
|
||||||
fetch_view_with_docs(name, query, raw, &block)
|
fetch_view_with_docs(db, name, query, raw, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
def all_design_doc_versions
|
def all_design_doc_versions(db = database)
|
||||||
database.documents :startkey => "_design/#{self.to_s}-",
|
db.documents :startkey => "_design/#{self.to_s}-",
|
||||||
:endkey => "_design/#{self.to_s}-\u9999"
|
:endkey => "_design/#{self.to_s}-\u9999"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -111,11 +116,11 @@ module CouchRest
|
||||||
# and consistently using the latest code, is the way to clear out old design
|
# 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
|
# docs. Running it to early could mean that live code has to regenerate
|
||||||
# potentially large indexes.
|
# potentially large indexes.
|
||||||
def cleanup_design_docs!
|
def cleanup_design_docs!(db = database)
|
||||||
ddocs = all_design_doc_versions
|
ddocs = all_design_doc_versions(db)
|
||||||
ddocs["rows"].each do |row|
|
ddocs["rows"].each do |row|
|
||||||
if (row['id'] != design_doc_id)
|
if (row['id'] != design_doc_id)
|
||||||
database.delete_doc({
|
db.delete_doc({
|
||||||
"_id" => row['id'],
|
"_id" => row['id'],
|
||||||
"_rev" => row['value']['rev']
|
"_rev" => row['value']['rev']
|
||||||
})
|
})
|
||||||
|
@ -125,30 +130,31 @@ module CouchRest
|
||||||
|
|
||||||
private
|
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)
|
if raw || (opts.has_key?(:include_docs) && opts[:include_docs] == false)
|
||||||
fetch_view(name, opts, &block)
|
fetch_view(db, name, opts, &block)
|
||||||
else
|
else
|
||||||
begin
|
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']
|
view['rows'].collect{|r|new(r['doc'])} if view['rows']
|
||||||
rescue
|
rescue
|
||||||
# fallback for old versions of couchdb that don't
|
# fallback for old versions of couchdb that don't
|
||||||
# have include_docs support
|
# have include_docs support
|
||||||
view = fetch_view(name, opts, &block)
|
view = fetch_view(db, name, opts, &block)
|
||||||
view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
|
view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
|
||||||
end
|
end
|
||||||
end
|
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
|
retryable = true
|
||||||
begin
|
begin
|
||||||
design_doc.view(view_name, opts, &block)
|
design_doc.view_on(db, view_name, opts, &block)
|
||||||
# the design doc could have been deleted by a rogue process
|
# the design doc may not have been saved yet on this database
|
||||||
rescue RestClient::ResourceNotFound => e
|
rescue RestClient::ResourceNotFound => e
|
||||||
if retryable
|
if retryable
|
||||||
refresh_design_doc
|
save_design_doc_on(db)
|
||||||
retryable = false
|
retryable = false
|
||||||
retry
|
retry
|
||||||
else
|
else
|
||||||
|
|
|
@ -77,10 +77,10 @@ module CouchRest
|
||||||
end
|
end
|
||||||
|
|
||||||
# Temp solution to make the view_by methods available
|
# 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)
|
if has_view?(m)
|
||||||
query = args.shift || {}
|
query = args.shift || {}
|
||||||
view(m, query, *args)
|
view(m, query, *args, &block)
|
||||||
else
|
else
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
|
@ -57,6 +57,13 @@ describe CouchRest::Design do
|
||||||
res = @des.view :by_name
|
res = @des.view :by_name
|
||||||
res["rows"][0]["key"].should == "x"
|
res["rows"][0]["key"].should == "x"
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "from a saved document" do
|
describe "from a saved document" do
|
||||||
|
|
|
@ -4,6 +4,13 @@ require File.join(FIXTURE_PATH, 'more', 'course')
|
||||||
|
|
||||||
describe "ExtendedDocument views" do
|
describe "ExtendedDocument views" do
|
||||||
|
|
||||||
|
class Unattached < CouchRest::ExtendedDocument
|
||||||
|
# Note: no use_database here
|
||||||
|
property :title
|
||||||
|
property :questions
|
||||||
|
view_by :title
|
||||||
|
end
|
||||||
|
|
||||||
describe "a model with simple views and a default param" do
|
describe "a model with simple views and a default param" do
|
||||||
before(:all) do
|
before(:all) do
|
||||||
Article.all.map{|a| a.destroy(true)}
|
Article.all.map{|a| a.destroy(true)}
|
||||||
|
@ -75,12 +82,18 @@ describe "ExtendedDocument views" do
|
||||||
end
|
end
|
||||||
it "should yield" do
|
it "should yield" do
|
||||||
courses = []
|
courses = []
|
||||||
rs = Course.by_title # remove me
|
|
||||||
Course.view(:by_title) do |course|
|
Course.view(:by_title) do |course|
|
||||||
courses << course
|
courses << course
|
||||||
end
|
end
|
||||||
courses[0]["doc"]["title"].should =='aaa'
|
courses[0]["doc"]["title"].should =='aaa'
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@ -103,6 +116,76 @@ describe "ExtendedDocument views" do
|
||||||
end
|
end
|
||||||
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 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 "a model with a compound key view" do
|
describe "a model with a compound key view" do
|
||||||
before(:all) do
|
before(:all) do
|
||||||
Article.design_doc_fresh = false
|
Article.design_doc_fresh = false
|
||||||
|
@ -177,14 +260,11 @@ describe "ExtendedDocument views" do
|
||||||
newdocs["rows"].length.should == @design_docs["rows"].length
|
newdocs["rows"].length.should == @design_docs["rows"].length
|
||||||
end
|
end
|
||||||
it "should create a new version of the design document on view access" do
|
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.view_by :updated_at
|
||||||
Article.by_updated_at
|
Article.by_updated_at
|
||||||
newdocs = Article.database.documents({:startkey => "_design/", :endkey => "_design/\u9999"})
|
Article.all_design_doc_versions["rows"].length.should == ddocs + 1
|
||||||
|
Article.design_doc["views"].keys.should include("by_updated_at")
|
||||||
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")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -196,12 +276,11 @@ describe "ExtendedDocument views" do
|
||||||
Article.by_field
|
Article.by_field
|
||||||
end
|
end
|
||||||
it "should clean them up" do
|
it "should clean them up" do
|
||||||
ddocs = Article.all_design_doc_versions
|
|
||||||
Article.view_by :stream
|
Article.view_by :stream
|
||||||
Article.by_stream
|
Article.by_stream
|
||||||
|
Article.all_design_doc_versions["rows"].length.should > 1
|
||||||
Article.cleanup_design_docs!
|
Article.cleanup_design_docs!
|
||||||
ddocs = Article.all_design_doc_versions
|
Article.all_design_doc_versions["rows"].length.should == 1
|
||||||
ddocs["rows"].length.should == 1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue