viking-sequel 3.10.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 (237) hide show
  1. data/CHANGELOG +3134 -0
  2. data/COPYING +19 -0
  3. data/README.rdoc +723 -0
  4. data/Rakefile +193 -0
  5. data/bin/sequel +196 -0
  6. data/doc/advanced_associations.rdoc +644 -0
  7. data/doc/cheat_sheet.rdoc +218 -0
  8. data/doc/dataset_basics.rdoc +106 -0
  9. data/doc/dataset_filtering.rdoc +158 -0
  10. data/doc/opening_databases.rdoc +296 -0
  11. data/doc/prepared_statements.rdoc +104 -0
  12. data/doc/reflection.rdoc +84 -0
  13. data/doc/release_notes/1.0.txt +38 -0
  14. data/doc/release_notes/1.1.txt +143 -0
  15. data/doc/release_notes/1.3.txt +101 -0
  16. data/doc/release_notes/1.4.0.txt +53 -0
  17. data/doc/release_notes/1.5.0.txt +155 -0
  18. data/doc/release_notes/2.0.0.txt +298 -0
  19. data/doc/release_notes/2.1.0.txt +271 -0
  20. data/doc/release_notes/2.10.0.txt +328 -0
  21. data/doc/release_notes/2.11.0.txt +215 -0
  22. data/doc/release_notes/2.12.0.txt +534 -0
  23. data/doc/release_notes/2.2.0.txt +253 -0
  24. data/doc/release_notes/2.3.0.txt +88 -0
  25. data/doc/release_notes/2.4.0.txt +106 -0
  26. data/doc/release_notes/2.5.0.txt +137 -0
  27. data/doc/release_notes/2.6.0.txt +157 -0
  28. data/doc/release_notes/2.7.0.txt +166 -0
  29. data/doc/release_notes/2.8.0.txt +171 -0
  30. data/doc/release_notes/2.9.0.txt +97 -0
  31. data/doc/release_notes/3.0.0.txt +221 -0
  32. data/doc/release_notes/3.1.0.txt +406 -0
  33. data/doc/release_notes/3.10.0.txt +286 -0
  34. data/doc/release_notes/3.2.0.txt +268 -0
  35. data/doc/release_notes/3.3.0.txt +192 -0
  36. data/doc/release_notes/3.4.0.txt +325 -0
  37. data/doc/release_notes/3.5.0.txt +510 -0
  38. data/doc/release_notes/3.6.0.txt +366 -0
  39. data/doc/release_notes/3.7.0.txt +179 -0
  40. data/doc/release_notes/3.8.0.txt +151 -0
  41. data/doc/release_notes/3.9.0.txt +233 -0
  42. data/doc/schema.rdoc +36 -0
  43. data/doc/sharding.rdoc +113 -0
  44. data/doc/virtual_rows.rdoc +205 -0
  45. data/lib/sequel.rb +1 -0
  46. data/lib/sequel/adapters/ado.rb +90 -0
  47. data/lib/sequel/adapters/ado/mssql.rb +30 -0
  48. data/lib/sequel/adapters/amalgalite.rb +176 -0
  49. data/lib/sequel/adapters/db2.rb +139 -0
  50. data/lib/sequel/adapters/dbi.rb +113 -0
  51. data/lib/sequel/adapters/do.rb +188 -0
  52. data/lib/sequel/adapters/do/mysql.rb +49 -0
  53. data/lib/sequel/adapters/do/postgres.rb +91 -0
  54. data/lib/sequel/adapters/do/sqlite.rb +40 -0
  55. data/lib/sequel/adapters/firebird.rb +283 -0
  56. data/lib/sequel/adapters/informix.rb +77 -0
  57. data/lib/sequel/adapters/jdbc.rb +587 -0
  58. data/lib/sequel/adapters/jdbc/as400.rb +58 -0
  59. data/lib/sequel/adapters/jdbc/h2.rb +133 -0
  60. data/lib/sequel/adapters/jdbc/mssql.rb +57 -0
  61. data/lib/sequel/adapters/jdbc/mysql.rb +78 -0
  62. data/lib/sequel/adapters/jdbc/oracle.rb +50 -0
  63. data/lib/sequel/adapters/jdbc/postgresql.rb +108 -0
  64. data/lib/sequel/adapters/jdbc/sqlite.rb +55 -0
  65. data/lib/sequel/adapters/mysql.rb +421 -0
  66. data/lib/sequel/adapters/odbc.rb +143 -0
  67. data/lib/sequel/adapters/odbc/mssql.rb +42 -0
  68. data/lib/sequel/adapters/openbase.rb +64 -0
  69. data/lib/sequel/adapters/oracle.rb +131 -0
  70. data/lib/sequel/adapters/postgres.rb +504 -0
  71. data/lib/sequel/adapters/shared/mssql.rb +490 -0
  72. data/lib/sequel/adapters/shared/mysql.rb +498 -0
  73. data/lib/sequel/adapters/shared/oracle.rb +195 -0
  74. data/lib/sequel/adapters/shared/postgres.rb +830 -0
  75. data/lib/sequel/adapters/shared/progress.rb +44 -0
  76. data/lib/sequel/adapters/shared/sqlite.rb +389 -0
  77. data/lib/sequel/adapters/sqlite.rb +224 -0
  78. data/lib/sequel/adapters/utils/stored_procedures.rb +84 -0
  79. data/lib/sequel/connection_pool.rb +99 -0
  80. data/lib/sequel/connection_pool/sharded_single.rb +84 -0
  81. data/lib/sequel/connection_pool/sharded_threaded.rb +211 -0
  82. data/lib/sequel/connection_pool/single.rb +29 -0
  83. data/lib/sequel/connection_pool/threaded.rb +150 -0
  84. data/lib/sequel/core.rb +293 -0
  85. data/lib/sequel/core_sql.rb +241 -0
  86. data/lib/sequel/database.rb +1079 -0
  87. data/lib/sequel/database/schema_generator.rb +327 -0
  88. data/lib/sequel/database/schema_methods.rb +203 -0
  89. data/lib/sequel/database/schema_sql.rb +320 -0
  90. data/lib/sequel/dataset.rb +32 -0
  91. data/lib/sequel/dataset/actions.rb +441 -0
  92. data/lib/sequel/dataset/features.rb +86 -0
  93. data/lib/sequel/dataset/graph.rb +254 -0
  94. data/lib/sequel/dataset/misc.rb +119 -0
  95. data/lib/sequel/dataset/mutation.rb +64 -0
  96. data/lib/sequel/dataset/prepared_statements.rb +227 -0
  97. data/lib/sequel/dataset/query.rb +709 -0
  98. data/lib/sequel/dataset/sql.rb +996 -0
  99. data/lib/sequel/exceptions.rb +51 -0
  100. data/lib/sequel/extensions/blank.rb +43 -0
  101. data/lib/sequel/extensions/inflector.rb +242 -0
  102. data/lib/sequel/extensions/looser_typecasting.rb +21 -0
  103. data/lib/sequel/extensions/migration.rb +239 -0
  104. data/lib/sequel/extensions/named_timezones.rb +61 -0
  105. data/lib/sequel/extensions/pagination.rb +100 -0
  106. data/lib/sequel/extensions/pretty_table.rb +82 -0
  107. data/lib/sequel/extensions/query.rb +52 -0
  108. data/lib/sequel/extensions/schema_dumper.rb +271 -0
  109. data/lib/sequel/extensions/sql_expr.rb +122 -0
  110. data/lib/sequel/extensions/string_date_time.rb +46 -0
  111. data/lib/sequel/extensions/thread_local_timezones.rb +48 -0
  112. data/lib/sequel/metaprogramming.rb +9 -0
  113. data/lib/sequel/model.rb +120 -0
  114. data/lib/sequel/model/associations.rb +1514 -0
  115. data/lib/sequel/model/base.rb +1069 -0
  116. data/lib/sequel/model/default_inflections.rb +45 -0
  117. data/lib/sequel/model/errors.rb +39 -0
  118. data/lib/sequel/model/exceptions.rb +21 -0
  119. data/lib/sequel/model/inflections.rb +162 -0
  120. data/lib/sequel/model/plugins.rb +70 -0
  121. data/lib/sequel/plugins/active_model.rb +59 -0
  122. data/lib/sequel/plugins/association_dependencies.rb +103 -0
  123. data/lib/sequel/plugins/association_proxies.rb +41 -0
  124. data/lib/sequel/plugins/boolean_readers.rb +53 -0
  125. data/lib/sequel/plugins/caching.rb +141 -0
  126. data/lib/sequel/plugins/class_table_inheritance.rb +214 -0
  127. data/lib/sequel/plugins/composition.rb +138 -0
  128. data/lib/sequel/plugins/force_encoding.rb +72 -0
  129. data/lib/sequel/plugins/hook_class_methods.rb +126 -0
  130. data/lib/sequel/plugins/identity_map.rb +116 -0
  131. data/lib/sequel/plugins/instance_filters.rb +98 -0
  132. data/lib/sequel/plugins/instance_hooks.rb +57 -0
  133. data/lib/sequel/plugins/lazy_attributes.rb +77 -0
  134. data/lib/sequel/plugins/many_through_many.rb +208 -0
  135. data/lib/sequel/plugins/nested_attributes.rb +206 -0
  136. data/lib/sequel/plugins/optimistic_locking.rb +81 -0
  137. data/lib/sequel/plugins/rcte_tree.rb +281 -0
  138. data/lib/sequel/plugins/schema.rb +66 -0
  139. data/lib/sequel/plugins/serialization.rb +166 -0
  140. data/lib/sequel/plugins/single_table_inheritance.rb +74 -0
  141. data/lib/sequel/plugins/subclasses.rb +45 -0
  142. data/lib/sequel/plugins/tactical_eager_loading.rb +61 -0
  143. data/lib/sequel/plugins/timestamps.rb +87 -0
  144. data/lib/sequel/plugins/touch.rb +118 -0
  145. data/lib/sequel/plugins/typecast_on_load.rb +72 -0
  146. data/lib/sequel/plugins/validation_class_methods.rb +405 -0
  147. data/lib/sequel/plugins/validation_helpers.rb +223 -0
  148. data/lib/sequel/sql.rb +1020 -0
  149. data/lib/sequel/timezones.rb +161 -0
  150. data/lib/sequel/version.rb +12 -0
  151. data/lib/sequel_core.rb +1 -0
  152. data/lib/sequel_model.rb +1 -0
  153. data/spec/adapters/firebird_spec.rb +407 -0
  154. data/spec/adapters/informix_spec.rb +97 -0
  155. data/spec/adapters/mssql_spec.rb +403 -0
  156. data/spec/adapters/mysql_spec.rb +1019 -0
  157. data/spec/adapters/oracle_spec.rb +286 -0
  158. data/spec/adapters/postgres_spec.rb +969 -0
  159. data/spec/adapters/spec_helper.rb +51 -0
  160. data/spec/adapters/sqlite_spec.rb +432 -0
  161. data/spec/core/connection_pool_spec.rb +808 -0
  162. data/spec/core/core_sql_spec.rb +417 -0
  163. data/spec/core/database_spec.rb +1662 -0
  164. data/spec/core/dataset_spec.rb +3827 -0
  165. data/spec/core/expression_filters_spec.rb +595 -0
  166. data/spec/core/object_graph_spec.rb +296 -0
  167. data/spec/core/schema_generator_spec.rb +159 -0
  168. data/spec/core/schema_spec.rb +830 -0
  169. data/spec/core/spec_helper.rb +56 -0
  170. data/spec/core/version_spec.rb +7 -0
  171. data/spec/extensions/active_model_spec.rb +76 -0
  172. data/spec/extensions/association_dependencies_spec.rb +127 -0
  173. data/spec/extensions/association_proxies_spec.rb +50 -0
  174. data/spec/extensions/blank_spec.rb +67 -0
  175. data/spec/extensions/boolean_readers_spec.rb +92 -0
  176. data/spec/extensions/caching_spec.rb +250 -0
  177. data/spec/extensions/class_table_inheritance_spec.rb +252 -0
  178. data/spec/extensions/composition_spec.rb +194 -0
  179. data/spec/extensions/force_encoding_spec.rb +117 -0
  180. data/spec/extensions/hook_class_methods_spec.rb +470 -0
  181. data/spec/extensions/identity_map_spec.rb +202 -0
  182. data/spec/extensions/inflector_spec.rb +181 -0
  183. data/spec/extensions/instance_filters_spec.rb +55 -0
  184. data/spec/extensions/instance_hooks_spec.rb +133 -0
  185. data/spec/extensions/lazy_attributes_spec.rb +153 -0
  186. data/spec/extensions/looser_typecasting_spec.rb +39 -0
  187. data/spec/extensions/many_through_many_spec.rb +884 -0
  188. data/spec/extensions/migration_spec.rb +332 -0
  189. data/spec/extensions/named_timezones_spec.rb +72 -0
  190. data/spec/extensions/nested_attributes_spec.rb +396 -0
  191. data/spec/extensions/optimistic_locking_spec.rb +100 -0
  192. data/spec/extensions/pagination_spec.rb +99 -0
  193. data/spec/extensions/pretty_table_spec.rb +91 -0
  194. data/spec/extensions/query_spec.rb +85 -0
  195. data/spec/extensions/rcte_tree_spec.rb +205 -0
  196. data/spec/extensions/schema_dumper_spec.rb +357 -0
  197. data/spec/extensions/schema_spec.rb +127 -0
  198. data/spec/extensions/serialization_spec.rb +209 -0
  199. data/spec/extensions/single_table_inheritance_spec.rb +96 -0
  200. data/spec/extensions/spec_helper.rb +91 -0
  201. data/spec/extensions/sql_expr_spec.rb +89 -0
  202. data/spec/extensions/string_date_time_spec.rb +93 -0
  203. data/spec/extensions/subclasses_spec.rb +52 -0
  204. data/spec/extensions/tactical_eager_loading_spec.rb +65 -0
  205. data/spec/extensions/thread_local_timezones_spec.rb +45 -0
  206. data/spec/extensions/timestamps_spec.rb +150 -0
  207. data/spec/extensions/touch_spec.rb +155 -0
  208. data/spec/extensions/typecast_on_load_spec.rb +69 -0
  209. data/spec/extensions/validation_class_methods_spec.rb +984 -0
  210. data/spec/extensions/validation_helpers_spec.rb +438 -0
  211. data/spec/integration/associations_test.rb +281 -0
  212. data/spec/integration/database_test.rb +26 -0
  213. data/spec/integration/dataset_test.rb +963 -0
  214. data/spec/integration/eager_loader_test.rb +734 -0
  215. data/spec/integration/model_test.rb +130 -0
  216. data/spec/integration/plugin_test.rb +814 -0
  217. data/spec/integration/prepared_statement_test.rb +213 -0
  218. data/spec/integration/schema_test.rb +361 -0
  219. data/spec/integration/spec_helper.rb +73 -0
  220. data/spec/integration/timezone_test.rb +55 -0
  221. data/spec/integration/transaction_test.rb +122 -0
  222. data/spec/integration/type_test.rb +96 -0
  223. data/spec/model/association_reflection_spec.rb +175 -0
  224. data/spec/model/associations_spec.rb +2633 -0
  225. data/spec/model/base_spec.rb +418 -0
  226. data/spec/model/dataset_methods_spec.rb +78 -0
  227. data/spec/model/eager_loading_spec.rb +1391 -0
  228. data/spec/model/hooks_spec.rb +240 -0
  229. data/spec/model/inflector_spec.rb +26 -0
  230. data/spec/model/model_spec.rb +593 -0
  231. data/spec/model/plugins_spec.rb +236 -0
  232. data/spec/model/record_spec.rb +1500 -0
  233. data/spec/model/spec_helper.rb +97 -0
  234. data/spec/model/validations_spec.rb +153 -0
  235. data/spec/rcov.opts +6 -0
  236. data/spec/spec_config.rb.example +10 -0
  237. metadata +346 -0
