sequel 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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