viking-sequel 3.10.0

Sign up to get free protection for your applications and to get access to all the features.
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).