Permalink
Browse files

implement compatibility with SimplyStored has_and_belongs_to_many

  • Loading branch information...
1 parent 9ef597d commit 4a8b6e88fdd557ab3ff7ef516c20a1b8a924d856 @jweiss committed Feb 16, 2011
Showing with 196 additions and 4 deletions.
  1. +39 −1 lib/rocking_chair/view.rb
  2. +24 −0 test/fixtures/simply_stored_fixtures.rb
  3. +52 −0 test/simply_stored_test.rb
  4. +81 −3 test/view_test.rb
View
@@ -59,6 +59,8 @@ def find
find_by_attribute(match[1])
elsif match = view_name.match(/\Aassociation_#{design_document_name}_belongs_to_(\w+)\Z/)
find_belongs_to(match[1])
+ elsif match = view_name.match(/\Aassociation_#{design_document_name}_has_and_belongs_to_many_(\w+)\Z/)
+ find_has_and_belongs_to_many(match[1])
else
raise "Unknown View implementation for view #{view_name.inspect} in design document _design/#{design_document_name}"
end
@@ -126,6 +128,17 @@ def find_belongs_to(belongs_to)
filter_deleted_items if options['without_deleted'].to_s == 'true'
sort_by_attribute('created_at')
end
+
+ def find_has_and_belongs_to_many(belongs_to)
+ if foreign_keys_are_stored_on_my_class?(belongs_to)
+ filter_items_by_key_in_attribute_group(foreign_key_array_id(belongs_to))
+ filter_items_without_correct_ruby_class
+ else
+ filter_items_by_query_document_attributes(belongs_to)
+ end
+ filter_deleted_items if options['without_deleted'].to_s == 'true'
+ sort_by_attribute('created_at')
+ end
def find_by_attribute(attribute_string)
attributes = attribute_string.split("_and_")
@@ -148,7 +161,12 @@ def normalize_view_name
end
@view_name = view_name.gsub(/_withoutdeleted\Z/, '').gsub(/_without_deleted\Z/, '').gsub(/_withdeleted\Z/, '').gsub(/_with_deleted\Z/, '')
end
-
+
+ def foreign_keys_are_stored_on_my_class?(belongs_to)
+ reduce_function = @view_document['reduce']
+ reduce_function == "_sum"
+ end
+
def initialize_ruby_store
@ruby_store = database.storage.dup
@ruby_store.each{|doc_id, json_document| ruby_store[doc_id] = JSON.parse(json_document, :create_additions => false) }
@@ -188,6 +206,22 @@ def filter_items_not_in_range(attribute, start_key, end_key)
end
end
+ def filter_items_by_key_in_attribute_group(attribute)
+ filter_key = (options['key'] || options['startkey']).to_s
+ @keys = keys.select do |key|
+ document = ruby_store[key]
+ document_attribute = RockingChair::Helper.access(attribute, document)
+ document_attribute && document_attribute.is_a?(Array) && document_attribute.include?(filter_key)
+ end
+ end
+
+ def filter_items_by_query_document_attributes(belongs_to)
+ filter_key = options['key'] || options['startkey']
+ filtering_document = ruby_store[filter_key.to_s]
+ reverse_foreign_key = foreign_key_array_id(design_document_name)
+ @keys = RockingChair::Helper.access(reverse_foreign_key, filtering_document)
+ end
+
def filter_deleted_items
@keys = keys.delete_if do |key|
document = ruby_store[key]
@@ -258,6 +292,10 @@ def filter_by_startkey_docid_and_endkey_docid
def foreign_key_id(name)
name.underscore.gsub('/','__').gsub('::','__') + "_id"
end
+
+ def foreign_key_array_id(name)
+ name.underscore.singularize.gsub('/','__').gsub('::','__') + "_ids"
+ end
def key_description
description = {'key' => options['key']}
@@ -6,6 +6,7 @@ class User
property :firstname
property :lastname
belongs_to :project
+ has_and_belongs_to_many :groups, :storing_keys => true
enable_soft_delete
@@ -33,4 +34,27 @@ class CustomViewUser
property :tags
view :by_tags, :type => SimplyStored::Couch::Views::ArrayPropertyViewSpec, :key => :tags
+end
+
+class Group
+ include SimplyStored::Couch
+
+ property :name
+ has_and_belongs_to_many :users, :storing_keys => false
+end
+
+class Server
+ include SimplyStored::Couch
+
+ property :hostname
+
+ has_and_belongs_to_many :networks, :storing_keys => true
+end
+
+class Network
+ include SimplyStored::Couch
+
+ property :klass
+
+ has_and_belongs_to_many :servers, :storing_keys => false
end
View
@@ -279,6 +279,58 @@ class SimplyStoredTest < Test::Unit::TestCase
end
end
+ context "when handling n:m relations using has_and_belongs_to_many" do
+ should "work relations from both sides" do
+ network_a = Network.create(:klass => "A")
+ network_b = Network.create(:klass => "B")
+ 3.times {
+ server = Server.new
+ server.add_network(network_a)
+ server.add_network(network_b)
+ }
+ assert_equal 3, network_a.servers.size
+ network_a.servers.each do |server|
+ assert_equal 2, server.networks.size
+ end
+ assert_equal 3, network_b.servers.size
+ network_b.servers.each do |server|
+ assert_equal 2, server.networks.size
+ end
+ end
+
+ should "work relations from both sides - regardless from where the add was called" do
+ network_a = Network.create(:klass => "A")
+ network_b = Network.create(:klass => "B")
+ 3.times {
+ server = Server.new
+ network_a.add_server(server)
+ network_b.add_server(server)
+ }
+ assert_equal 3, network_a.servers.size
+ network_a.servers.each do |server|
+ assert_equal 2, server.networks.size, server.network_ids.inspect
+ end
+ assert_equal 3, network_b.servers.size
+ network_b.servers.each do |server|
+ assert_equal 2, server.networks.size
+ end
+ end
+
+ should "cound correctly - regardless of the side of the relation" do
+ network_a = Network.create(:klass => "A")
+ network_b = Network.create(:klass => "B")
+ 3.times {
+ server = Server.new
+ network_a.add_server(server)
+ network_b.add_server(server)
+ }
+ assert_equal 3, network_a.server_count
+ assert_equal 3, network_b.server_count
+ assert_equal 2, network_a.servers.first.network_count
+ assert_equal 2, network_b.servers.first.network_count
+ end
+ end
+
context "when deleting all design docs" do
should "reset all design docs" do
User.find_all_by_firstname('a')
View
@@ -46,15 +46,19 @@ class ViewTest < Test::Unit::TestCase
'map' => "function(item){emit(item)}"
},
'by_firstname' => {
- 'reduce' => "function(key, values){ return values.length }",
+ 'reduce' => "_sum",
"map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
},
'by_firstname_and_lastname' => {
- 'reduce' => "function(key, values){ return values.length }",
+ 'reduce' => "_sum",
"map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
},
'association_user_belongs_to_project' => {
- 'reduce' => "function(key, values){ return values.length }",
+ 'reduce' => "_sum",
+ "map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
+ },
+ 'association_user_has_and_belongs_to_many_groups' => {
+ 'reduce' => "_sum",
"map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
}
}}
@@ -366,6 +370,80 @@ class ViewTest < Test::Unit::TestCase
end
end
+ context "has and belongs to many views" do
+ setup do
+ @db['group_1'] = {"name" => 'A', 'ruby_class' => 'Group'}
+ @db['group_2'] = {"name" => 'B', 'ruby_class' => 'Group'}
+ @db['user_1'] = {"group_ids" => ['group_1', 'group_2'], 'firstname' => 'Bert', 'ruby_class' => 'User'}
+ @db['user_2'] = {"group_ids" => ['group_1'], 'firstname' => 'Alf', 'ruby_class' => 'User'}
+ @db['_design/group'] = { 'language' => 'javascript', 'views' => {
+ 'all_documents' => {
+ 'reduce' => nil,
+ 'map' => "function(item){emit(item)}"
+ },
+ 'association_group_has_and_belongs_to_many_users' => {
+ 'reduce' => "function(key, values){ return values.length }",
+ "map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
+ }
+ }}
+ end
+
+ should "return all item not storing keys" do
+ assert_equal(JSON.parse({
+ "total_rows" => 2,
+ "rows" => [
+ {"doc" => {
+ "_rev" => "the-rev",
+ "group_ids" => ["group_1","group_2"],
+ "_id" => "user_1",
+ "firstname" => "Bert",
+ "ruby_class" => "User"
+ },
+ "id" => "user_1",
+ "value" => nil,
+ "key" => "group_1"
+ },{
+ "doc" => {
+ "_rev" => "the-rev",
+ "group_ids" => ["group_1"],
+ "_id" => "user_2",
+ "firstname" => "Alf",
+ "ruby_class" => "User"
+ },
+ "id" => "user_2",
+ "value" => nil,
+ "key" => "group_1"
+ }],
+ "offset" => 0}.to_json), JSON.parse(@db.view('user', 'association_user_has_and_belongs_to_many_groups', 'key' => "group_1".to_json, 'include_docs' => 'true')))
+ end
+
+ should "return all item storing keys" do
+ assert_equal(JSON.parse({
+ "total_rows" => 2,
+ "rows" => [
+ {"doc" => {
+ "_rev" => "the-rev",
+ "_id" => "group_1",
+ "name" => "A",
+ "ruby_class" => "Group"
+ },
+ "id" => "group_1",
+ "value" => nil,
+ "key" => "user_1"
+ },{
+ "doc" => {
+ "_rev" => "the-rev",
+ "_id" => "group_2",
+ "name" => "B",
+ "ruby_class" => "Group"
+ },
+ "id" => "group_2",
+ "value" => nil,
+ "key" => "user_1"
+ }],
+ "offset" => 0}.to_json), JSON.parse(@db.view('group', 'association_group_has_and_belongs_to_many_users', 'key' => "user_1".to_json, 'include_docs' => 'true')))
+ end
+ end
end
end

0 comments on commit 4a8b6e8

Please sign in to comment.