@@ -0,0 +1,734 @@
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, {})
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
+ INTEGRATION_DB.drop_table :nodes
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
+ nodes.length.should == 1
79
+ node = nodes.first
80
+ node.pk.should == 1
81
+ node.children.length.should == 2
82
+ node.children.collect{|x| x.pk}.sort.should == [2, 3]
83
+ node.children.collect{|x| x.parent}.should == [node, node]
84
+ node = nodes.first.children.find{|x| x.pk == 2}
85
+ node.children.length.should == 1
86
+ node.children.first.pk.should == 4
87
+ node.children.first.parent.should == node
88
+ node = node.children.first
89
+ node.children.length.should == 1
90
+ node.children.first.pk.should == 5
91
+ node.children.first.parent.should == node
92
+ node = node.children.first
93
+ node.children.length.should == 1
94
+ node.children.first.pk.should == 6
95
+ node.children.first.parent.should == node
96
+ node = node.children.first
97
+ node.children.length.should == 1
98
+ node.children.first.pk.should == 7
99
+ node.children.first.parent.should == node
100
+ end
101
+
102
+ it "#ancestors should get all ancestors in one call" do
103
+ nodes = Node.filter(:id=>[7,3]).order(:id).eager(:ancestors).all
104
+ nodes.length.should == 2
105
+ nodes.collect{|x| x.pk}.should == [3, 7]
106
+ nodes.first.parent.pk.should == 1
107
+ nodes.first.parent.parent.should == nil
108
+ node = nodes.last
109
+ node.parent.pk.should == 6
110
+ node = node.parent
111
+ node.parent.pk.should == 5
112
+ node = node.parent
113
+ node.parent.pk.should == 4
114
+ node = node.parent
115
+ node.parent.pk.should == 2
116
+ node = node.parent
117
+ node.parent.pk.should == 1
118
+ node.parent.parent.should == nil
119
+ end
120
+ end
121
+
122
+ describe "Association Extensions" do
123
+ before do
124
+ module ::FindOrCreate
125
+ def find_or_create(vals)
126
+ first(vals) || model.create(vals.merge(:author_id=>model_object.pk))
127
+ end
128
+ def find_or_create_by_name(name)
129
+ first(:name=>name) || model.create(:name=>name, :author_id=>model_object.pk)
130
+ end
131
+ end
132
+ INTEGRATION_DB.instance_variable_set(:@schemas, {})
133
+ INTEGRATION_DB.create_table!(:authors) do
134
+ primary_key :id
135
+ end
136
+ class ::Author < Sequel::Model
137
+ one_to_many :authorships, :extend=>FindOrCreate
138
+ end
139
+ INTEGRATION_DB.create_table!(:authorships) do
140
+ primary_key :id
141
+ foreign_key :author_id, :authors
142
+ String :name
143
+ end
144
+ class ::Authorship < Sequel::Model
145
+ many_to_one :author
146
+ end
147
+ @author = Author.create
148
+ clear_sqls
149
+ end
150
+ after do
151
+ INTEGRATION_DB.drop_table :authorships, :authors
152
+ Object.send(:remove_const, :Author)
153
+ Object.send(:remove_const, :Authorship)
154
+ end
155
+
156
+ it "should allow methods to be called on the dataset method" do
157
+ Authorship.count.should == 0
158
+ authorship = @author.authorships_dataset.find_or_create_by_name('Bob')
159
+ Authorship.count.should == 1
160
+ Authorship.first.should == authorship
161
+ authorship.name.should == 'Bob'
162
+ authorship.author_id.should == @author.id
163
+ @author.authorships_dataset.find_or_create_by_name('Bob').should == authorship
164
+ Authorship.count.should == 1
165
+ authorship2 = @author.authorships_dataset.find_or_create(:name=>'Jim')
166
+ Authorship.count.should == 2
167
+ Authorship.order(:name).map(:name).should == ['Bob', 'Jim']
168
+ authorship2.name.should == 'Jim'
169
+ authorship2.author_id.should == @author.id
170
+ @author.authorships_dataset.find_or_create(:name=>'Jim').should == authorship2
171
+ end
172
+ end
173
+
174
+ describe "has_many :through has_many and has_one :through belongs_to" do
175
+ before do
176
+ INTEGRATION_DB.instance_variable_set(:@schemas, {})
177
+ INTEGRATION_DB.create_table!(:firms) do
178
+ primary_key :id
179
+ end
180
+ class ::Firm < Sequel::Model
181
+ one_to_many :clients
182
+ one_to_many :invoices, :read_only=>true, \
183
+ :dataset=>proc{Invoice.eager_graph(:client).filter(:client__firm_id=>pk)}, \
184
+ :after_load=>(proc do |firm, invs|
185
+ invs.each do |inv|
186
+ inv.client.associations[:firm] = inv.associations[:firm] = firm
187
+ end
188
+ end), \
189
+ :eager_loader=>(proc do |key_hash, firms, associations|
190
+ id_map = key_hash[Firm.primary_key]
191
+ firms.each{|firm| firm.associations[:invoices] = []}
192
+ Invoice.eager_graph(:client).filter(:client__firm_id=>id_map.keys).all do |inv|
193
+ id_map[inv.client.firm_id].each do |firm|
194
+ firm.associations[:invoices] << inv
195
+ end
196
+ end
197
+ end)
198
+ end
199
+
200
+ INTEGRATION_DB.create_table!(:clients) do
201
+ primary_key :id
202
+ foreign_key :firm_id, :firms
203
+ end
204
+ class ::Client < Sequel::Model
205
+ many_to_one :firm
206
+ one_to_many :invoices
207
+ end
208
+
209
+ INTEGRATION_DB.create_table!(:invoices) do
210
+ primary_key :id
211
+ foreign_key :client_id, :clients
212
+ end
213
+ class ::Invoice < Sequel::Model
214
+ many_to_one :client
215
+ many_to_one :firm, :key=>nil, :read_only=>true, \
216
+ :dataset=>proc{Firm.eager_graph(:clients).filter(:clients__id=>client_id)}, \
217
+ :after_load=>(proc do |inv, firm|
218
+ # Delete the cached associations from firm, because it only has the
219
+ # client with this invoice, instead of all clients of the firm
220
+ if c = firm.associations.delete(:clients)
221
+ firm.associations[:invoice_client] = c.first
222
+ end
223
+ inv.associations[:client] ||= firm.associations[:invoice_client]
224
+ end), \
225
+ :eager_loader=>(proc do |key_hash, invoices, associations|
226
+ id_map = {}
227
+ invoices.each do |inv|
228
+ inv.associations[:firm] = nil
229
+ (id_map[inv.client_id] ||= []) << inv
230
+ end
231
+ Firm.eager_graph(:clients).filter(:clients__id=>id_map.keys).all do |firm|
232
+ # Delete the cached associations from firm, because it only has the
233
+ # clients related the invoices being eagerly loaded, instead of all
234
+ # clients of the firm.
235
+ firm.associations[:clients].each do |client|
236
+ id_map[client.pk].each do |inv|
237
+ inv.associations[:firm] = firm
238
+ inv.associations[:client] = client
239
+ end
240
+ end
241
+ end
242
+ end)
243
+ end
244
+ @firm1 = Firm.create
245
+ @firm2 = Firm.create
246
+ @client1 = Client.create(:firm => @firm1)
247
+ @client2 = Client.create(:firm => @firm1)
248
+ @client3 = Client.create(:firm => @firm2)
249
+ @invoice1 = Invoice.create(:client => @client1)
250
+ @invoice2 = Invoice.create(:client => @client1)
251
+ @invoice3 = Invoice.create(:client => @client2)
252
+ @invoice4 = Invoice.create(:client => @client3)
253
+ @invoice5 = Invoice.create(:client => @client3)
254
+ clear_sqls
255
+ end
256
+ after do
257
+ INTEGRATION_DB.drop_table :invoices, :clients, :firms
258
+ Object.send(:remove_const, :Firm)
259
+ Object.send(:remove_const, :Client)
260
+ Object.send(:remove_const, :Invoice)
261
+ end
262
+
263
+ it "should return has_many :through has_many records for a single object" do
264
+ invs = @firm1.invoices.sort_by{|x| x.pk}
265
+ invs.should == [@invoice1, @invoice2, @invoice3]
266
+ invs[0].client.should == @client1
267
+ invs[1].client.should == @client1
268
+ invs[2].client.should == @client2
269
+ invs.collect{|i| i.firm}.should == [@firm1, @firm1, @firm1]
270
+ invs.collect{|i| i.client.firm}.should == [@firm1, @firm1, @firm1]
271
+ end
272
+
273
+ it "should eagerly load has_many :through has_many records for multiple objects" do
274
+ firms = Firm.order(:id).eager(:invoices).all
275
+ firms.should == [@firm1, @firm2]
276
+ firm1, firm2 = firms
277
+ invs1 = firm1.invoices.sort_by{|x| x.pk}
278
+ invs2 = firm2.invoices.sort_by{|x| x.pk}
279
+ invs1.should == [@invoice1, @invoice2, @invoice3]
280
+ invs2.should == [@invoice4, @invoice5]
281
+ invs1[0].client.should == @client1
282
+ invs1[1].client.should == @client1
283
+ invs1[2].client.should == @client2
284
+ invs2[0].client.should == @client3
285
+ invs2[1].client.should == @client3
286
+ invs1.collect{|i| i.firm}.should == [@firm1, @firm1, @firm1]
287
+ invs2.collect{|i| i.firm}.should == [@firm2, @firm2]
288
+ invs1.collect{|i| i.client.firm}.should == [@firm1, @firm1, @firm1]
289
+ invs2.collect{|i| i.client.firm}.should == [@firm2, @firm2]
290
+ end
291
+
292
+ it "should return has_one :through belongs_to records for a single object" do
293
+ firm = @invoice1.firm
294
+ firm.should == @firm1
295
+ @invoice1.client.should == @client1
296
+ @invoice1.client.firm.should == @firm1
297
+ firm.associations[:clients].should == nil
298
+ end
299
+
300
+ it "should eagerly load has_one :through belongs_to records for multiple objects" do
301
+ invs = Invoice.order(:id).eager(:firm).all
302
+ invs.should == [@invoice1, @invoice2, @invoice3, @invoice4, @invoice5]
303
+ invs[0].firm.should == @firm1
304
+ invs[0].client.should == @client1
305
+ invs[0].client.firm.should == @firm1
306
+ invs[0].firm.associations[:clients].should == nil
307
+ invs[1].firm.should == @firm1
308
+ invs[1].client.should == @client1
309
+ invs[1].client.firm.should == @firm1
310
+ invs[1].firm.associations[:clients].should == nil
311
+ invs[2].firm.should == @firm1
312
+ invs[2].client.should == @client2
313
+ invs[2].client.firm.should == @firm1
314
+ invs[2].firm.associations[:clients].should == nil
315
+ invs[3].firm.should == @firm2
316
+ invs[3].client.should == @client3
317
+ invs[3].client.firm.should == @firm2
318
+ invs[3].firm.associations[:clients].should == nil
319
+ invs[4].firm.should == @firm2
320
+ invs[4].client.should == @client3
321
+ invs[4].client.firm.should == @firm2
322
+ invs[4].firm.associations[:clients].should == nil
323
+ end
324
+ end
325
+
326
+ describe "Polymorphic Associations" do
327
+ before do
328
+ INTEGRATION_DB.instance_variable_set(:@schemas, {})
329
+ INTEGRATION_DB.create_table!(:assets) do
330
+ primary_key :id
331
+ Integer :attachable_id
332
+ String :attachable_type
333
+ end
334
+ class ::Asset < Sequel::Model
335
+ m = method(:constantize)
336
+ many_to_one :attachable, :reciprocal=>:assets, \
337
+ :dataset=>(proc do
338
+ klass = m.call(attachable_type)
339
+ klass.filter(klass.primary_key=>attachable_id)
340
+ end), \
341
+ :eager_loader=>(proc do |key_hash, assets, associations|
342
+ id_map = {}
343
+ assets.each do |asset|
344
+ asset.associations[:attachable] = nil
345
+ ((id_map[asset.attachable_type] ||= {})[asset.attachable_id] ||= []) << asset
346
+ end
347
+ id_map.each do |klass_name, id_map|
348
+ klass = m.call(klass_name)
349
+ klass.filter(klass.primary_key=>id_map.keys).all do |attach|
350
+ id_map[attach.pk].each do |asset|
351
+ asset.associations[:attachable] = attach
352
+ end
353
+ end
354
+ end
355
+ end)
356
+
357
+ private
358
+
359
+ def _attachable=(attachable)
360
+ self[:attachable_id] = (attachable.pk if attachable)
361
+ self[:attachable_type] = (attachable.class.name if attachable)
362
+ end
363
+ end
364
+
365
+ INTEGRATION_DB.create_table!(:posts) do
366
+ primary_key :id
367
+ end
368
+ class ::Post < Sequel::Model
369
+ one_to_many :assets, :key=>:attachable_id, :reciprocal=>:attachable do |ds|
370
+ ds.filter(:attachable_type=>'Post')
371
+ end
372
+
373
+ private
374
+
375
+ def _add_asset(asset)
376
+ asset.attachable_id = pk
377
+ asset.attachable_type = 'Post'
378
+ asset.save
379
+ end
380
+ def _remove_asset(asset)
381
+ asset.attachable_id = nil
382
+ asset.attachable_type = nil
383
+ asset.save
384
+ end
385
+ def _remove_all_assets
386
+ Asset.filter(:attachable_id=>pk, :attachable_type=>'Post')\
387
+ .update(:attachable_id=>nil, :attachable_type=>nil)
388
+ end
389
+ end
390
+
391
+ INTEGRATION_DB.create_table!(:notes) do
392
+ primary_key :id
393
+ end
394
+ class ::Note < Sequel::Model
395
+ one_to_many :assets, :key=>:attachable_id, :reciprocal=>:attachable do |ds|
396
+ ds.filter(:attachable_type=>'Note')
397
+ end
398
+
399
+ private
400
+
401
+ def _add_asset(asset)
402
+ asset.attachable_id = pk
403
+ asset.attachable_type = 'Note'
404
+ asset.save
405
+ end
406
+ def _remove_asset(asset)
407
+ asset.attachable_id = nil
408
+ asset.attachable_type = nil
409
+ asset.save
410
+ end
411
+ def _remove_all_assets
412
+ Asset.filter(:attachable_id=>pk, :attachable_type=>'Note')\
413
+ .update(:attachable_id=>nil, :attachable_type=>nil)
414
+ end
415
+ end
416
+ @post = Post.create
417
+ Note.create
418
+ @note = Note.create
419
+ @asset1 = Asset.create(:attachable=>@post)
420
+ @asset2 = Asset.create(:attachable=>@note)
421
+ @asset1.associations.clear
422
+ @asset2.associations.clear
423
+ clear_sqls
424
+ end
425
+ after do
426
+ INTEGRATION_DB.drop_table :assets, :posts, :notes
427
+ Object.send(:remove_const, :Asset)
428
+ Object.send(:remove_const, :Post)
429
+ Object.send(:remove_const, :Note)
430
+ end
431
+
432
+ it "should load the correct associated object for a single object" do
433
+ @asset1.attachable.should == @post
434
+ @asset2.attachable.should == @note
435
+ end
436
+
437
+ it "should eagerly load the correct associated object for a group of objects" do
438
+ assets = Asset.order(:id).eager(:attachable).all
439
+ assets.should == [@asset1, @asset2]
440
+ assets[0].attachable.should == @post
441
+ assets[1].attachable.should == @note
442
+ end
443
+
444
+ it "should set items correctly" do
445
+ @asset1.attachable = @note
446
+ @asset2.attachable = @post
447
+ @asset1.attachable.should == @note
448
+ @asset1.attachable_id.should == @note.pk
449
+ @asset1.attachable_type.should == 'Note'
450
+ @asset2.attachable.should == @post
451
+ @asset2.attachable_id.should == @post.pk
452
+ @asset2.attachable_type.should == 'Post'
453
+ @asset1.attachable = nil
454
+ @asset1.attachable.should == nil
455
+ @asset1.attachable_id.should == nil
456
+ @asset1.attachable_type.should == nil
457
+ end
458
+
459
+ it "should add items correctly" do
460
+ @post.assets.should == [@asset1]
461
+ @post.add_asset(@asset2)
462
+ @post.assets.should == [@asset1, @asset2]
463
+ @asset2.attachable.should == @post
464
+ @asset2.attachable_id.should == @post.pk
465
+ @asset2.attachable_type.should == 'Post'
466
+ end
467
+
468
+ it "should remove items correctly" do
469
+ @note.assets.should == [@asset2]
470
+ @note.remove_asset(@asset2)
471
+ @note.assets.should == []
472
+ @asset2.attachable.should == nil
473
+ @asset2.attachable_id.should == nil
474
+ @asset2.attachable_type.should == nil
475
+ end
476
+
477
+ it "should remove all items correctly" do
478
+ @post.remove_all_assets
479
+ @note.remove_all_assets
480
+ @asset1.reload.attachable.should == nil
481
+ @asset2.reload.attachable.should == nil
482
+ end
483
+ end
484
+
485
+ describe "many_to_one/one_to_many not referencing primary key" do
486
+ before do
487
+ INTEGRATION_DB.instance_variable_set(:@schemas, {})
488
+ INTEGRATION_DB.create_table!(:clients) do
489
+ primary_key :id
490
+ String :name
491
+ end
492
+ class ::Client < Sequel::Model
493
+ one_to_many :invoices, :reciprocal=>:client, \
494
+ :dataset=>proc{Invoice.filter(:client_name=>name)}, \
495
+ :eager_loader=>(proc do |key_hash, clients, associations|
496
+ id_map = {}
497
+ clients.each do |client|
498
+ id_map[client.name] = client
499
+ client.associations[:invoices] = []
500
+ end
501
+ Invoice.filter(:client_name=>id_map.keys.sort).all do |inv|
502
+ inv.associations[:client] = client = id_map[inv.client_name]
503
+ client.associations[:invoices] << inv
504
+ end
505
+ end)
506
+
507
+ private
508
+
509
+ def _add_invoice(invoice)
510
+ invoice.client_name = name
511
+ invoice.save
512
+ end
513
+ def _remove_invoice(invoice)
514
+ invoice.client_name = nil
515
+ invoice.save
516
+ end
517
+ def _remove_all_invoices
518
+ Invoice.filter(:client_name=>name).update(:client_name=>nil)
519
+ end
520
+ end
521
+
522
+ INTEGRATION_DB.create_table!(:invoices) do
523
+ primary_key :id
524
+ String :client_name
525
+ end
526
+ class ::Invoice < Sequel::Model
527
+ many_to_one :client, :key=>:client_name, \
528
+ :dataset=>proc{Client.filter(:name=>client_name)}, \
529
+ :eager_loader=>(proc do |key_hash, invoices, associations|
530
+ id_map = key_hash[:client_name]
531
+ invoices.each{|inv| inv.associations[:client] = nil}
532
+ Client.filter(:name=>id_map.keys).all do |client|
533
+ id_map[client.name].each{|inv| inv.associations[:client] = client}
534
+ end
535
+ end)
536
+
537
+ private
538
+
539
+ def _client=(client)
540
+ self.client_name = (client.name if client)
541
+ end
542
+ end
543
+
544
+ @client1 = Client.create(:name=>'X')
545
+ @client2 = Client.create(:name=>'Y')
546
+ @invoice1 = Invoice.create(:client_name=>'X')
547
+ @invoice2 = Invoice.create(:client_name=>'X')
548
+ clear_sqls
549
+ end
550
+ after do
551
+ INTEGRATION_DB.drop_table :invoices, :clients
552
+ Object.send(:remove_const, :Client)
553
+ Object.send(:remove_const, :Invoice)
554
+ end
555
+
556
+ it "should load all associated one_to_many objects for a single object" do
557
+ invs = @client1.invoices
558
+ invs.should == [@invoice1, @invoice2]
559
+ invs[0].client.should == @client1
560
+ invs[1].client.should == @client1
561
+ end
562
+
563
+ it "should load the associated many_to_one object for a single object" do
564
+ client = @invoice1.client
565
+ client.should == @client1
566
+ end
567
+
568
+ it "should eagerly load all associated one_to_many objects for a group of objects" do
569
+ clients = Client.order(:id).eager(:invoices).all
570
+ clients.should == [@client1, @client2]
571
+ clients[1].invoices.should == []
572
+ invs = clients[0].invoices.sort_by{|x| x.pk}
573
+ invs.should == [@invoice1, @invoice2]
574
+ invs[0].client.should == @client1
575
+ invs[1].client.should == @client1
576
+ end
577
+
578
+ it "should eagerly load the associated many_to_one object for a group of objects" do
579
+ invoices = Invoice.order(:id).eager(:client).all
580
+ invoices.should == [@invoice1, @invoice2]
581
+ invoices[0].client.should == @client1
582
+ invoices[1].client.should == @client1
583
+ end
584
+
585
+ it "should set the associated object correctly" do
586
+ @invoice1.client = @client2
587
+ @invoice1.client.should == @client2
588
+ @invoice1.client_name.should == 'Y'
589
+ @invoice1.client = nil
590
+ @invoice1.client_name.should == nil
591
+ end
592
+
593
+ it "should add the associated object correctly" do
594
+ @client2.invoices.should == []
595
+ @client2.add_invoice(@invoice1)
596
+ @client2.invoices.should == [@invoice1]
597
+ @invoice1.client_name.should == 'Y'
598
+ @invoice1.client = nil
599
+ @invoice1.client_name.should == nil
600
+ end
601
+
602
+ it "should remove the associated object correctly" do
603
+ invs = @client1.invoices.sort_by{|x| x.pk}
604
+ invs.should == [@invoice1, @invoice2]
605
+ @client1.remove_invoice(@invoice1)
606
+ @client1.invoices.should == [@invoice2]
607
+ @invoice1.client_name.should == nil
608
+ @invoice1.client.should == nil
609
+ end
610
+
611
+ it "should remove all associated objects correctly" do
612
+ invs = @client1.remove_all_invoices
613
+ @invoice1.refresh.client.should == nil
614
+ @invoice1.client_name.should == nil
615
+ @invoice2.refresh.client.should == nil
616
+ @invoice2.client_name.should == nil
617
+ end
618
+ end
619
+
620
+ describe "statistics associations" do
621
+ before do
622
+ INTEGRATION_DB.create_table!(:projects) do
623
+ primary_key :id
624
+ String :name
625
+ end
626
+ class ::Project < Sequel::Model
627
+ many_to_one :ticket_hours, :read_only=>true, :key=>:id, :class=>:Ticket,
628
+ :dataset=>proc{Ticket.filter(:project_id=>id).select{sum(hours).as(hours)}},
629
+ :eager_loader=>(proc do |kh, projects, a|
630
+ projects.each{|p| p.associations[:ticket_hours] = nil}
631
+ Ticket.filter(:project_id=>kh[:id].keys).
632
+ group(:project_id).
633
+ select{[project_id.as(project_id), sum(hours).as(hours)]}.
634
+ all do |t|
635
+ p = kh[:id][t.values.delete(:project_id)].first
636
+ p.associations[:ticket_hours] = t
637
+ end
638
+ end)
639
+ def ticket_hours
640
+ if s = super
641
+ s[:hours]
642
+ end
643
+ end
644
+ end
645
+
646
+ INTEGRATION_DB.create_table!(:tickets) do
647
+ primary_key :id
648
+ foreign_key :project_id, :projects
649
+ Integer :hours
650
+ end
651
+ class ::Ticket < Sequel::Model
652
+ many_to_one :project
653
+ end
654
+
655
+ @project1 = Project.create(:name=>'X')
656
+ @project2 = Project.create(:name=>'Y')
657
+ @ticket1 = Ticket.create(:project=>@project1, :hours=>1)
658
+ @ticket2 = Ticket.create(:project=>@project1, :hours=>10)
659
+ @ticket3 = Ticket.create(:project=>@project2, :hours=>2)
660
+ @ticket4 = Ticket.create(:project=>@project2, :hours=>20)
661
+ clear_sqls
662
+ end
663
+ after do
664
+ INTEGRATION_DB.drop_table :tickets, :projects
665
+ Object.send(:remove_const, :Project)
666
+ Object.send(:remove_const, :Ticket)
667
+ end
668
+
669
+ it "should give the correct sum of ticket hours for each project" do
670
+ @project1.ticket_hours.to_i.should == 11
671
+ @project2.ticket_hours.to_i.should == 22
672
+ end
673
+
674
+ it "should give the correct sum of ticket hours for each project when eager loading" do
675
+ p1, p2 = Project.order(:name).eager(:ticket_hours).all
676
+ p1.ticket_hours.to_i.should == 11
677
+ p2.ticket_hours.to_i.should == 22
678
+ end
679
+ end
680
+
681
+ describe "one to one associations" do
682
+ before do
683
+ INTEGRATION_DB.create_table!(:books) do
684
+ primary_key :id
685
+ end
686
+ class ::Book < Sequel::Model
687
+ one_to_one :first_page, :class=>:Page, :conditions=>{:page_number=>1}
688
+ one_to_one :second_page, :class=>:Page, :conditions=>{:page_number=>2}
689
+ end
690
+
691
+ INTEGRATION_DB.create_table!(:pages) do
692
+ primary_key :id
693
+ foreign_key :book_id, :books
694
+ Integer :page_number
695
+ end
696
+ class ::Page < Sequel::Model
697
+ many_to_one :book
698
+ end
699
+
700
+ @book1 = Book.create
701
+ @book2 = Book.create
702
+ @page1 = Page.create(:book=>@book1, :page_number=>1)
703
+ @page2 = Page.create(:book=>@book1, :page_number=>2)
704
+ @page3 = Page.create(:book=>@book2, :page_number=>1)
705
+ @page4 = Page.create(:book=>@book2, :page_number=>2)
706
+ clear_sqls
707
+ end
708
+
709
+ after do
710
+ INTEGRATION_DB.drop_table :pages, :books
711
+ Object.send(:remove_const, :Book)
712
+ Object.send(:remove_const, :Page)
713
+ end
714
+
715
+ it "should be eager loadable" do
716
+ bk1, bk2 = Book.filter(:books__id=>[1,2]).eager(:first_page).all
717
+ bk1.first_page.should == @page1
718
+ bk2.first_page.should == @page3
719
+ end
720
+
721
+ it "should be eager graphable" do
722
+ bk1, bk2 = Book.filter(:books__id=>[1,2]).eager_graph(:first_page).all
723
+ bk1.first_page.should == @page1
724
+ bk2.first_page.should == @page3
725
+ end
726
+
727
+ it "should be eager graphable two at once" do
728
+ bk1, bk2 = Book.filter(:books__id=>[1,2]).eager_graph(:first_page, :second_page).all
729
+ bk1.first_page.should == @page1
730
+ bk1.second_page.should == @page2
731
+ bk2.first_page.should == @page3
732
+ bk2.second_page.should == @page4
733
+ end
734
+ end