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.
- data/CHANGELOG +1551 -4
- data/README +306 -19
- data/Rakefile +84 -56
- data/bin/sequel +106 -0
- data/doc/cheat_sheet.rdoc +225 -0
- data/doc/dataset_filtering.rdoc +182 -0
- data/lib/sequel_core.rb +136 -0
- data/lib/sequel_core/adapters/adapter_skeleton.rb +54 -0
- data/lib/sequel_core/adapters/ado.rb +80 -0
- data/lib/sequel_core/adapters/db2.rb +148 -0
- data/lib/sequel_core/adapters/dbi.rb +117 -0
- data/lib/sequel_core/adapters/informix.rb +78 -0
- data/lib/sequel_core/adapters/jdbc.rb +186 -0
- data/lib/sequel_core/adapters/jdbc/mysql.rb +55 -0
- data/lib/sequel_core/adapters/jdbc/postgresql.rb +66 -0
- data/lib/sequel_core/adapters/jdbc/sqlite.rb +47 -0
- data/lib/sequel_core/adapters/mysql.rb +231 -0
- data/lib/sequel_core/adapters/odbc.rb +155 -0
- data/lib/sequel_core/adapters/odbc_mssql.rb +106 -0
- data/lib/sequel_core/adapters/openbase.rb +64 -0
- data/lib/sequel_core/adapters/oracle.rb +170 -0
- data/lib/sequel_core/adapters/postgres.rb +199 -0
- data/lib/sequel_core/adapters/shared/mysql.rb +275 -0
- data/lib/sequel_core/adapters/shared/postgres.rb +351 -0
- data/lib/sequel_core/adapters/shared/sqlite.rb +146 -0
- data/lib/sequel_core/adapters/sqlite.rb +138 -0
- data/lib/sequel_core/connection_pool.rb +194 -0
- data/lib/sequel_core/core_ext.rb +203 -0
- data/lib/sequel_core/core_sql.rb +184 -0
- data/lib/sequel_core/database.rb +471 -0
- data/lib/sequel_core/database/schema.rb +156 -0
- data/lib/sequel_core/dataset.rb +457 -0
- data/lib/sequel_core/dataset/callback.rb +13 -0
- data/lib/sequel_core/dataset/convenience.rb +245 -0
- data/lib/sequel_core/dataset/pagination.rb +96 -0
- data/lib/sequel_core/dataset/query.rb +41 -0
- data/lib/sequel_core/dataset/schema.rb +15 -0
- data/lib/sequel_core/dataset/sql.rb +889 -0
- data/lib/sequel_core/deprecated.rb +26 -0
- data/lib/sequel_core/exceptions.rb +42 -0
- data/lib/sequel_core/migration.rb +187 -0
- data/lib/sequel_core/object_graph.rb +216 -0
- data/lib/sequel_core/pretty_table.rb +71 -0
- data/lib/sequel_core/schema.rb +2 -0
- data/lib/sequel_core/schema/generator.rb +239 -0
- data/lib/sequel_core/schema/sql.rb +325 -0
- data/lib/sequel_core/sql.rb +812 -0
- data/lib/sequel_model.rb +5 -1
- data/lib/sequel_model/association_reflection.rb +3 -8
- data/lib/sequel_model/base.rb +15 -10
- data/lib/sequel_model/inflector.rb +3 -5
- data/lib/sequel_model/plugins.rb +1 -1
- data/lib/sequel_model/record.rb +11 -3
- data/lib/sequel_model/schema.rb +4 -4
- data/lib/sequel_model/validations.rb +6 -1
- data/spec/adapters/ado_spec.rb +17 -0
- data/spec/adapters/informix_spec.rb +96 -0
- data/spec/adapters/mysql_spec.rb +764 -0
- data/spec/adapters/oracle_spec.rb +222 -0
- data/spec/adapters/postgres_spec.rb +441 -0
- data/spec/adapters/spec_helper.rb +7 -0
- data/spec/adapters/sqlite_spec.rb +400 -0
- data/spec/integration/dataset_test.rb +51 -0
- data/spec/integration/eager_loader_test.rb +702 -0
- data/spec/integration/schema_test.rb +102 -0
- data/spec/integration/spec_helper.rb +44 -0
- data/spec/integration/type_test.rb +43 -0
- data/spec/rcov.opts +2 -0
- data/spec/sequel_core/connection_pool_spec.rb +363 -0
- data/spec/sequel_core/core_ext_spec.rb +156 -0
- data/spec/sequel_core/core_sql_spec.rb +427 -0
- data/spec/sequel_core/database_spec.rb +964 -0
- data/spec/sequel_core/dataset_spec.rb +2977 -0
- data/spec/sequel_core/expression_filters_spec.rb +346 -0
- data/spec/sequel_core/migration_spec.rb +261 -0
- data/spec/sequel_core/object_graph_spec.rb +234 -0
- data/spec/sequel_core/pretty_table_spec.rb +58 -0
- data/spec/sequel_core/schema_generator_spec.rb +122 -0
- data/spec/sequel_core/schema_spec.rb +497 -0
- data/spec/sequel_core/spec_helper.rb +51 -0
- data/spec/{association_reflection_spec.rb → sequel_model/association_reflection_spec.rb} +6 -6
- data/spec/{associations_spec.rb → sequel_model/associations_spec.rb} +47 -18
- data/spec/{base_spec.rb → sequel_model/base_spec.rb} +2 -1
- data/spec/{caching_spec.rb → sequel_model/caching_spec.rb} +0 -0
- data/spec/{dataset_methods_spec.rb → sequel_model/dataset_methods_spec.rb} +13 -1
- data/spec/{eager_loading_spec.rb → sequel_model/eager_loading_spec.rb} +75 -14
- data/spec/{hooks_spec.rb → sequel_model/hooks_spec.rb} +4 -4
- data/spec/sequel_model/inflector_spec.rb +119 -0
- data/spec/{model_spec.rb → sequel_model/model_spec.rb} +30 -11
- data/spec/{plugins_spec.rb → sequel_model/plugins_spec.rb} +0 -0
- data/spec/{record_spec.rb → sequel_model/record_spec.rb} +47 -6
- data/spec/{schema_spec.rb → sequel_model/schema_spec.rb} +18 -4
- data/spec/{spec_helper.rb → sequel_model/spec_helper.rb} +3 -2
- data/spec/{validations_spec.rb → sequel_model/validations_spec.rb} +37 -17
- data/spec/spec_config.rb +9 -0
- data/spec/spec_config.rb.example +10 -0
- metadata +110 -37
- 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
|