rocking_chair 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -59,6 +59,8 @@ module RockingChair
59
59
  find_by_attribute(match[1])
60
60
  elsif match = view_name.match(/\Aassociation_#{design_document_name}_belongs_to_(\w+)\Z/)
61
61
  find_belongs_to(match[1])
62
+ elsif match = view_name.match(/\Aassociation_#{design_document_name}_has_and_belongs_to_many_(\w+)\Z/)
63
+ find_has_and_belongs_to_many(match[1])
62
64
  else
63
65
  raise "Unknown View implementation for view #{view_name.inspect} in design document _design/#{design_document_name}"
64
66
  end
@@ -126,6 +128,17 @@ module RockingChair
126
128
  filter_deleted_items if options['without_deleted'].to_s == 'true'
127
129
  sort_by_attribute('created_at')
128
130
  end
131
+
132
+ def find_has_and_belongs_to_many(belongs_to)
133
+ if foreign_keys_are_stored_on_my_class?(belongs_to)
134
+ filter_items_by_key_in_attribute_group(foreign_key_array_id(belongs_to))
135
+ filter_items_without_correct_ruby_class
136
+ else
137
+ filter_items_by_query_document_attributes(belongs_to)
138
+ end
139
+ filter_deleted_items if options['without_deleted'].to_s == 'true'
140
+ sort_by_attribute('created_at')
141
+ end
129
142
 
130
143
  def find_by_attribute(attribute_string)
131
144
  attributes = attribute_string.split("_and_")
@@ -148,7 +161,12 @@ module RockingChair
148
161
  end
149
162
  @view_name = view_name.gsub(/_withoutdeleted\Z/, '').gsub(/_without_deleted\Z/, '').gsub(/_withdeleted\Z/, '').gsub(/_with_deleted\Z/, '')
150
163
  end
151
-
164
+
165
+ def foreign_keys_are_stored_on_my_class?(belongs_to)
166
+ reduce_function = @view_document['reduce']
167
+ reduce_function == "_sum"
168
+ end
169
+
152
170
  def initialize_ruby_store
153
171
  @ruby_store = database.storage.dup
154
172
  @ruby_store.each{|doc_id, json_document| ruby_store[doc_id] = JSON.parse(json_document, :create_additions => false) }
@@ -188,6 +206,22 @@ module RockingChair
188
206
  end
189
207
  end
190
208
 
209
+ def filter_items_by_key_in_attribute_group(attribute)
210
+ filter_key = (options['key'] || options['startkey']).to_s
211
+ @keys = keys.select do |key|
212
+ document = ruby_store[key]
213
+ document_attribute = RockingChair::Helper.access(attribute, document)
214
+ document_attribute && document_attribute.is_a?(Array) && document_attribute.include?(filter_key)
215
+ end
216
+ end
217
+
218
+ def filter_items_by_query_document_attributes(belongs_to)
219
+ filter_key = options['key'] || options['startkey']
220
+ filtering_document = ruby_store[filter_key.to_s]
221
+ reverse_foreign_key = foreign_key_array_id(design_document_name)
222
+ @keys = RockingChair::Helper.access(reverse_foreign_key, filtering_document) || []
223
+ end
224
+
191
225
  def filter_deleted_items
192
226
  @keys = keys.delete_if do |key|
193
227
  document = ruby_store[key]
@@ -258,6 +292,10 @@ module RockingChair
258
292
  def foreign_key_id(name)
259
293
  name.underscore.gsub('/','__').gsub('::','__') + "_id"
260
294
  end
295
+
296
+ def foreign_key_array_id(name)
297
+ name.underscore.singularize.gsub('/','__').gsub('::','__') + "_ids"
298
+ end
261
299
 
262
300
  def key_description
263
301
  description = {'key' => options['key']}
@@ -6,6 +6,7 @@ class User
6
6
  property :firstname
7
7
  property :lastname
8
8
  belongs_to :project
9
+ has_and_belongs_to_many :groups, :storing_keys => true
9
10
 
10
11
  enable_soft_delete
11
12
 
@@ -33,4 +34,27 @@ class CustomViewUser
33
34
 
34
35
  property :tags
35
36
  view :by_tags, :type => SimplyStored::Couch::Views::ArrayPropertyViewSpec, :key => :tags
37
+ end
38
+
39
+ class Group
40
+ include SimplyStored::Couch
41
+
42
+ property :name
43
+ has_and_belongs_to_many :users, :storing_keys => false
44
+ end
45
+
46
+ class Server
47
+ include SimplyStored::Couch
48
+
49
+ property :hostname
50
+
51
+ has_and_belongs_to_many :networks, :storing_keys => true
52
+ end
53
+
54
+ class Network
55
+ include SimplyStored::Couch
56
+
57
+ property :klass
58
+
59
+ has_and_belongs_to_many :servers, :storing_keys => false
36
60
  end
@@ -279,6 +279,113 @@ class SimplyStoredTest < Test::Unit::TestCase
279
279
  end
280
280
  end
281
281
 
282
+ context "when handling n:m relations using has_and_belongs_to_many" do
283
+ should "work relations from both sides" do
284
+ network_a = Network.create(:klass => "A")
285
+ network_b = Network.create(:klass => "B")
286
+ 3.times {
287
+ server = Server.new
288
+ server.add_network(network_a)
289
+ server.add_network(network_b)
290
+ }
291
+ assert_equal 3, network_a.servers.size
292
+ network_a.servers.each do |server|
293
+ assert_equal 2, server.networks.size
294
+ end
295
+ assert_equal 3, network_b.servers.size
296
+ network_b.servers.each do |server|
297
+ assert_equal 2, server.networks.size
298
+ end
299
+ end
300
+
301
+ should "work relations from both sides - regardless from where the add was called" do
302
+ network_a = Network.create(:klass => "A")
303
+ network_b = Network.create(:klass => "B")
304
+ 3.times {
305
+ server = Server.new
306
+ network_a.add_server(server)
307
+ network_b.add_server(server)
308
+ }
309
+ assert_equal 3, network_a.servers.size
310
+ network_a.servers.each do |server|
311
+ assert_equal 2, server.networks.size, server.network_ids.inspect
312
+ end
313
+ assert_equal 3, network_b.servers.size
314
+ network_b.servers.each do |server|
315
+ assert_equal 2, server.networks.size
316
+ end
317
+ end
318
+
319
+ should "cound correctly - regardless of the side of the relation" do
320
+ network_a = Network.create(:klass => "A")
321
+ network_b = Network.create(:klass => "B")
322
+ 3.times {
323
+ server = Server.new
324
+ network_a.add_server(server)
325
+ network_b.add_server(server)
326
+ }
327
+ assert_equal 3, network_a.server_count
328
+ assert_equal 3, network_b.server_count
329
+ assert_equal 2, network_a.servers.first.network_count
330
+ assert_equal 2, network_b.servers.first.network_count
331
+ end
332
+
333
+ should "support mixing order and limit" do
334
+ network_1 = Network.find(Network.create!(:klass => "A").id)
335
+ network_1.created_at = Time.local(2001)
336
+ network_1.save!
337
+
338
+ network_2 = Network.find(Network.create!(:klass => "B").id)
339
+ network_2.created_at = Time.local(2002)
340
+ network_2.save!
341
+
342
+ server_1 = Server.find(Server.create!(:hostname => 'www.example.com').id)
343
+ server_1.created_at = Time.local(2003)
344
+ network_1.add_server(server_1)
345
+ network_2.add_server(server_1)
346
+
347
+ server_2 = Server.find(Server.create!(:hostname => 'foo.com').id)
348
+ server_2.created_at = Time.local(2004)
349
+ network_1.add_server(server_2)
350
+ network_2.add_server(server_2)
351
+
352
+ assert_equal ['www.example.com', 'foo.com'], network_1.servers(:order => :asc).map(&:hostname)
353
+ assert_equal ['www.example.com', 'foo.com'].reverse, network_1.servers(:order => :desc).map(&:hostname)
354
+
355
+ assert_equal ['A', 'B'], server_2.networks(:order => :asc).map(&:klass)
356
+ assert_equal ['A', 'B'].reverse, server_2.networks(:order => :desc).map(&:klass)
357
+
358
+ assert_equal ['www.example.com'], network_1.servers(:order => :asc, :limit => 1).map(&:hostname)
359
+ assert_equal ['foo.com'], network_1.servers(:order => :desc, :limit => 1).map(&:hostname)
360
+
361
+ assert_equal ['A'], server_2.networks(:order => :asc, :limit => 1).map(&:klass)
362
+ assert_equal ['B'], server_2.networks(:order => :desc, :limit => 1).map(&:klass)
363
+ end
364
+
365
+ should "when counting cache the result" do
366
+ @network = Network.create(:klass => "C")
367
+ @server = Server.create
368
+ assert_equal 0, @network.server_count
369
+ Server.create(:network_ids => [@network.id])
370
+ assert_equal 0, @network.server_count
371
+ assert_equal 0, @network.instance_variable_get("@server_count")
372
+ @network.instance_variable_set("@server_count", nil)
373
+ assert_equal 1, @network.server_count
374
+ end
375
+
376
+ should "when counting cache the result - from both directions" do
377
+ @network = Network.create(:klass => "C")
378
+ @server = Server.create
379
+ assert_equal 0, @server.network_count
380
+ @server.network_ids = [@network.id]
381
+ @server.save!
382
+ assert_equal 0, @server.network_count
383
+ assert_equal 0, @server.instance_variable_get("@network_count")
384
+ @server.instance_variable_set("@network_count", nil)
385
+ assert_equal 1, @server.network_count
386
+ end
387
+ end
388
+
282
389
  context "when deleting all design docs" do
283
390
  should "reset all design docs" do
284
391
  User.find_all_by_firstname('a')
data/test/view_test.rb CHANGED
@@ -46,15 +46,19 @@ class ViewTest < Test::Unit::TestCase
46
46
  'map' => "function(item){emit(item)}"
47
47
  },
48
48
  'by_firstname' => {
49
- 'reduce' => "function(key, values){ return values.length }",
49
+ 'reduce' => "_sum",
50
50
  "map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
51
51
  },
52
52
  'by_firstname_and_lastname' => {
53
- 'reduce' => "function(key, values){ return values.length }",
53
+ 'reduce' => "_sum",
54
54
  "map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
55
55
  },
56
56
  'association_user_belongs_to_project' => {
57
- 'reduce' => "function(key, values){ return values.length }",
57
+ 'reduce' => "_sum",
58
+ "map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
59
+ },
60
+ 'association_user_has_and_belongs_to_many_groups' => {
61
+ 'reduce' => "_sum",
58
62
  "map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
59
63
  }
60
64
  }}
