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,193 @@
1
+ require "rake"
2
+ require "rake/clean"
3
+ require "rake/gempackagetask"
4
+ begin
5
+ require "hanna/rdoctask"
6
+ rescue LoadError
7
+ require "rake/rdoctask"
8
+ end
9
+
10
+ NAME = 'viking-sequel'
11
+ VERS = lambda do
12
+ require File.expand_path("../lib/sequel/version", __FILE__)
13
+ Sequel.version
14
+ end
15
+ CLEAN.include ["**/.*.sw?", "sequel-*.gem", ".config", "rdoc", "coverage", "www/public/*.html", "www/public/rdoc*"]
16
+ RDOC_DEFAULT_OPTS = ["--quiet", "--line-numbers", "--inline-source", '--title', 'Sequel: The Database Toolkit for Ruby']
17
+ RDOC_OPTS = RDOC_DEFAULT_OPTS + ['--main', 'README.rdoc']
18
+
19
+ # Gem Packaging and Release
20
+
21
+ desc "Packages sequel"
22
+ task :package=>[:clean] do |p|
23
+ sh %{gem build sequel.gemspec}
24
+ end
25
+
26
+ desc "Install sequel gem"
27
+ task :install=>[:package] do
28
+ sh %{sudo gem install ./#{NAME}-#{VERS.call} --local}
29
+ end
30
+
31
+ desc "Uninstall sequel gem"
32
+ task :uninstall=>[:clean] do
33
+ sh %{sudo gem uninstall #{NAME}}
34
+ end
35
+
36
+ desc "Upload sequel gem to gemcutter"
37
+ task :release=>[:package] do
38
+ sh %{gem push ./#{NAME}-#{VERS.call}.gem}
39
+ end
40
+
41
+ ### RDoc
42
+
43
+ Rake::RDocTask.new do |rdoc|
44
+ rdoc.rdoc_dir = "rdoc"
45
+ rdoc.options += RDOC_OPTS
46
+ rdoc.rdoc_files.add %w"README.rdoc CHANGELOG COPYING lib/**/*.rb doc/*.rdoc doc/release_notes/*.txt"
47
+ end
48
+
49
+ ### Website
50
+
51
+ desc "Make local version of website"
52
+ task :website do
53
+ sh %{www/make_www.rb}
54
+ end
55
+
56
+ desc "Make rdoc for website"
57
+ task :website_rdoc=>[:website_rdoc_main, :website_rdoc_adapters, :website_rdoc_plugins]
58
+
59
+ Rake::RDocTask.new(:website_rdoc_main) do |rdoc|
60
+ rdoc.rdoc_dir = "www/public/rdoc"
61
+ rdoc.options += RDOC_OPTS
62
+ rdoc.rdoc_files.add %w"README.rdoc CHANGELOG COPYING lib/*.rb lib/sequel/*.rb lib/sequel/{connection_pool,dataset,database,model}/*.rb doc/*.rdoc doc/release_notes/*.txt"
63
+ end
64
+
65
+ Rake::RDocTask.new(:website_rdoc_adapters) do |rdoc|
66
+ rdoc.rdoc_dir = "www/public/rdoc-adapters"
67
+ rdoc.options += RDOC_DEFAULT_OPTS + %w'--main Sequel'
68
+ rdoc.rdoc_files.add %w"lib/sequel/adapters/**/*.rb"
69
+ end
70
+
71
+ Rake::RDocTask.new(:website_rdoc_plugins) do |rdoc|
72
+ rdoc.rdoc_dir = "www/public/rdoc-plugins"
73
+ rdoc.options += RDOC_DEFAULT_OPTS + %w'--main Sequel'
74
+ rdoc.rdoc_files.add %w"lib/sequel/{extensions,plugins}/**/*.rb"
75
+ end
76
+
77
+ desc "Update Non-RDoc section of sequel.rubyforge.org"
78
+ task :website_rf_base=>[:website] do
79
+ sh %{rsync -rt www/public/*.html rubyforge.org:/var/www/gforge-projects/sequel/}
80
+ end
81
+
82
+ desc "Update sequel.rubyforge.org"
83
+ task :website_rf=>[:website, :website_rdoc] do
84
+ sh %{rsync -rvt www/public/* rubyforge.org:/var/www/gforge-projects/sequel/}
85
+ end
86
+
87
+ ### Specs
88
+
89
+ begin
90
+ require "spec/rake/spectask"
91
+
92
+ spec_opts = lambda do
93
+ lib_dir = File.join(File.dirname(__FILE__), 'lib')
94
+ ENV['RUBYLIB'] ? (ENV['RUBYLIB'] += ":#{lib_dir}") : (ENV['RUBYLIB'] = lib_dir)
95
+ end
96
+
97
+ rcov_opts = lambda do
98
+ [true, File.read("spec/rcov.opts").split("\n")]
99
+ end
100
+
101
+ desc "Run core and model specs with coverage"
102
+ Spec::Rake::SpecTask.new("spec_coverage") do |t|
103
+ t.spec_files = Dir["spec/{core,model}/*_spec.rb"]
104
+ spec_opts.call
105
+ t.rcov, t.rcov_opts = rcov_opts.call
106
+ end
107
+
108
+ desc "Run core and model specs"
109
+ task :default => [:spec]
110
+ Spec::Rake::SpecTask.new("spec") do |t|
111
+ t.spec_files = Dir["spec/{core,model}/*_spec.rb"]
112
+ spec_opts.call
113
+ end
114
+
115
+ desc "Run core specs"
116
+ Spec::Rake::SpecTask.new("spec_core") do |t|
117
+ t.spec_files = Dir["spec/core/*_spec.rb"]
118
+ spec_opts.call
119
+ end
120
+
121
+ desc "Run model specs"
122
+ Spec::Rake::SpecTask.new("spec_model") do |t|
123
+ t.spec_files = Dir["spec/model/*_spec.rb"]
124
+ spec_opts.call
125
+ end
126
+
127
+ desc "Run extension/plugin specs"
128
+ Spec::Rake::SpecTask.new("spec_plugin") do |t|
129
+ t.spec_files = Dir["spec/extensions/*_spec.rb"]
130
+ spec_opts.call
131
+ end
132
+
133
+ desc "Run extention/plugin specs with coverage"
134
+ Spec::Rake::SpecTask.new("spec_plugin_cov") do |t|
135
+ t.spec_files = Dir["spec/extensions/*_spec.rb"]
136
+ spec_opts.call
137
+ t.rcov, t.rcov_opts = rcov_opts.call
138
+ end
139
+
140
+ desc "Run integration tests"
141
+ Spec::Rake::SpecTask.new("integration") do |t|
142
+ t.spec_files = Dir["spec/integration/*_test.rb"]
143
+ spec_opts.call
144
+ end
145
+
146
+ desc "Run integration tests with coverage"
147
+ Spec::Rake::SpecTask.new("integration_cov") do |t|
148
+ t.spec_files = Dir["spec/integration/*_test.rb"]
149
+ spec_opts.call
150
+ t.rcov, t.rcov_opts = rcov_opts.call
151
+ end
152
+
153
+ %w'postgres sqlite mysql informix oracle firebird mssql'.each do |adapter|
154
+ desc "Run #{adapter} specs"
155
+ Spec::Rake::SpecTask.new("spec_#{adapter}") do |t|
156
+ t.spec_files = ["spec/adapters/#{adapter}_spec.rb"] + Dir["spec/integration/*_test.rb"]
157
+ spec_opts.call
158
+ end
159
+
160
+ desc "Run #{adapter} specs with coverage"
161
+ Spec::Rake::SpecTask.new("spec_#{adapter}_cov") do |t|
162
+ t.spec_files = ["spec/adapters/#{adapter}_spec.rb"] + Dir["spec/integration/*_test.rb"]
163
+ spec_opts.call
164
+ t.rcov, t.rcov_opts = rcov_opts.call
165
+ end
166
+ end
167
+ rescue LoadError
168
+ end
169
+
170
+ desc "check documentation coverage"
171
+ task :dcov do
172
+ sh %{find lib -name '*.rb' | xargs dcov}
173
+ end
174
+
175
+ ### Statistics
176
+
177
+ desc "Report code statistics (KLOCs, etc) from the application"
178
+ task :stats do
179
+ STATS_DIRECTORIES = [%w(Code lib/), %w(Spec spec)].map{|name, dir| [ name, "./#{dir}" ] }.select { |name, dir| File.directory?(dir)}
180
+ require "extra/stats"
181
+ verbose = true
182
+ CodeStatistics.new(*STATS_DIRECTORIES).to_s
183
+ end
184
+
185
+ desc "Print Sequel version"
186
+ task :version do
187
+ puts VERS.call
188
+ end
189
+
190
+ desc "Check syntax of all .rb files"
191
+ task :check_syntax do
192
+ Dir['**/*.rb'].each{|file| print `#{ENV['RUBY'] || :ruby} -c #{file} | fgrep -v "Syntax OK"`}
193
+ end
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'optparse'
5
+ require 'sequel'
6
+
7
+ db_opts = {:test=>true}
8
+ copy_databases = nil
9
+ dump_migration = nil
10
+ echo = nil
11
+ env = nil
12
+ logfile = nil
13
+ migrate_dir = nil
14
+ migrate_ver = nil
15
+ backtrace = nil
16
+ load_dirs = []
17
+
18
+ opts = OptionParser.new do |opts|
19
+ opts.banner = "Sequel: The Database Toolkit for Ruby"
20
+ opts.define_head "Usage: sequel <uri|path> [options]"
21
+ opts.separator ""
22
+ opts.separator "Examples:"
23
+ opts.separator " sequel sqlite://blog.db"
24
+ opts.separator " sequel postgres://localhost/my_blog"
25
+ opts.separator " sequel config/database.yml"
26
+ opts.separator ""
27
+ opts.separator "For more information see http://sequel.rubyforge.org"
28
+ opts.separator ""
29
+ opts.separator "Options:"
30
+
31
+ opts.on_tail("-h", "-?", "--help", "Show this message") do
32
+ puts opts
33
+ exit
34
+ end
35
+
36
+ opts.on("-C", "--copy-databases", "copy one database to another") do
37
+ copy_databases = true
38
+ end
39
+
40
+ opts.on("-d", "--dump-migration", "print database migration to STDOUT") do
41
+ dump_migration = true
42
+ end
43
+
44
+ opts.on("-D", "--dump-migration-same-db", "print database migration to STDOUT without type translation") do
45
+ dump_migration = :same_db
46
+ end
47
+
48
+ opts.on("-e", "--env ENV", "use environment config for database") do |v|
49
+ env = v
50
+ end
51
+
52
+ opts.on("-E", "--echo", "echo SQL statements") do
53
+ echo = true
54
+ end
55
+
56
+ opts.on("-l", "--log logfile", "log SQL statements to log file") do |v|
57
+ logfile = v
58
+ end
59
+
60
+ opts.on("-L", "--load-dir DIR", "loads all *.rb under specifed directory") do |v|
61
+ load_dirs << v
62
+ end
63
+
64
+ opts.on("-m", "--migrate-directory DIR", "run the migrations in directory") do |v|
65
+ migrate_dir = v
66
+ end
67
+
68
+ opts.on("-M", "--migrate-version VER", "migrate the database to version given") do |v|
69
+ migrate_ver = Integer(v)
70
+ end
71
+
72
+ opts.on("-N", "--no-test-connection", "do not test the connection") do
73
+ db_opts[:test] = false
74
+ end
75
+
76
+ opts.on("-t", "--trace", "Output the full backtrace if an exception is raised") do
77
+ backtrace = true
78
+ end
79
+
80
+ opts.on_tail("-v", "--version", "Show version") do
81
+ puts "sequel #{Sequel.version}"
82
+ exit
83
+ end
84
+ end
85
+ opts.parse!
86
+
87
+ db = ARGV.shift
88
+
89
+ error_proc = lambda do |msg|
90
+ $stderr.puts(msg)
91
+ exit 1
92
+ end
93
+
94
+ error_proc["Error: Must specify -m if using -M"] if migrate_ver && !migrate_dir
95
+ error_proc["Error: Cannot specify -D or -d with -m"] if dump_migration && migrate_dir
96
+ error_proc["Error: Cannot specify -C with -d, -D, or -m"] if copy_databases && (dump_migration || migrate_dir)
97
+
98
+ if logfile || echo
99
+ require 'logger'
100
+ db_opts[:loggers] = []
101
+ db_opts[:loggers] << Logger.new(logfile) if logfile
102
+ db_opts[:loggers] << Logger.new($stdout) if echo
103
+ end
104
+
105
+ connect_proc = lambda do |database|
106
+ db = if database.nil? || database.empty?
107
+ db = Sequel::Database.new(:quote_identifiers=>false)
108
+ def db.connect(*args); Object.new; end
109
+ db.identifier_input_method = nil
110
+ db.identifier_output_method = nil
111
+ db
112
+ elsif File.exist?(database)
113
+ require 'yaml'
114
+ env ||= "development"
115
+ db_config = YAML.load_file(database)
116
+ db_config = db_config[env] || db_config[env.to_sym] || db_config
117
+ db_config.keys.each{|k| db_config[k.to_sym] = db_config.delete(k)}
118
+ Sequel.connect(db_config.merge!(db_opts))
119
+ else
120
+ Sequel.connect(database, db_opts)
121
+ end
122
+ db
123
+ end
124
+
125
+ begin
126
+ DB = connect_proc[db]
127
+ if migrate_dir
128
+ Sequel.extension :migration
129
+ Sequel::Migrator.apply(DB, migrate_dir, migrate_ver)
130
+ exit
131
+ end
132
+ if dump_migration
133
+ Sequel.extension :schema_dumper
134
+ puts DB.dump_schema_migration(:same_db=>dump_migration==:same_db)
135
+ exit
136
+ end
137
+ if copy_databases
138
+ Sequel.extension :migration, :schema_dumper
139
+
140
+ db2 = ARGV.shift
141
+ error_proc["Error: Must specify database connection string or path to yaml file as second argument for database you want to copy to"] if db2.nil? || db2.empty?
142
+ start_time = Time.now
143
+ TO_DB = connect_proc[db2]
144
+ same_db = DB.database_type==TO_DB.database_type
145
+
146
+ puts "Databases connections successful"
147
+ schema_migration = eval(DB.dump_schema_migration(:indexes=>false, :same_db=>same_db))
148
+ index_migration = eval(DB.dump_indexes_migration(:same_db=>same_db))
149
+ puts "Migrations dumped successfully"
150
+
151
+ schema_migration.apply(TO_DB, :up)
152
+ puts "Tables created"
153
+
154
+ puts "Begin copying data"
155
+ DB.transaction do
156
+ TO_DB.transaction do
157
+ DB.tables.each do |table|
158
+ puts "Begin copying records for table: #{table}"
159
+ time = Time.now
160
+ to_ds = TO_DB.from(table)
161
+ j = 0
162
+ DB.from(table).each do |record|
163
+ if Time.now - time > 5
164
+ puts "Status: #{j} records copied"
165
+ time = Time.now
166
+ end
167
+ to_ds.insert(record)
168
+ j += 1
169
+ end
170
+ puts "Finished copying #{j} records for table: #{table}"
171
+ end
172
+ end
173
+ end
174
+ puts "Finished copying data"
175
+
176
+ puts "Begin creating indexes"
177
+ index_migration.apply(TO_DB, :up)
178
+ puts "Finished creating indexes"
179
+
180
+ if TO_DB.database_type == :postgres
181
+ TO_DB.tables.each{|t| TO_DB.reset_primary_key_sequence(t)}
182
+ puts "Primary key sequences reset successfully"
183
+ end
184
+ puts "Database copy finished in #{Time.now - start_time} seconds"
185
+ exit
186
+ end
187
+ rescue => e
188
+ raise e if backtrace
189
+ error_proc["Error: #{e.class}: #{e.message}#{e.backtrace.first}"]
190
+ end
191
+
192
+ load_dirs.each{|d| Dir["#{d}/**/*.rb"].each{|f| load(f)}}
193
+
194
+ require 'irb'
195
+ puts "Your database is stored in DB..."
196
+ IRB.start
@@ -0,0 +1,644 @@
1
+ = Advanced Associations
2
+
3
+ Sequel::Model has the most powerful and flexible associations of any ruby ORM.
4
+
5
+ "Extraordinary claims require extraordinary proof" - Carl Sagan
6
+
7
+ == Background: Sequel::Model association options
8
+
9
+ There are a bunch of advanced association options that are available to
10
+ handle the other-than-bog-standard cases. First we'll go over some of
11
+ the simpler ones:
12
+
13
+ All associations take a block that can be used to further filter/modify the
14
+ default dataset. There's also an :eager_block option if you want to use
15
+ a different block when eager loading via Dataset#eager. Association blocks are
16
+ useful for things like:
17
+
18
+ Artist.one_to_many :gold_albums, :class=>:Album do |ds|
19
+ ds.filter{|o| o.copies_sold > 500000}
20
+ end
21
+
22
+ There are a whole bunch of options for changing how the association is eagerly
23
+ loaded via Dataset#eager_graph: :graph_block, :graph_conditions,
24
+ :graph_only_conditions, :graph_join_type (and :graph_join_table_* ones for
25
+ JOINing to the join table in a many_to_many association).
26
+
27
+ - :graph_join_type - The type of join to do
28
+ - :graph_conditions - Additional conditions to put on join (needs to be a
29
+ hash or array of all two pairs). Automatically assumes unqualified symbols
30
+ as first element of the pair to be columns of the associated model, and
31
+ unqualified symbols of the second element of the pair to be columns of the
32
+ current model.
33
+ - :graph_block - A block passed to join_table, allowing you to specify
34
+ conditions other than equality, or to use OR, or set up any arbitrary
35
+ condition. The block is passed the associated table alias, current model
36
+ alias, and array of previous joins.
37
+ - :graph_only_conditions - Use these conditions instead of the standard
38
+ association conditions. This is necessary when you don't want to have an
39
+ equal condition between the foreign key and primary key of the tables.
40
+ You can also use this to have a JOIN USING (array of symbols), or a NATURAL
41
+ or CROSS JOIN (nil, with the appropriate :graph_join_type).
42
+
43
+ These can be used like this:
44
+
45
+ # Makes Artist.eager_graph(:required_albums).all not return artists that
46
+ # don't have any albums
47
+ Artist.one_to_many :required_albums, :class=>:Album, :graph_join_type=>:inner
48
+
49
+ # Makes sure all returned albums have the active flag set
50
+ Artist.one_to_many :active_albums, :class=>:Album, \
51
+ :graph_conditions=>{:active=>true}
52
+
53
+ # Only returns albums that have sold more than 500,000 copies
54
+ Artist.one_to_many :gold_albums, :class=>:Album, \
55
+ :graph_block=>proc{|j,lj,js| :copies_sold.qualify(j) > 500000}
56
+
57
+ # Handles the case where the to tables are associated by a case insensitive name string
58
+ Artist.one_to_many :albums, :key=>:artist_name, \
59
+ :graph_only_conditions=>nil, \
60
+ :graph_block=>proc{|j,lj,js| {:lower.sql_function(artist_name.qualify(j))=>:lower.sql_function(name.qualify(lj))}}
61
+
62
+ # Handles the case where both key columns have the name artist_name, and you want to use
63
+ # a JOIN USING
64
+ Artist.one_to_many :albums, :key=>:artist_name, :graph_only_conditions=>[:artist_name]
65
+
66
+ Remember, using #eager_graph is generally only necessary when you need to
67
+ filter/order based on columns in an associated table, it is recommended to
68
+ use #eager for eager loading if possible.
69
+
70
+ For lazy loading (e.g. Model[1].association), the :dataset option can be used
71
+ to specify an arbitrary dataset (one that uses different keys, multiple keys,
72
+ joins to other tables, etc.).
73
+
74
+ For eager loading via #eager, the :eager_loader option can be used to specify
75
+ how to eagerly load a complex association. This is an extremely powerful
76
+ option. Though it can often be verbose (compared to other things in Sequel),
77
+ it allows you complete control over how to eagerly load associations for a
78
+ group of objects.
79
+
80
+ :eager_loader should be a proc that takes 3 arguments, a key_hash,
81
+ an array of records, and a hash of dependent associations. Since you
82
+ are given all of the records, you can do things like filter on
83
+ associations that are specified by multiple keys, or do multiple
84
+ queries depending on the content of the records (which would be
85
+ necessary for polymorphic associations). Inside the :eager_loader
86
+ proc, you should get the related objects and populate the
87
+ associations cache for all objects in the array of records. The hash
88
+ of dependent associations is available for you to cascade the eager
89
+ loading down multiple levels, but it is up to you to use it. The
90
+ key_hash is a performance enhancement that is used by the default
91
+ code and is also available to you. It is a hash with keys being
92
+ foreign/primary key symbols in the current table, and the values
93
+ being hashes where the key is foreign/primary key values and values
94
+ being arrays of current model objects having the foreign/primary key
95
+ value associated with the key. This is hard to visualize, so I'll
96
+ give an example:
97
+
98
+ album1 = Album.load(:id=>1, :artist_id=>2)
99
+ album2 = Album.load(:id=>3, :artist_id=>2)
100
+ Album.many_to_one :artist
101
+ Album.one_to_many :tracks
102
+ Album.eager(:band, :tracks).all
103
+ # The key_hash provided to the :eager_loader proc would be:
104
+ {:id=>{1=>[album1], 3=>[album2]}, :artist_id=>{2=>[album1, album2]}}
105
+
106
+ Using these options, you can build associations Sequel doesn't natively support,
107
+ and still be able to use the same eager loading features that you are used to.
108
+
109
+ == ActiveRecord associations
110
+
111
+ Sequel supports all of associations that ActiveRecord supports, one way or
112
+ another. Sometimes this requires more code, as Sequel is a toolkit and not
113
+ a swiss army chainsaw.
114
+
115
+ === Association callbacks
116
+
117
+ Sequel supports the same callbacks that ActiveRecord does on one_to_many and
118
+ many_to_many associations: :before_add, :before_remove, :after_add, and
119
+ :after_remove. One many_to_one associations and one_to_one associations
120
+ (which are one_to_many associations with the :one_to_one option), Sequel
121
+ supports the :before_set and :after_set callbacks. On all associations,
122
+ Sequel supports :after_load, which is called after the association has been
123
+ loaded.
124
+
125
+ Each of these options can be a Symbol specifying an instance method
126
+ that takes one argument (the associated object), or a Proc that takes
127
+ two arguments (the current object and the associated object), or an
128
+ array of Symbols and Procs. For :after_load with a *_to_many association,
129
+ the associated object argument is an array of associated objects.
130
+
131
+ If any of the before callbacks return false, the adding/removing
132
+ does not happen and it either raises an error (the default), or
133
+ returns nil (if raise_on_save_failure is false).
134
+
135
+ === Association extensions
136
+
137
+ All associations come with an association_dataset method that can be further filtered or
138
+ otherwise modified:
139
+
140
+ class Author < Sequel::Model
141
+ one_to_many :authorships
142
+ end
143
+ Author.first.authorships_dataset.filter{|o| o.number < 10}.first
144
+
145
+ You can extend a dataset with a module easily with :extend. You can reference
146
+ the model object that created the association dataset via the dataset's
147
+ model_object method, and the related association reflection via the dataset's
148
+ association_reflection method:
149
+
150
+ module FindOrCreate
151
+ def find_or_create(vals)
152
+ first(vals) || model.create(vals.merge(association_reflection[:key]=>model_object.id))
153
+ end
154
+ end
155
+ class Author < Sequel::Model
156
+ one_to_many :authorships, :extend=>FindOrCreate
157
+ end
158
+ Author.first.authorships_dataset.find_or_create(:name=>'Blah', :number=>10)
159
+
160
+ === has_many :through associations
161
+
162
+ many_to_many handles the usual case of a has_many :through with a belongs_to in
163
+ the associated model. It doesn't break on the case where the join table is a
164
+ model table, unlike ActiveRecord's has_and_belongs_to_many.
165
+
166
+ ActiveRecord:
167
+
168
+ class Author < ActiveRecord::Base
169
+ has_many :authorships
170
+ has_many :books, :through => :authorships
171
+ end
172
+
173
+ class Authorship < ActiveRecord::Base
174
+ belongs_to :author
175
+ belongs_to :book
176
+ end
177
+
178
+ @author = Author.find :first
179
+ @author.books
180
+
181
+ Sequel::Model:
182
+
183
+ class Author < Sequel::Model
184
+ one_to_many :authorships
185
+ many_to_many :books, :join_table=>:authorships
186
+ end
187
+
188
+ class Authorship < Sequel::Model
189
+ many_to_one :author
190
+ many_to_one :book
191
+ end
192
+
193
+ @author = Author.first
194
+ @author.books
195
+
196
+ If you use an association other than belongs_to in the associated model, you'll have
197
+ to specify some of the :*key options and write a short method.
198
+
199
+ ActiveRecord:
200
+
201
+ class Firm < ActiveRecord::Base
202
+ has_many :clients
203
+ has_many :invoices, :through => :clients
204
+ end
205
+
206
+ class Client < ActiveRecord::Base
207
+ belongs_to :firm
208
+ has_many :invoices
209
+ end
210
+
211
+ class Invoice < ActiveRecord::Base
212
+ belongs_to :client
213
+ has_one :firm, :through => :client
214
+ end
215
+
216
+ Firm.find(:first).invoices
217
+
218
+ Sequel::Model:
219
+
220
+ class Firm < Sequel::Model
221
+ one_to_many :clients
222
+ many_to_many :invoices, :join_table=>:clients, :right_key=>:id, :right_primary_key=>:client_id
223
+ end
224
+
225
+ class Client < Sequel::Model
226
+ many_to_one :firm
227
+ one_to_many :invoices
228
+ end
229
+
230
+ class Invoice < Sequel::Model
231
+ many_to_one :client
232
+
233
+ def firm
234
+ client.firm if client
235
+ end
236
+ end
237
+
238
+ Firm.first.invoices
239
+
240
+ === Polymorphic Associations
241
+
242
+ Sequel discourages the use of polymorphic associations, which is the reason they
243
+ are not supported by default. All polymorphic associations can be made non-polymorphic
244
+ by using additional tables and/or columns instead of having a column
245
+ containing the associated class name as a string.
246
+
247
+ Polymorphic associations break referential integrity and are significantly more
248
+ complex than non-polymorphic associations, so their use is not recommended unless
249
+ you are stuck with an existing design that uses them.
250
+
251
+ If you must use them, look for the sequel_polymorphic plugin, as it makes using
252
+ polymorphic associations in Sequel about as easy as it is in ActiveRecord. However,
253
+ here's how they can be done using Sequel's custom associations:
254
+
255
+ ActiveRecord:
256
+
257
+ class Asset < ActiveRecord::Base
258
+ belongs_to :attachable, :polymorphic => true
259
+ end
260
+
261
+ class Post < ActiveRecord::Base
262
+ has_many :assets, :as => :attachable
263
+ end
264
+
265
+ class Note < ActiveRecord::Base
266
+ has_many :assets, :as => :attachable
267
+ end
268
+
269
+ @asset.attachable = @post
270
+ @asset.attachable = @note
271
+
272
+ Sequel::Model:
273
+
274
+ class Asset < Sequel::Model
275
+ many_to_one :attachable, :reciprocal=>:assets, \
276
+ :dataset=>(proc do
277
+ klass = attachable_type.constantize
278
+ klass.filter(klass.primary_key=>attachable_id)
279
+ end), \
280
+ :eager_loader=>(proc do |key_hash, assets, associations|
281
+ id_map = {}
282
+ assets.each do |asset|
283
+ asset.associations[:attachable] = nil
284
+ ((id_map[asset.attachable_type] ||= {})[asset.attachable_id] ||= []) << asset
285
+ end
286
+ id_map.each do |klass_name, id_map|
287
+ klass = klass_name.constantize
288
+ klass.filter(klass.primary_key=>id_map.keys).all do |attach|
289
+ id_map[attach.pk].each do |asset|
290
+ asset.associations[:attachable] = attach
291
+ end
292
+ end
293
+ end
294
+ end)
295
+
296
+ private
297
+
298
+ def _attachable=(attachable)
299
+ self[:attachable_id] = (attachable.pk if attachable)
300
+ self[:attachable_type] = (attachable.class.name if attachable)
301
+ end
302
+ end
303
+
304
+ class Post < Sequel::Model
305
+ one_to_many :assets, :key=>:attachable_id do |ds|
306
+ ds.filter(:attachable_type=>'Post')
307
+ end
308
+
309
+ private
310
+
311
+ def _add_asset(asset)
312
+ asset.attachable_id = pk
313
+ asset.attachable_type = 'Post'
314
+ asset.save
315
+ end
316
+ def _remove_asset(asset)
317
+ asset.attachable_id = nil
318
+ asset.attachable_type = nil
319
+ asset.save
320
+ end
321
+ def _remove_all_assets
322
+ Asset.filter(:attachable_id=>pk, :attachable_type=>'Post')\
323
+ .update(:attachable_id=>nil, :attachable_type=>nil)
324
+ end
325
+ end
326
+
327
+ class Note < Sequel::Model
328
+ one_to_many :assets, :key=>:attachable_id do |ds|
329
+ ds.filter(:attachable_type=>'Note')
330
+ end
331
+
332
+ private
333
+
334
+ def _add_asset(asset)
335
+ asset.attachable_id = pk
336
+ asset.attachable_type = 'Note'
337
+ asset.save
338
+ end
339
+ def _remove_asset(asset)
340
+ asset.attachable_id = nil
341
+ asset.attachable_type = nil
342
+ asset.save
343
+ end
344
+ def _remove_all_assets
345
+ Asset.filter(:attachable_id=>pk, :attachable_type=>'Note')\
346
+ .update(:attachable_id=>nil, :attachable_type=>nil)
347
+ end
348
+ end
349
+
350
+ @asset.attachable = @post
351
+ @asset.attachable = @note
352
+
353
+ == More advanced associations
354
+
355
+ So far, we've only shown that Sequel::Model has associations as powerful as
356
+ ActiveRecord's. Now we will show how Sequel::Model's associations are more
357
+ powerful.
358
+
359
+ === many_to_one/one_to_many not referencing primary key
360
+
361
+ This can now be handled easily in Sequel using the :primary_key association
362
+ option. However, this example shows how the association was possible before
363
+ the introduction of that option.
364
+
365
+ Let's say you have two tables, invoices and clients, where each client is
366
+ associated with many invoices. However, instead of using the client's primary
367
+ key, the invoice is associated to the client by name (this is bad database
368
+ design, but sometimes you have to play with the cards you are dealt).
369
+
370
+ class Client < Sequel::Model
371
+ one_to_many :invoices, :reciprocal=>:client, \
372
+ :dataset=>proc{Invoice.filter(:client_name=>name)}, \
373
+ :eager_loader=>(proc do |key_hash, clients, associations|
374
+ id_map = {}
375
+ clients.each do |client|
376
+ id_map[client.name] = client
377
+ client.associations[:invoices] = []
378
+ end
379
+ Invoice.filter(:client_name=>id_map.keys.sort).all do |inv|
380
+ inv.associations[:client] = client = id_map[inv.client_name]
381
+ client.associations[:invoices] << inv
382
+ end
383
+ end)
384
+
385
+ private
386
+
387
+ def _add_invoice(invoice)
388
+ invoice.client_name = name
389
+ invoice.save
390
+ end
391
+ def _remove_invoice(invoice)
392
+ invoice.client_name = nil
393
+ invoice.save
394
+ end
395
+ def _remove_all_invoices
396
+ Invoice.filter(:client_name=>name).update(:client_name=>nil)
397
+ end
398
+ end
399
+
400
+ class Invoice < Sequel::Model
401
+ many_to_one :client, :key=>:client_name, \
402
+ :dataset=>proc{Client.filter(:name=>client_name)}, \
403
+ :eager_loader=>(proc do |key_hash, invoices, associations|
404
+ id_map = key_hash[:client_name]
405
+ invoices.each{|inv| inv.associations[:client] = nil}
406
+ Client.filter(:name=>id_map.keys).all do |client|
407
+ id_map[client.name].each{|inv| inv.associations[:client] = client}
408
+ end
409
+ end)
410
+
411
+ private
412
+
413
+ def _client=(client)
414
+ self.client_name = (client.name if client)
415
+ end
416
+ end
417
+
418
+ === Joining on multiple keys
419
+
420
+ Let's say you have two tables that are associated with each other with multiple
421
+ keys. This can now be handled using Sequel's built in composite key support for
422
+ associations:
423
+
424
+ # Both of these models have an album_id, number, and disc_number fields.
425
+ # All FavoriteTracks have an associated track, but not all tracks have an
426
+ # associated favorite track
427
+
428
+ class Track < Sequel::Model
429
+ many_to_one :favorite_track, :key=>[:disc_number, :number, :album_id], :primary_key=>[:disc_number, :number, :album_id]
430
+ end
431
+ class FavoriteTrack < Sequel::Model
432
+ one_to_many :tracks, :key=>[:disc_number, :number, :album_id], :primary_key=>[:disc_number, :number, :album_id], :one_to_one=>true
433
+ end
434
+
435
+ Here's the old way to do it via custom associations:
436
+
437
+ class Track < Sequel::Model
438
+ many_to_one :favorite_track, \
439
+ :dataset=>(proc do
440
+ FavoriteTrack.filter(:disc_number=>disc_number, :number=>number, :album_id=>album_id)
441
+ end), \
442
+ :eager_loader=>(proc do |key_hash, tracks, associations|
443
+ id_map = {}
444
+ tracks.each do |t|
445
+ t.associations[:favorite_track] = nil
446
+ id_map[[t[:album_id], t[:disc_number], t[:number]]] = t
447
+ end
448
+ FavoriteTrack.filter([:album_id, :disc_number, :number]=>id_map.keys).all do |ft|
449
+ if t = id_map[[ft[:album_id], ft[:disc_number], ft[:number]]]
450
+ t.associations[:favorite_track] = ft
451
+ end
452
+ end
453
+ end)
454
+ end
455
+
456
+ class FavoriteTrack < Sequel::Model
457
+ many_to_one :track, \
458
+ :dataset=>(proc do
459
+ Track.filter(:disc_number=>disc_number, :number=>number, :album_id=>album_id)
460
+ end), \
461
+ :eager_loader=>(proc do |key_hash, ftracks, associations|
462
+ id_map = {}
463
+ ftracks.each{|ft| id_map[[ft[:album_id], ft[:disc_number], ft[:number]]] = ft}
464
+ Track.filter([:album_id, :disc_number, :number]=>id_map.keys).all do |t|
465
+ id_map[[t[:album_id], t[:disc_number], t[:number]]].associations[:track] = t
466
+ end
467
+ end)
468
+ end
469
+
470
+ === Tree - All Ancestors and Descendents
471
+
472
+ Let's say you want to store a tree relationship in your database, it's pretty
473
+ simple:
474
+
475
+ class Node < Sequel::Model
476
+ many_to_one :parent, :class=>self
477
+ one_to_many :children, :key=>:parent_id, :class=>self
478
+ end
479
+
480
+ You can easily get a node's parent with node.parent, and a node's children with
481
+ node.children. You can even eager load the relationship up to a certain depth:
482
+
483
+ # Eager load three generations of generations of children for a given node
484
+ Node.filter(:id=>1).eager(:children=>{:children=>:children}).all.first
485
+ # Load parents and grandparents for a group of nodes
486
+ Node.filter{|o| o.id < 10}.eager(:parent=>:parent).all
487
+
488
+ What if you want to get all ancestors up to the root node, or all descendents,
489
+ without knowing the depth of the tree?
490
+
491
+ class Node < Sequel::Model
492
+ many_to_one :ancestors, :class=>self,
493
+ :eager_loader=>(proc do |key_hash, nodes, associations|
494
+ # Handle cases where the root node has the same parent_id as primary_key
495
+ # and also when it is NULL
496
+ non_root_nodes = nodes.reject do |n|
497
+ if [nil, n.pk].include?(n.parent_id)
498
+ # Make sure root nodes have their parent association set to nil
499
+ n.associations[:parent] = nil
500
+ true
501
+ else
502
+ false
503
+ end
504
+ end
505
+ unless non_root_nodes.empty?
506
+ id_map = {}
507
+ # Create an map of parent_ids to nodes that have that parent id
508
+ non_root_nodes.each{|n| (id_map[n.parent_id] ||= []) << n}
509
+ # Doesn't cause an infinte loop, because when only the root node
510
+ # is left, this is not called.
511
+ Node.filter(Node.primary_key=>id_map.keys).eager(:ancestors).all do |node|
512
+ # Populate the parent association for each node
513
+ id_map[node.pk].each{|n| n.associations[:parent] = node}
514
+ end
515
+ end
516
+ end)
517
+ many_to_one :descendants, :eager_loader=>(proc do |key_hash, nodes, associations|
518
+ id_map = {}
519
+ nodes.each do |n|
520
+ # Initialize an empty array of child associations for each parent node
521
+ n.associations[:children] = []
522
+ # Populate identity map of nodes
523
+ id_map[n.pk] = n
524
+ end
525
+ # Doesn't cause an infinite loop, because the :eager_loader is not called
526
+ # if no records are returned. Exclude id = parent_id to avoid infinite loop
527
+ # if the root note is one of the returned records and it has parent_id = id
528
+ # instead of parent_id = NULL.
529
+ Node.filter(:parent_id=>id_map.keys).exclude(:id=>:parent_id).eager(:descendants).all do |node|
530
+ # Get the parent from the identity map
531
+ parent = id_map[node.parent_id]
532
+ # Set the child's parent association to the parent
533
+ node.associations[:parent] = parent
534
+ # Add the child association to the array of children in the parent
535
+ parent.associations[:children] << node
536
+ end
537
+ end)
538
+ end
539
+
540
+ Note that unlike ActiveRecord, Sequel supports common table expressions, which allows you to use recursive queries.
541
+ The results are not the same as in the above case, as all descendents are stored in a single association,
542
+ but all descendants can be both lazy loaded or eager loaded in a single query (assuming your database
543
+ supports recursive common table expressions):
544
+
545
+ class Node < Sequel::Model
546
+ one_to_many :descendants, :class=>Node, :dataset=>(proc do
547
+ Node.from(:t).
548
+ with_recursive(:t, Node.filter(:parent_id=>pk),
549
+ Node.join(:t, :id=>:parent_id).
550
+ select(:nodes.*))
551
+ end),
552
+ :eager_loader=>(proc do |key_hash, nodes, associations|
553
+ id_map = key_hash[:id]
554
+ nodes.each{|n| n.associations[:descendants] = []}
555
+ Node.from(:t).
556
+ with_recursive(:t, Node.filter(:parent_id=>id_map.keys).
557
+ select(:parent_id___root, :id, :parent_id),
558
+ Node.join(:t, :id=>:parent_id).
559
+ select(:t__root, :nodes.*)).
560
+ all.each do |node|
561
+ if root = id_map[node.values.delete(:root)].first
562
+ root.associations[:descendants] << node
563
+ end
564
+ end
565
+ end)
566
+ end
567
+
568
+ You could modify the code to also store direct children relationships at the same time,
569
+ for functionality similar to the non-common table expression case.
570
+
571
+ === Joining multiple keys to a single key, through a third table
572
+
573
+ Let's say you have a database, of songs, lyrics, and artists. Each song
574
+ may or may not have a lyric (most songs are instrumental). The lyric can be
575
+ associated to an artist in each of four ways: composer, arranger, vocalist,
576
+ or lyricist. These may all be the same, or they could all be different, and
577
+ none of them are required. The songs table has a lyric_id field to associate
578
+ it to the lyric, and the lyric table has four fields to associate it to the
579
+ artist (composer_id, arranger_id, vocalist_id, and lyricist_id).
580
+
581
+ What you want to do is get all songs for a given artist, ordered by the song's
582
+ name, with no duplicates?
583
+
584
+ class Artist < Sequel::Model
585
+ one_to_many :songs, :order=>:songs__name, \
586
+ :dataset=>proc{Song.select(:songs.*).join(Lyric, :id=>:lyric_id, id=>[:composer_id, :arranger_id, :vocalist_id, :lyricist_id])}, \
587
+ :eager_loader=>(proc do |key_hash, records, associations|
588
+ h = key_hash[:id]
589
+ ids = h.keys
590
+ records.each{|r| r.associations[:songs] = []}
591
+ Song.select(:songs.*, :lyrics__composer_id, :lyrics__arranger_id, :lyrics__vocalist_id, :lyrics__lyricist_id)\
592
+ .join(Lyric, :id=>:lyric_id){{:composer_id=>ids, :arranger_id=>ids, :vocalist_id=>ids, :lyricist_id=>ids}.sql_or}\
593
+ .order(:songs__name).all do |song|
594
+ [:composer_id, :arranger_id, :vocalist_id, :lyricist_id].each do |x|
595
+ recs = h[song.values.delete(x)]
596
+ recs.each{|r| r.associations[:songs] << song} if recs
597
+ end
598
+ end
599
+ records.each{|r| r.associations[:songs].uniq!}
600
+ end)
601
+ end
602
+
603
+ === Statistics Associations (Sum of Associated Table Column)
604
+
605
+ In addition to getting associated records, you can use Sequel's association support
606
+ to get aggregate information for columns in associated tables (sums, averages, etc.).
607
+
608
+ Let's say you have a database with projects and tickets. A project can have many
609
+ tickets, and each ticket has a number of hours associated with it. You can use the
610
+ association support to create a Project association that gives the sum of hours for all
611
+ associated tickets.
612
+
613
+ class Project < Sequel::Model
614
+ one_to_many :tickets
615
+ many_to_one :ticket_hours, :read_only=>true, :key=>:id,
616
+ :dataset=>proc{Ticket.filter(:project_id=>id).select{sum(hours).as(hours)}},
617
+ :eager_loader=>(proc do |kh, projects, a|
618
+ projects.each{|p| p.associations[:ticket_hours] = nil}
619
+ Ticket.filter(:project_id=>kh[:id].keys).
620
+ group(:project_id).
621
+ select{[project_id, sum(hours).as(hours)]}.
622
+ all do |t|
623
+ p = kh[:id][t.values.delete(:project_id)].first
624
+ p.associations[:ticket_hours] = t
625
+ end
626
+ end)
627
+ # The association method returns a Ticket object with a single aggregate
628
+ # sum-of-hours value, but you want it to return an Integer/Float of just the
629
+ # sum of hours, so you call super and return just the sum-of-hours value.
630
+ # This works for both lazy loading and eager loading.
631
+ def ticket_hours
632
+ if s = super
633
+ s[:hours]
634
+ end
635
+ end
636
+ end
637
+ class Ticket < Sequel::Model
638
+ many_to_one :project
639
+ end
640
+
641
+ Note that it is often better to use a sum cache instead of this approach. You can implement
642
+ a sum cache using after_create and after_delete hooks, or using a database trigger
643
+ (the preferred method if you only have to support one database and that database supports
644
+ triggers).