sequel 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. data/CHANGELOG +1551 -4
  2. data/README +306 -19
  3. data/Rakefile +84 -56
  4. data/bin/sequel +106 -0
  5. data/doc/cheat_sheet.rdoc +225 -0
  6. data/doc/dataset_filtering.rdoc +182 -0
  7. data/lib/sequel_core.rb +136 -0
  8. data/lib/sequel_core/adapters/adapter_skeleton.rb +54 -0
  9. data/lib/sequel_core/adapters/ado.rb +80 -0
  10. data/lib/sequel_core/adapters/db2.rb +148 -0
  11. data/lib/sequel_core/adapters/dbi.rb +117 -0
  12. data/lib/sequel_core/adapters/informix.rb +78 -0
  13. data/lib/sequel_core/adapters/jdbc.rb +186 -0
  14. data/lib/sequel_core/adapters/jdbc/mysql.rb +55 -0
  15. data/lib/sequel_core/adapters/jdbc/postgresql.rb +66 -0
  16. data/lib/sequel_core/adapters/jdbc/sqlite.rb +47 -0
  17. data/lib/sequel_core/adapters/mysql.rb +231 -0
  18. data/lib/sequel_core/adapters/odbc.rb +155 -0
  19. data/lib/sequel_core/adapters/odbc_mssql.rb +106 -0
  20. data/lib/sequel_core/adapters/openbase.rb +64 -0
  21. data/lib/sequel_core/adapters/oracle.rb +170 -0
  22. data/lib/sequel_core/adapters/postgres.rb +199 -0
  23. data/lib/sequel_core/adapters/shared/mysql.rb +275 -0
  24. data/lib/sequel_core/adapters/shared/postgres.rb +351 -0
  25. data/lib/sequel_core/adapters/shared/sqlite.rb +146 -0
  26. data/lib/sequel_core/adapters/sqlite.rb +138 -0
  27. data/lib/sequel_core/connection_pool.rb +194 -0
  28. data/lib/sequel_core/core_ext.rb +203 -0
  29. data/lib/sequel_core/core_sql.rb +184 -0
  30. data/lib/sequel_core/database.rb +471 -0
  31. data/lib/sequel_core/database/schema.rb +156 -0
  32. data/lib/sequel_core/dataset.rb +457 -0
  33. data/lib/sequel_core/dataset/callback.rb +13 -0
  34. data/lib/sequel_core/dataset/convenience.rb +245 -0
  35. data/lib/sequel_core/dataset/pagination.rb +96 -0
  36. data/lib/sequel_core/dataset/query.rb +41 -0
  37. data/lib/sequel_core/dataset/schema.rb +15 -0
  38. data/lib/sequel_core/dataset/sql.rb +889 -0
  39. data/lib/sequel_core/deprecated.rb +26 -0
  40. data/lib/sequel_core/exceptions.rb +42 -0
  41. data/lib/sequel_core/migration.rb +187 -0
  42. data/lib/sequel_core/object_graph.rb +216 -0
  43. data/lib/sequel_core/pretty_table.rb +71 -0
  44. data/lib/sequel_core/schema.rb +2 -0
  45. data/lib/sequel_core/schema/generator.rb +239 -0
  46. data/lib/sequel_core/schema/sql.rb +325 -0
  47. data/lib/sequel_core/sql.rb +812 -0
  48. data/lib/sequel_model.rb +5 -1
  49. data/lib/sequel_model/association_reflection.rb +3 -8
  50. data/lib/sequel_model/base.rb +15 -10
  51. data/lib/sequel_model/inflector.rb +3 -5
  52. data/lib/sequel_model/plugins.rb +1 -1
  53. data/lib/sequel_model/record.rb +11 -3
  54. data/lib/sequel_model/schema.rb +4 -4
  55. data/lib/sequel_model/validations.rb +6 -1
  56. data/spec/adapters/ado_spec.rb +17 -0
  57. data/spec/adapters/informix_spec.rb +96 -0
  58. data/spec/adapters/mysql_spec.rb +764 -0
  59. data/spec/adapters/oracle_spec.rb +222 -0
  60. data/spec/adapters/postgres_spec.rb +441 -0
  61. data/spec/adapters/spec_helper.rb +7 -0
  62. data/spec/adapters/sqlite_spec.rb +400 -0
  63. data/spec/integration/dataset_test.rb +51 -0
  64. data/spec/integration/eager_loader_test.rb +702 -0
  65. data/spec/integration/schema_test.rb +102 -0
  66. data/spec/integration/spec_helper.rb +44 -0
  67. data/spec/integration/type_test.rb +43 -0
  68. data/spec/rcov.opts +2 -0
  69. data/spec/sequel_core/connection_pool_spec.rb +363 -0
  70. data/spec/sequel_core/core_ext_spec.rb +156 -0
  71. data/spec/sequel_core/core_sql_spec.rb +427 -0
  72. data/spec/sequel_core/database_spec.rb +964 -0
  73. data/spec/sequel_core/dataset_spec.rb +2977 -0
  74. data/spec/sequel_core/expression_filters_spec.rb +346 -0
  75. data/spec/sequel_core/migration_spec.rb +261 -0
  76. data/spec/sequel_core/object_graph_spec.rb +234 -0
  77. data/spec/sequel_core/pretty_table_spec.rb +58 -0
  78. data/spec/sequel_core/schema_generator_spec.rb +122 -0
  79. data/spec/sequel_core/schema_spec.rb +497 -0
  80. data/spec/sequel_core/spec_helper.rb +51 -0
  81. data/spec/{association_reflection_spec.rb → sequel_model/association_reflection_spec.rb} +6 -6
  82. data/spec/{associations_spec.rb → sequel_model/associations_spec.rb} +47 -18
  83. data/spec/{base_spec.rb → sequel_model/base_spec.rb} +2 -1
  84. data/spec/{caching_spec.rb → sequel_model/caching_spec.rb} +0 -0
  85. data/spec/{dataset_methods_spec.rb → sequel_model/dataset_methods_spec.rb} +13 -1
  86. data/spec/{eager_loading_spec.rb → sequel_model/eager_loading_spec.rb} +75 -14
  87. data/spec/{hooks_spec.rb → sequel_model/hooks_spec.rb} +4 -4
  88. data/spec/sequel_model/inflector_spec.rb +119 -0
  89. data/spec/{model_spec.rb → sequel_model/model_spec.rb} +30 -11
  90. data/spec/{plugins_spec.rb → sequel_model/plugins_spec.rb} +0 -0
  91. data/spec/{record_spec.rb → sequel_model/record_spec.rb} +47 -6
  92. data/spec/{schema_spec.rb → sequel_model/schema_spec.rb} +18 -4
  93. data/spec/{spec_helper.rb → sequel_model/spec_helper.rb} +3 -2
  94. data/spec/{validations_spec.rb → sequel_model/validations_spec.rb} +37 -17
  95. data/spec/spec_config.rb +9 -0
  96. data/spec/spec_config.rb.example +10 -0
  97. metadata +110 -37
  98. data/spec/inflector_spec.rb +0 -34