@@ -366,6 +370,80 @@ class ViewTest < Test::Unit::TestCase
366
370
  end
367
371
  end
368
372
 
373
+ context "has and belongs to many views" do
374
+ setup do
375
+ @db['group_1'] = {"name" => 'A', 'ruby_class' => 'Group'}
376
+ @db['group_2'] = {"name" => 'B', 'ruby_class' => 'Group'}
377
+ @db['user_1'] = {"group_ids" => ['group_1', 'group_2'], 'firstname' => 'Bert', 'ruby_class' => 'User'}
378
+ @db['user_2'] = {"group_ids" => ['group_1'], 'firstname' => 'Alf', 'ruby_class' => 'User'}
379
+ @db['_design/group'] = { 'language' => 'javascript', 'views' => {
380
+ 'all_documents' => {
381
+ 'reduce' => nil,
382
+ 'map' => "function(item){emit(item)}"
383
+ },
384
+ 'association_group_has_and_belongs_to_many_users' => {
385
+ 'reduce' => "function(key, values){ return values.length }",
386
+ "map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
387
+ }
388
+ }}
389
+ end
390
+
391
+ should "return all item not storing keys" do
392
+ assert_equal(JSON.parse({
393
+ "total_rows" => 2,
394
+ "rows" => [
395
+ {"doc" => {
396
+ "_rev" => "the-rev",
397
+ "group_ids" => ["group_1","group_2"],
398
+ "_id" => "user_1",
399
+ "firstname" => "Bert",
400
+ "ruby_class" => "User"
401
+ },
402
+ "id" => "user_1",
403
+ "value" => nil,
404
+ "key" => "group_1"
405
+ },{
406
+ "doc" => {
407
+ "_rev" => "the-rev",
408
+ "group_ids" => ["group_1"],
409
+ "_id" => "user_2",
410
+ "firstname" => "Alf",
411
+ "ruby_class" => "User"
412
+ },
413
+ "id" => "user_2",
414
+ "value" => nil,
415
+ "key" => "group_1"
416
+ }],
417
+ "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')))
418
+ end
419
+
420
+ should "return all item storing keys" do
421
+ assert_equal(JSON.parse({
422
+ "total_rows" => 2,
423
+ "rows" => [
424
+ {"doc" => {
425
+ "_rev" => "the-rev",
426
+ "_id" => "group_1",
427
+ "name" => "A",
428
+ "ruby_class" => "Group"
429
+ },
430
+ "id" => "group_1",
431
+ "value" => nil,
432
+ "key" => "user_1"
433
+ },{
434
+ "doc" => {
435
+ "_rev" => "the-rev",
436
+ "_id" => "group_2",
437
+ "name" => "B",
438
+ "ruby_class" => "Group"
439
+ },
440
+ "id" => "group_2",
441
+ "value" => nil,
442
+ "key" => "user_1"
443
+ }],
444
+ "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')))
445
+ end
446
+ end
369
447
  end
370
448
 
371
449
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rocking_chair
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 19
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 5
10
- version: 0.2.5
8
+ - 3
9
+ - 0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Jonathan Weiss
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-09-30 00:00:00 +02:00
18
+ date: 2011-02-22 00:00:00 +01:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency