colincasey-sequel 2.10.0 → 2.10.1

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