@@ -0,0 +1,702 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ describe "Eagerly loading a tree structure" do
4
+ before do
5
+ class ::Node < Sequel::Model
6
+ set_schema do
7
+ primary_key :id
8
+ foreign_key :parent_id, :nodes
9
+ end
10
+ create_table!
11
+
12
+ many_to_one :parent
13
+ one_to_many :children, :key=>:parent_id
14
+
15
+ # Only useful when eager loading
16
+ many_to_one :ancestors, :eager_loader=>(proc do |key_hash, nodes, associations|
17
+ # Handle cases where the root node has the same parent_id as primary_key
18
+ # and also when it is NULL
19
+ non_root_nodes = nodes.reject do |n|
20
+ if [nil, n.pk].include?(n.parent_id)
21
+ # Make sure root nodes have their parent association set to nil
22
+ n.associations[:parent] = nil
23
+ true
24
+ else
25
+ false
26
+ end
27
+ end
28
+ unless non_root_nodes.empty?
29
+ id_map = {}
30
+ # Create an map of parent_ids to nodes that have that parent id
31
+ non_root_nodes.each{|n| (id_map[n.parent_id] ||= []) << n}
32
+ # Doesn't cause an infinte loop, because when only the root node
33
+ # is left, this is not called.
34
+ Node.filter(Node.primary_key=>id_map.keys.sort).eager(:ancestors).all do |node|
35
+ # Populate the parent association for each node
36
+ id_map[node.pk].each{|n| n.associations[:parent] = node}
37
+ end
38
+ end
39
+ end)
40
+ many_to_one :descendants, :eager_loader=>(proc do |key_hash, nodes, associations|
41
+ id_map = {}
42
+ nodes.each do |n|
43
+ # Initialize an empty array of child associations for each parent node
44
+ n.associations[:children] = []
45
+ # Populate identity map of nodes
46
+ id_map[n.pk] = n
47
+ end
48
+ # Doesn't cause an infinite loop, because the :eager_loader is not called
49
+ # if no records are returned. Exclude id = parent_id to avoid infinite loop
50
+ # if the root note is one of the returned records and it has parent_id = id
51
+ # instead of parent_id = NULL.
52
+ Node.filter(:parent_id=>id_map.keys.sort).exclude(:id=>:parent_id).eager(:descendants).all do |node|
53
+ # Get the parent from the identity map
54
+ parent = id_map[node.parent_id]
55
+ # Set the child's parent association to the parent
56
+ node.associations[:parent] = parent
57
+ # Add the child association to the array of children in the parent
58
+ parent.associations[:children] << node
59
+ end
60
+ end)
61
+ end
62
+
63
+ Node.insert(:parent_id=>1)
64
+ Node.insert(:parent_id=>1)
65
+ Node.insert(:parent_id=>1)
66
+ Node.insert(:parent_id=>2)
67
+ Node.insert(:parent_id=>4)
68
+ Node.insert(:parent_id=>5)
69
+ Node.insert(:parent_id=>6)
70
+ clear_sqls
71
+ end
72
+ after do
73
+ Node.drop_table
74
+ Object.send(:remove_const, :Node)
75
+ end
76
+
77
+ it "#descendants should get all descendants in one call" do
78
+ nodes = Node.filter(:id=>1).eager(:descendants).all
79
+ sqls_should_be('SELECT * FROM nodes WHERE (id = 1)',
80
+ 'SELECT * FROM nodes WHERE ((parent_id IN (1)) AND (id != parent_id))',
81
+ 'SELECT * FROM nodes WHERE ((parent_id IN (2, 3)) AND (id != parent_id))',
82
+ 'SELECT * FROM nodes WHERE ((parent_id IN (4)) AND (id != parent_id))',
83
+ 'SELECT * FROM nodes WHERE ((parent_id IN (5)) AND (id != parent_id))',
84
+ 'SELECT * FROM nodes WHERE ((parent_id IN (6)) AND (id != parent_id))',
85
+ 'SELECT * FROM nodes WHERE ((parent_id IN (7)) AND (id != parent_id))')
86
+ nodes.length.should == 1
87
+ node = nodes.first
88
+ node.pk.should == 1
89
+ node.children.length.should == 2
90
+ node.children.collect{|x| x.pk}.sort.should == [2, 3]
91
+ node.children.collect{|x| x.parent}.should == [node, node]
92
+ node = nodes.first.children.find{|x| x.pk == 2}
93
+ node.children.length.should == 1
94
+ node.children.first.pk.should == 4
95
+ node.children.first.parent.should == node
96
+ node = node.children.first
97
+ node.children.length.should == 1
98
+ node.children.first.pk.should == 5
99
+ node.children.first.parent.should == node
100
+ node = node.children.first
101
+ node.children.length.should == 1
102
+ node.children.first.pk.should == 6
103
+ node.children.first.parent.should == node
104
+ node = node.children.first
105
+ node.children.length.should == 1
106
+ node.children.first.pk.should == 7
107
+ node.children.first.parent.should == node
108
+ sqls_should_be
109
+ end
110
+
111
+ it "#ancestors should get all ancestors in one call" do
112
+ nodes = Node.filter(:id=>[7,3]).order(:id).eager(:ancestors).all
113
+ sqls_should_be('SELECT * FROM nodes WHERE (id IN (7, 3)) ORDER BY id',
114
+ 'SELECT * FROM nodes WHERE (id IN (1, 6))',
115
+ 'SELECT * FROM nodes WHERE (id IN (5))',
116
+ 'SELECT * FROM nodes WHERE (id IN (4))',
117
+ 'SELECT * FROM nodes WHERE (id IN (2))',
118
+ 'SELECT * FROM nodes WHERE (id IN (1))')
119
+ nodes.length.should == 2
120
+ nodes.collect{|x| x.pk}.should == [3, 7]
121
+ nodes.first.parent.pk.should == 1
122
+ nodes.first.parent.parent.should == nil
123
+ node = nodes.last
124
+ node.parent.pk.should == 6
125
+ node = node.parent
126
+ node.parent.pk.should == 5
127
+ node = node.parent
128
+ node.parent.pk.should == 4
129
+ node = node.parent
130
+ node.parent.pk.should == 2
131
+ node = node.parent
132
+ node.parent.pk.should == 1
133
+ node.parent.parent.should == nil
134
+ sqls_should_be
135
+ end
136
+ end
137
+
138
+ describe "Association Extensions" do
139
+ before do
140
+ module ::FindOrCreate
141
+ def find_or_create(vals)
142
+ # Exploits the fact that Sequel filters are ruby objects that
143
+ # can be introspected.
144
+ author_id = @opts[:where].args[1]
145
+ first(vals) || \
146
+ @opts[:models][nil].create(vals.merge(:author_id=>author_id))
147
+ end
148
+ end
149
+ class ::Author < Sequel::Model
150
+ set_schema do
151
+ primary_key :id
152
+ end
153
+ create_table!
154
+ one_to_many :authorships, :extend=>FindOrCreate, :dataset=>(proc do
155
+ key = pk
156
+ ds = Authorship.filter(:author_id=>key)
157
+ ds.meta_def(:find_or_create_by_name) do |name|
158
+ first(:name=>name) || Authorship.create(:name=>name, :author_id=>key)
159
+ end
160
+ ds
161
+ end)
162
+ end
163
+ class ::Authorship < Sequel::Model
164
+ set_schema do
165
+ primary_key :id
166
+ foreign_key :author_id, :authors
167
+ text :name
168
+ end
169
+ create_table!
170
+ many_to_one :author
171
+ end
172
+ @author = Author.create
173
+ clear_sqls
174
+ end
175
+ after do
176
+ Authorship.drop_table
177
+ Author.drop_table
178
+ Object.send(:remove_const, :Author)
179
+ Object.send(:remove_const, :Authorship)
180
+ end
181
+
182
+ it "should allow methods to be called on the dataset method" do
183
+ Authorship.count.should == 0
184
+ sqls_should_be('SELECT COUNT(*) FROM authorships LIMIT 1')
185
+ authorship = @author.authorships_dataset.find_or_create_by_name('Bob')
186
+ sqls_should_be("SELECT * FROM authorships WHERE ((author_id = 1) AND (name = 'Bob')) LIMIT 1",
187
+ /INSERT INTO authorships \((author_id, name|name, author_id)\) VALUES \((1, 'Bob'|'Bob', 1)\)/,
188
+ "SELECT * FROM authorships WHERE (id = 1) LIMIT 1")
189
+ Authorship.count.should == 1
190
+ Authorship.first.should == authorship
191
+ sqls_should_be('SELECT COUNT(*) FROM authorships LIMIT 1', "SELECT * FROM authorships LIMIT 1")
192
+ authorship.name.should == 'Bob'
193
+ authorship.author_id.should == @author.id
194
+ @author.authorships_dataset.find_or_create_by_name('Bob').should == authorship
195
+ sqls_should_be("SELECT * FROM authorships WHERE ((author_id = 1) AND (name = 'Bob')) LIMIT 1")
196
+ Authorship.count.should == 1
197
+ sqls_should_be('SELECT COUNT(*) FROM authorships LIMIT 1')
198
+ authorship2 = @author.authorships_dataset.find_or_create(:name=>'Jim')
199
+ sqls_should_be("SELECT * FROM authorships WHERE ((author_id = 1) AND (name = 'Jim')) LIMIT 1",
200
+ /INSERT INTO authorships \((author_id, name|name, author_id)\) VALUES \((1, 'Jim'|'Jim', 1)\)/,
201
+ "SELECT * FROM authorships WHERE (id = 2) LIMIT 1")
202
+ Authorship.count.should == 2
203
+ sqls_should_be('SELECT COUNT(*) FROM authorships LIMIT 1')
204
+ Authorship.order(:name).map(:name).should == ['Bob', 'Jim']
205
+ sqls_should_be('SELECT * FROM authorships ORDER BY name')
206
+ authorship2.name.should == 'Jim'
207
+ authorship2.author_id.should == @author.id
208
+ @author.authorships_dataset.find_or_create(:name=>'Jim').should == authorship2
209
+ sqls_should_be("SELECT * FROM authorships WHERE ((author_id = 1) AND (name = 'Jim')) LIMIT 1")
210
+ end
211
+ end
212
+
213
+ describe "has_many :through has_many and has_one :through belongs_to" do
214
+ before do
215
+ class ::Firm < Sequel::Model
216
+ set_schema do
217
+ primary_key :id
218
+ end
219
+ create_table!
220
+ one_to_many :clients
221
+ one_to_many :invoices, :read_only=>true, \
222
+ :dataset=>proc{Invoice.eager_graph(:client).filter(:client__firm_id=>pk)}, \
223
+ :after_load=>(proc do |firm, invs|
224
+ invs.each do |inv|
225
+ inv.client.associations[:firm] = inv.associations[:firm] = firm
226
+ end
227
+ end), \
228
+ :eager_loader=>(proc do |key_hash, firms, associations|
229
+ id_map = key_hash[Firm.primary_key]
230
+ firms.each{|firm| firm.associations[:invoices] = []}
231
+ Invoice.eager_graph(:client).filter(:client__firm_id=>id_map.keys).all do |inv|
232
+ id_map[inv.client.firm_id].each do |firm|
233
+ inv.client.associations[:firm] = inv.associations[:firm] = firm
234
+ firm.associations[:invoices] << inv
235
+ end
236
+ end
237
+ end)
238
+ end
239
+
240
+ class ::Client < Sequel::Model
241
+ set_schema do
242
+ primary_key :id
243
+ foreign_key :firm_id, :firms
244
+ end
245
+ create_table!
246
+ many_to_one :firm
247
+ one_to_many :invoices
248
+ end
249
+
250
+ class ::Invoice < Sequel::Model
251
+ set_schema do
252
+ primary_key :id
253
+ foreign_key :client_id, :clients
254
+ end
255
+ create_table!
256
+ many_to_one :client
257
+ many_to_one :firm, :key=>nil, :read_only=>true, \
258
+ :dataset=>proc{Firm.eager_graph(:clients).filter(:clients__id=>client_id)}, \
259
+ :after_load=>(proc do |inv, firm|
260
+ # Delete the cached associations from firm, because it only has the
261
+ # client with this invoice, instead of all clients of the firm
262
+ inv.associations[:client] = firm.associations.delete(:clients).first
263
+ end), \
264
+ :eager_loader=>(proc do |key_hash, invoices, associations|
265
+ id_map = {}
266
+ invoices.each do |inv|
267
+ inv.associations[:firm] = nil
268
+ inv.associations[:client] = nil
269
+ (id_map[inv.client_id] ||= []) << inv
270
+ end
271
+ Firm.eager_graph(:clients).filter(:clients__id=>id_map.keys).all do |firm|
272
+ # Delete the cached associations from firm, because it only has the
273
+ # clients related the invoices being eagerly loaded, instead of all
274
+ # clients of the firm.
275
+ firm.associations.delete(:clients).each do |client|
276
+ id_map[client.pk].each do |inv|
277
+ inv.associations[:firm] = firm
278
+ inv.associations[:client] = client
279
+ end
280
+ end
281
+ end
282
+ end)
283
+ end
284
+ @firm1 = Firm.create
285
+ @firm2 = Firm.create
286
+ @client1 = Client.create(:firm => @firm1)
287
+ @client2 = Client.create(:firm => @firm1)
288
+ @client3 = Client.create(:firm => @firm2)
289
+ @invoice1 = Invoice.create(:client => @client1)
290
+ @invoice2 = Invoice.create(:client => @client1)
291
+ @invoice3 = Invoice.create(:client => @client2)
292
+ @invoice4 = Invoice.create(:client => @client3)
293
+ @invoice5 = Invoice.create(:client => @client3)
294
+ clear_sqls
295
+ end
296
+ after do
297
+ Invoice.drop_table
298
+ Client.drop_table
299
+ Firm.drop_table
300
+ Object.send(:remove_const, :Firm)
301
+ Object.send(:remove_const, :Client)
302
+ Object.send(:remove_const, :Invoice)
303
+ end
304
+
305
+ it "should return has_many :through has_many records for a single object" do
306
+ invs = @firm1.invoices.sort_by{|x| x.pk}
307
+ sqls_should_be('SELECT invoices.id, invoices.client_id, client.id AS client_id_0, client.firm_id FROM invoices LEFT OUTER JOIN clients AS client ON (client.id = invoices.client_id) WHERE (client.firm_id = 1)')
308
+ invs.should == [@invoice1, @invoice2, @invoice3]
309
+ invs[0].client.should == @client1
310
+ invs[1].client.should == @client1
311
+ invs[2].client.should == @client2
312
+ invs.collect{|i| i.firm}.should == [@firm1, @firm1, @firm1]
313
+ invs.collect{|i| i.client.firm}.should == [@firm1, @firm1, @firm1]
314
+ sqls_should_be
315
+ end
316
+
317
+ it "should eagerly load has_many :through has_many records for multiple objects" do
318
+ firms = Firm.order(:id).eager(:invoices).all
319
+ sqls_should_be("SELECT * FROM firms ORDER BY id", "SELECT invoices.id, invoices.client_id, client.id AS client_id_0, client.firm_id FROM invoices LEFT OUTER JOIN clients AS client ON (client.id = invoices.client_id) WHERE (client.firm_id IN (1, 2))")
320
+ firms.should == [@firm1, @firm2]
321
+ firm1, firm2 = firms
322
+ invs1 = firm1.invoices.sort_by{|x| x.pk}
323
+ invs2 = firm2.invoices.sort_by{|x| x.pk}
324
+ invs1.should == [@invoice1, @invoice2, @invoice3]
325
+ invs2.should == [@invoice4, @invoice5]
326
+ invs1[0].client.should == @client1
327
+ invs1[1].client.should == @client1
328
+ invs1[2].client.should == @client2
329
+ invs2[0].client.should == @client3
330
+ invs2[1].client.should == @client3
331
+ invs1.collect{|i| i.firm}.should == [@firm1, @firm1, @firm1]
332
+ invs2.collect{|i| i.firm}.should == [@firm2, @firm2]
333
+ invs1.collect{|i| i.client.firm}.should == [@firm1, @firm1, @firm1]
334
+ invs2.collect{|i| i.client.firm}.should == [@firm2, @firm2]
335
+ sqls_should_be
336
+ end
337
+
338
+ it "should return has_one :through belongs_to records for a single object" do
339
+ firm = @invoice1.firm
340
+ sqls_should_be('SELECT firms.id, clients.id AS clients_id, clients.firm_id FROM firms LEFT OUTER JOIN clients ON (clients.firm_id = firms.id) WHERE (clients.id = 1)')
341
+ firm.should == @firm1
342
+ @invoice1.client.should == @client1
343
+ @invoice1.client.firm.should == @firm1
344
+ sqls_should_be
345
+ firm.associations[:clients].should == nil
346
+ end
347
+
348
+ it "should eagerly load has_one :through belongs_to records for multiple objects" do
349
+ invs = Invoice.order(:id).eager(:firm).all
350
+ sqls_should_be("SELECT * FROM invoices ORDER BY id", "SELECT firms.id, clients.id AS clients_id, clients.firm_id FROM firms LEFT OUTER JOIN clients ON (clients.firm_id = firms.id) WHERE (clients.id IN (1, 2, 3))")
351
+ invs.should == [@invoice1, @invoice2, @invoice3, @invoice4, @invoice5]
352
+ invs[0].firm.should == @firm1
353
+ invs[0].client.should == @client1
354
+ invs[0].client.firm.should == @firm1
355
+ invs[0].firm.associations[:clients].should == nil
356
+ invs[1].firm.should == @firm1
357
+ invs[1].client.should == @client1
358
+ invs[1].client.firm.should == @firm1
359
+ invs[1].firm.associations[:clients].should == nil
360
+ invs[2].firm.should == @firm1
361
+ invs[2].client.should == @client2
362
+ invs[2].client.firm.should == @firm1
363
+ invs[2].firm.associations[:clients].should == nil
364
+ invs[3].firm.should == @firm2
365
+ invs[3].client.should == @client3
366
+ invs[3].client.firm.should == @firm2
367
+ invs[3].firm.associations[:clients].should == nil
368
+ invs[4].firm.should == @firm2
369
+ invs[4].client.should == @client3
370
+ invs[4].client.firm.should == @firm2
371
+ invs[4].firm.associations[:clients].should == nil
372
+ sqls_should_be
373
+ end
374
+ end
375
+
376
+ describe "Polymorphic Associations" do
377
+ before do
378
+ class ::Asset < Sequel::Model
379
+ set_schema do
380
+ primary_key :id
381
+ integer :attachable_id
382
+ text :attachable_type
383
+ end
384
+ create_table!
385
+ many_to_one :attachable, :reciprocal=>:assets, \
386
+ :dataset=>(proc do
387
+ klass = attachable_type.constantize
388
+ klass.filter(klass.primary_key=>attachable_id)
389
+ end), \
390
+ :eager_loader=>(proc do |key_hash, assets, associations|
391
+ id_map = {}
392
+ assets.each do |asset|
393
+ asset.associations[:attachable] = nil
394
+ ((id_map[asset.attachable_type] ||= {})[asset.attachable_id] ||= []) << asset
395
+ end
396
+ id_map.each do |klass_name, id_map|
397
+ klass = klass_name.constantize
398
+ klass.filter(klass.primary_key=>id_map.keys).all do |attach|
399
+ id_map[attach.pk].each do |asset|
400
+ asset.associations[:attachable] = attach
401
+ end
402
+ end
403
+ end
404
+ end)
405
+
406
+ private
407
+
408
+ def _attachable=(attachable)
409
+ self[:attachable_id] = (attachable.pk if attachable)
410
+ self[:attachable_type] = (attachable.class.name if attachable)
411
+ end
412
+ end
413
+
414
+ class ::Post < Sequel::Model
415
+ set_schema do
416
+ primary_key :id
417
+ end
418
+ create_table!
419
+ one_to_many :assets, :key=>:attachable_id do |ds|
420
+ ds.filter(:attachable_type=>'Post')
421
+ end
422
+
423
+ private
424
+
425
+ def _add_asset(asset)
426
+ asset.attachable_id = pk
427
+ asset.attachable_type = 'Post'
428
+ asset.save
429
+ end
430
+ def _remove_asset(asset)
431
+ asset.attachable_id = nil
432
+ asset.attachable_type = nil
433
+ asset.save
434
+ end
435
+ def _remove_all_assets
436
+ Asset.filter(:attachable_id=>pk, :attachable_type=>'Post')\
437
+ .update(:attachable_id=>nil, :attachable_type=>nil)
438
+ end
439
+ end
440
+
441
+ class ::Note < Sequel::Model
442
+ set_schema do
443
+ primary_key :id
444
+ end
445
+ create_table!
446
+ one_to_many :assets, :key=>:attachable_id do |ds|
447
+ ds.filter(:attachable_type=>'Note')
448
+ end
449
+
450
+ private
451
+
452
+ def _add_asset(asset)
453
+ asset.attachable_id = pk
454
+ asset.attachable_type = 'Note'
455
+ asset.save
456
+ end
457
+ def _remove_asset(asset)
458
+ asset.attachable_id = nil
459
+ asset.attachable_type = nil
460
+ asset.save
461
+ end
462
+ def _remove_all_assets
463
+ Asset.filter(:attachable_id=>pk, :attachable_type=>'Note')\
464
+ .update(:attachable_id=>nil, :attachable_type=>nil)
465
+ end
466
+ end
467
+ @post = Post.create
468
+ Note.create
469
+ @note = Note.create
470
+ @asset1 = Asset.create(:attachable=>@post)
471
+ @asset2 = Asset.create(:attachable=>@note)
472
+ @asset1.associations.clear
473
+ @asset2.associations.clear
474
+ clear_sqls
475
+ end
476
+ after do
477
+ Asset.drop_table
478
+ Post.drop_table
479
+ Note.drop_table
480
+ Object.send(:remove_const, :Asset)
481
+ Object.send(:remove_const, :Post)
482
+ Object.send(:remove_const, :Note)
483
+ end
484
+
485
+ it "should load the correct associated object for a single object" do
486
+ @asset1.attachable.should == @post
487
+ @asset2.attachable.should == @note
488
+ sqls_should_be("SELECT * FROM posts WHERE (id = 1) LIMIT 1", "SELECT * FROM notes WHERE (id = 2) LIMIT 1")
489
+ end
490
+
491
+ it "should eagerly load the correct associated object for a group of objects" do
492
+ assets = Asset.order(:id).eager(:attachable).all
493
+ sqls_should_be("SELECT * FROM assets ORDER BY id", "SELECT * FROM posts WHERE (id IN (1))", "SELECT * FROM notes WHERE (id IN (2))")
494
+ assets.should == [@asset1, @asset2]
495
+ assets[0].attachable.should == @post
496
+ assets[1].attachable.should == @note
497
+ sqls_should_be
498
+ end
499
+
500
+ it "should set items correctly" do
501
+ @asset1.attachable = @note
502
+ @asset2.attachable = @post
503
+ sqls_should_be("SELECT * FROM posts WHERE (id = 1) LIMIT 1", "SELECT * FROM notes WHERE (id = 2) LIMIT 1")
504
+ @asset1.attachable.should == @note
505
+ @asset1.attachable_id.should == @note.pk
506
+ @asset1.attachable_type.should == 'Note'
507
+ @asset2.attachable.should == @post
508
+ @asset2.attachable_id.should == @post.pk
509
+ @asset2.attachable_type.should == 'Post'
510
+ @asset1.attachable = nil
511
+ @asset1.attachable.should == nil
512
+ @asset1.attachable_id.should == nil
513
+ @asset1.attachable_type.should == nil
514
+ sqls_should_be
515
+ end
516
+
517
+ it "should add items correctly" do
518
+ @post.assets.should == [@asset1]
519
+ sqls_should_be("SELECT * FROM assets WHERE ((assets.attachable_id = 1) AND (attachable_type = 'Post'))")
520
+ @post.add_asset(@asset2)
521
+ sqls_should_be(/UPDATE assets SET ((attachable_id = 1|attachable_type = 'Post'|id = 2)(, )?){3} WHERE \(id = 2\)/)
522
+ @post.assets.should == [@asset1, @asset2]
523
+ @asset2.attachable.should == @post
524
+ @asset2.attachable_id.should == @post.pk
525
+ @asset2.attachable_type.should == 'Post'
526
+ sqls_should_be
527
+ end
528
+
529
+ it "should remove items correctly" do
530
+ @note.assets.should == [@asset2]
531
+ sqls_should_be("SELECT * FROM assets WHERE ((assets.attachable_id = 2) AND (attachable_type = 'Note'))")
532
+ @note.remove_asset(@asset2)
533
+ sqls_should_be(/UPDATE assets SET ((attachable_id = NULL|attachable_type = NULL|id = 2)(, )?){3} WHERE \(id = 2\)/)
534
+ @note.assets.should == []
535
+ @asset2.attachable.should == nil
536
+ @asset2.attachable_id.should == nil
537
+ @asset2.attachable_type.should == nil
538
+ sqls_should_be
539
+ end
540
+
541
+ it "should remove all items correctly" do
542
+ @post.remove_all_assets
543
+ @note.remove_all_assets
544
+ sqls_should_be(/UPDATE assets SET attachable_(id|type) = NULL, attachable_(type|id) = NULL WHERE \(\(attachable_(id|type) = (1|'Post')\) AND \(attachable_(type|id) = ('Post'|1)\)\)/,
545
+ /UPDATE assets SET attachable_(id|type) = NULL, attachable_(type|id) = NULL WHERE \(\(attachable_(id|type) = (2|'Note')\) AND \(attachable_(type|id) = ('Note'|2)\)\)/)
546
+ @asset1.reload.attachable.should == nil
547
+ @asset2.reload.attachable.should == nil
548
+ end
549
+ end
550
+
551
+ describe "many_to_one/one_to_many not referencing primary key" do
552
+ before do
553
+ class ::Client < Sequel::Model
554
+ set_schema do
555
+ primary_key :id
556
+ text :name
557
+ end
558
+ create_table!
559
+ one_to_many :invoices, :reciprocal=>:client, \
560
+ :dataset=>proc{Invoice.filter(:client_name=>name)}, \
561
+ :eager_loader=>(proc do |key_hash, clients, associations|
562
+ id_map = {}
563
+ clients.each do |client|
564
+ id_map[client.name] = client
565
+ client.associations[:invoices] = []
566
+ end
567
+ Invoice.filter(:client_name=>id_map.keys.sort).all do |inv|
568
+ inv.associations[:client] = client = id_map[inv.client_name]
569
+ client.associations[:invoices] << inv
570
+ end
571
+ end)
572
+
573
+ private
574
+
575
+ def _add_invoice(invoice)
576
+ invoice.client_name = name
577
+ invoice.save
578
+ end
579
+ def _remove_invoice(invoice)
580
+ invoice.client_name = nil
581
+ invoice.save
582
+ end
583
+ def _remove_all_invoices
584
+ Invoice.filter(:client_name=>name).update(:client_name=>nil)
585
+ end
586
+ end
587
+
588
+ class ::Invoice < Sequel::Model
589
+ set_schema do
590
+ primary_key :id
591
+ text :client_name
592
+ end
593
+ create_table!
594
+ many_to_one :client, :key=>:client_name, \
595
+ :dataset=>proc{Client.filter(:name=>client_name)}, \
596
+ :eager_loader=>(proc do |key_hash, invoices, associations|
597
+ id_map = key_hash[:client_name]
598
+ invoices.each{|inv| inv.associations[:client] = nil}
599
+ Client.filter(:name=>id_map.keys).all do |client|
600
+ id_map[client.name].each{|inv| inv.associations[:client] = client}
601
+ end
602
+ end)
603
+
604
+ private
605
+
606
+ def _client=(client)
607
+ self.client_name = (client.name if client)
608
+ end
609
+ end
610
+
611
+ @client1 = Client.create(:name=>'X')
612
+ @client2 = Client.create(:name=>'Y')
613
+ @invoice1 = Invoice.create(:client_name=>'X')
614
+ @invoice2 = Invoice.create(:client_name=>'X')
615
+ clear_sqls
616
+ end
617
+ after do
618
+ Invoice.drop_table
619
+ Client.drop_table
620
+ Object.send(:remove_const, :Client)
621
+ Object.send(:remove_const, :Invoice)
622
+ end
623
+
624
+ it "should load all associated one_to_many objects for a single object" do
625
+ invs = @client1.invoices
626
+ sqls_should_be("SELECT * FROM invoices WHERE (client_name = 'X')")
627
+ invs.should == [@invoice1, @invoice2]
628
+ invs[0].client.should == @client1
629
+ invs[1].client.should == @client1
630
+ sqls_should_be
631
+ end
632
+
633
+ it "should load the associated many_to_one object for a single object" do
634
+ client = @invoice1.client
635
+ sqls_should_be("SELECT * FROM clients WHERE (name = 'X') LIMIT 1")
636
+ client.should == @client1
637
+ end
638
+
639
+ it "should eagerly load all associated one_to_many objects for a group of objects" do
640
+ clients = Client.order(:id).eager(:invoices).all
641
+ sqls_should_be("SELECT * FROM clients ORDER BY id", "SELECT * FROM invoices WHERE (client_name IN ('X', 'Y'))")
642
+ clients.should == [@client1, @client2]
643
+ clients[1].invoices.should == []
644
+ invs = clients[0].invoices.sort_by{|x| x.pk}
645
+ invs.should == [@invoice1, @invoice2]
646
+ invs[0].client.should == @client1
647
+ invs[1].client.should == @client1
648
+ sqls_should_be
649
+ end
650
+
651
+ it "should eagerly load the associated many_to_one object for a group of objects" do
652
+ invoices = Invoice.order(:id).eager(:client).all
653
+ sqls_should_be("SELECT * FROM invoices ORDER BY id", "SELECT * FROM clients WHERE (name IN ('X'))")
654
+ invoices.should == [@invoice1, @invoice2]
655
+ invoices[0].client.should == @client1
656
+ invoices[1].client.should == @client1
657
+ sqls_should_be
658
+ end
659
+
660
+ it "should set the associated object correctly" do
661
+ @invoice1.client = @client2
662
+ sqls_should_be("SELECT * FROM clients WHERE (name = 'X') LIMIT 1")
663
+ @invoice1.client.should == @client2
664
+ @invoice1.client_name.should == 'Y'
665
+ @invoice1.client = nil
666
+ @invoice1.client_name.should == nil
667
+ sqls_should_be
668
+ end
669
+
670
+ it "should add the associated object correctly" do
671
+ @client2.invoices.should == []
672
+ sqls_should_be("SELECT * FROM invoices WHERE (client_name = 'Y')")
673
+ @client2.add_invoice(@invoice1)
674
+ sqls_should_be(/UPDATE invoices SET (client_name = 'Y'|id = 1), (client_name = 'Y'|id = 1) WHERE \(id = 1\)/)
675
+ @client2.invoices.should == [@invoice1]
676
+ @invoice1.client_name.should == 'Y'
677
+ @invoice1.client = nil
678
+ @invoice1.client_name.should == nil
679
+ sqls_should_be
680
+ end
681
+
682
+ it "should remove the associated object correctly" do
683
+ invs = @client1.invoices.sort_by{|x| x.pk}
684
+ sqls_should_be("SELECT * FROM invoices WHERE (client_name = 'X')")
685
+ invs.should == [@invoice1, @invoice2]
686
+ @client1.remove_invoice(@invoice1)
687
+ sqls_should_be(/UPDATE invoices SET (client_name = NULL|id = 1), (client_name = NULL|id = 1) WHERE \(id = 1\)/)
688
+ @client1.invoices.should == [@invoice2]
689
+ @invoice1.client_name.should == nil
690
+ @invoice1.client.should == nil
691
+ sqls_should_be
692
+ end
693
+
694
+ it "should remove all associated objects correctly" do
695
+ invs = @client1.remove_all_invoices
696
+ sqls_should_be("UPDATE invoices SET client_name = NULL WHERE (client_name = 'X')")
697
+ @invoice1.refresh.client.should == nil
698
+ @invoice1.client_name.should == nil
699
+ @invoice2.refresh.client.should == nil
700
+ @invoice2.client_name.should == nil
701
+ end
702
+ end