viking-sequel 3.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +3134 -0
- data/COPYING +19 -0
- data/README.rdoc +723 -0
- data/Rakefile +193 -0
- data/bin/sequel +196 -0
- data/doc/advanced_associations.rdoc +644 -0
- data/doc/cheat_sheet.rdoc +218 -0
- data/doc/dataset_basics.rdoc +106 -0
- data/doc/dataset_filtering.rdoc +158 -0
- data/doc/opening_databases.rdoc +296 -0
- data/doc/prepared_statements.rdoc +104 -0
- data/doc/reflection.rdoc +84 -0
- data/doc/release_notes/1.0.txt +38 -0
- data/doc/release_notes/1.1.txt +143 -0
- data/doc/release_notes/1.3.txt +101 -0
- data/doc/release_notes/1.4.0.txt +53 -0
- data/doc/release_notes/1.5.0.txt +155 -0
- data/doc/release_notes/2.0.0.txt +298 -0
- data/doc/release_notes/2.1.0.txt +271 -0
- data/doc/release_notes/2.10.0.txt +328 -0
- data/doc/release_notes/2.11.0.txt +215 -0
- data/doc/release_notes/2.12.0.txt +534 -0
- data/doc/release_notes/2.2.0.txt +253 -0
- data/doc/release_notes/2.3.0.txt +88 -0
- data/doc/release_notes/2.4.0.txt +106 -0
- data/doc/release_notes/2.5.0.txt +137 -0
- data/doc/release_notes/2.6.0.txt +157 -0
- data/doc/release_notes/2.7.0.txt +166 -0
- data/doc/release_notes/2.8.0.txt +171 -0
- data/doc/release_notes/2.9.0.txt +97 -0
- data/doc/release_notes/3.0.0.txt +221 -0
- data/doc/release_notes/3.1.0.txt +406 -0
- data/doc/release_notes/3.10.0.txt +286 -0
- data/doc/release_notes/3.2.0.txt +268 -0
- data/doc/release_notes/3.3.0.txt +192 -0
- data/doc/release_notes/3.4.0.txt +325 -0
- data/doc/release_notes/3.5.0.txt +510 -0
- data/doc/release_notes/3.6.0.txt +366 -0
- data/doc/release_notes/3.7.0.txt +179 -0
- data/doc/release_notes/3.8.0.txt +151 -0
- data/doc/release_notes/3.9.0.txt +233 -0
- data/doc/schema.rdoc +36 -0
- data/doc/sharding.rdoc +113 -0
- data/doc/virtual_rows.rdoc +205 -0
- data/lib/sequel.rb +1 -0
- data/lib/sequel/adapters/ado.rb +90 -0
- data/lib/sequel/adapters/ado/mssql.rb +30 -0
- data/lib/sequel/adapters/amalgalite.rb +176 -0
- data/lib/sequel/adapters/db2.rb +139 -0
- data/lib/sequel/adapters/dbi.rb +113 -0
- data/lib/sequel/adapters/do.rb +188 -0
- data/lib/sequel/adapters/do/mysql.rb +49 -0
- data/lib/sequel/adapters/do/postgres.rb +91 -0
- data/lib/sequel/adapters/do/sqlite.rb +40 -0
- data/lib/sequel/adapters/firebird.rb +283 -0
- data/lib/sequel/adapters/informix.rb +77 -0
- data/lib/sequel/adapters/jdbc.rb +587 -0
- data/lib/sequel/adapters/jdbc/as400.rb +58 -0
- data/lib/sequel/adapters/jdbc/h2.rb +133 -0
- data/lib/sequel/adapters/jdbc/mssql.rb +57 -0
- data/lib/sequel/adapters/jdbc/mysql.rb +78 -0
- data/lib/sequel/adapters/jdbc/oracle.rb +50 -0
- data/lib/sequel/adapters/jdbc/postgresql.rb +108 -0
- data/lib/sequel/adapters/jdbc/sqlite.rb +55 -0
- data/lib/sequel/adapters/mysql.rb +421 -0
- data/lib/sequel/adapters/odbc.rb +143 -0
- data/lib/sequel/adapters/odbc/mssql.rb +42 -0
- data/lib/sequel/adapters/openbase.rb +64 -0
- data/lib/sequel/adapters/oracle.rb +131 -0
- data/lib/sequel/adapters/postgres.rb +504 -0
- data/lib/sequel/adapters/shared/mssql.rb +490 -0
- data/lib/sequel/adapters/shared/mysql.rb +498 -0
- data/lib/sequel/adapters/shared/oracle.rb +195 -0
- data/lib/sequel/adapters/shared/postgres.rb +830 -0
- data/lib/sequel/adapters/shared/progress.rb +44 -0
- data/lib/sequel/adapters/shared/sqlite.rb +389 -0
- data/lib/sequel/adapters/sqlite.rb +224 -0
- data/lib/sequel/adapters/utils/stored_procedures.rb +84 -0
- data/lib/sequel/connection_pool.rb +99 -0
- data/lib/sequel/connection_pool/sharded_single.rb +84 -0
- data/lib/sequel/connection_pool/sharded_threaded.rb +211 -0
- data/lib/sequel/connection_pool/single.rb +29 -0
- data/lib/sequel/connection_pool/threaded.rb +150 -0
- data/lib/sequel/core.rb +293 -0
- data/lib/sequel/core_sql.rb +241 -0
- data/lib/sequel/database.rb +1079 -0
- data/lib/sequel/database/schema_generator.rb +327 -0
- data/lib/sequel/database/schema_methods.rb +203 -0
- data/lib/sequel/database/schema_sql.rb +320 -0
- data/lib/sequel/dataset.rb +32 -0
- data/lib/sequel/dataset/actions.rb +441 -0
- data/lib/sequel/dataset/features.rb +86 -0
- data/lib/sequel/dataset/graph.rb +254 -0
- data/lib/sequel/dataset/misc.rb +119 -0
- data/lib/sequel/dataset/mutation.rb +64 -0
- data/lib/sequel/dataset/prepared_statements.rb +227 -0
- data/lib/sequel/dataset/query.rb +709 -0
- data/lib/sequel/dataset/sql.rb +996 -0
- data/lib/sequel/exceptions.rb +51 -0
- data/lib/sequel/extensions/blank.rb +43 -0
- data/lib/sequel/extensions/inflector.rb +242 -0
- data/lib/sequel/extensions/looser_typecasting.rb +21 -0
- data/lib/sequel/extensions/migration.rb +239 -0
- data/lib/sequel/extensions/named_timezones.rb +61 -0
- data/lib/sequel/extensions/pagination.rb +100 -0
- data/lib/sequel/extensions/pretty_table.rb +82 -0
- data/lib/sequel/extensions/query.rb +52 -0
- data/lib/sequel/extensions/schema_dumper.rb +271 -0
- data/lib/sequel/extensions/sql_expr.rb +122 -0
- data/lib/sequel/extensions/string_date_time.rb +46 -0
- data/lib/sequel/extensions/thread_local_timezones.rb +48 -0
- data/lib/sequel/metaprogramming.rb +9 -0
- data/lib/sequel/model.rb +120 -0
- data/lib/sequel/model/associations.rb +1514 -0
- data/lib/sequel/model/base.rb +1069 -0
- data/lib/sequel/model/default_inflections.rb +45 -0
- data/lib/sequel/model/errors.rb +39 -0
- data/lib/sequel/model/exceptions.rb +21 -0
- data/lib/sequel/model/inflections.rb +162 -0
- data/lib/sequel/model/plugins.rb +70 -0
- data/lib/sequel/plugins/active_model.rb +59 -0
- data/lib/sequel/plugins/association_dependencies.rb +103 -0
- data/lib/sequel/plugins/association_proxies.rb +41 -0
- data/lib/sequel/plugins/boolean_readers.rb +53 -0
- data/lib/sequel/plugins/caching.rb +141 -0
- data/lib/sequel/plugins/class_table_inheritance.rb +214 -0
- data/lib/sequel/plugins/composition.rb +138 -0
- data/lib/sequel/plugins/force_encoding.rb +72 -0
- data/lib/sequel/plugins/hook_class_methods.rb +126 -0
- data/lib/sequel/plugins/identity_map.rb +116 -0
- data/lib/sequel/plugins/instance_filters.rb +98 -0
- data/lib/sequel/plugins/instance_hooks.rb +57 -0
- data/lib/sequel/plugins/lazy_attributes.rb +77 -0
- data/lib/sequel/plugins/many_through_many.rb +208 -0
- data/lib/sequel/plugins/nested_attributes.rb +206 -0
- data/lib/sequel/plugins/optimistic_locking.rb +81 -0
- data/lib/sequel/plugins/rcte_tree.rb +281 -0
- data/lib/sequel/plugins/schema.rb +66 -0
- data/lib/sequel/plugins/serialization.rb +166 -0
- data/lib/sequel/plugins/single_table_inheritance.rb +74 -0
- data/lib/sequel/plugins/subclasses.rb +45 -0
- data/lib/sequel/plugins/tactical_eager_loading.rb +61 -0
- data/lib/sequel/plugins/timestamps.rb +87 -0
- data/lib/sequel/plugins/touch.rb +118 -0
- data/lib/sequel/plugins/typecast_on_load.rb +72 -0
- data/lib/sequel/plugins/validation_class_methods.rb +405 -0
- data/lib/sequel/plugins/validation_helpers.rb +223 -0
- data/lib/sequel/sql.rb +1020 -0
- data/lib/sequel/timezones.rb +161 -0
- data/lib/sequel/version.rb +12 -0
- data/lib/sequel_core.rb +1 -0
- data/lib/sequel_model.rb +1 -0
- data/spec/adapters/firebird_spec.rb +407 -0
- data/spec/adapters/informix_spec.rb +97 -0
- data/spec/adapters/mssql_spec.rb +403 -0
- data/spec/adapters/mysql_spec.rb +1019 -0
- data/spec/adapters/oracle_spec.rb +286 -0
- data/spec/adapters/postgres_spec.rb +969 -0
- data/spec/adapters/spec_helper.rb +51 -0
- data/spec/adapters/sqlite_spec.rb +432 -0
- data/spec/core/connection_pool_spec.rb +808 -0
- data/spec/core/core_sql_spec.rb +417 -0
- data/spec/core/database_spec.rb +1662 -0
- data/spec/core/dataset_spec.rb +3827 -0
- data/spec/core/expression_filters_spec.rb +595 -0
- data/spec/core/object_graph_spec.rb +296 -0
- data/spec/core/schema_generator_spec.rb +159 -0
- data/spec/core/schema_spec.rb +830 -0
- data/spec/core/spec_helper.rb +56 -0
- data/spec/core/version_spec.rb +7 -0
- data/spec/extensions/active_model_spec.rb +76 -0
- data/spec/extensions/association_dependencies_spec.rb +127 -0
- data/spec/extensions/association_proxies_spec.rb +50 -0
- data/spec/extensions/blank_spec.rb +67 -0
- data/spec/extensions/boolean_readers_spec.rb +92 -0
- data/spec/extensions/caching_spec.rb +250 -0
- data/spec/extensions/class_table_inheritance_spec.rb +252 -0
- data/spec/extensions/composition_spec.rb +194 -0
- data/spec/extensions/force_encoding_spec.rb +117 -0
- data/spec/extensions/hook_class_methods_spec.rb +470 -0
- data/spec/extensions/identity_map_spec.rb +202 -0
- data/spec/extensions/inflector_spec.rb +181 -0
- data/spec/extensions/instance_filters_spec.rb +55 -0
- data/spec/extensions/instance_hooks_spec.rb +133 -0
- data/spec/extensions/lazy_attributes_spec.rb +153 -0
- data/spec/extensions/looser_typecasting_spec.rb +39 -0
- data/spec/extensions/many_through_many_spec.rb +884 -0
- data/spec/extensions/migration_spec.rb +332 -0
- data/spec/extensions/named_timezones_spec.rb +72 -0
- data/spec/extensions/nested_attributes_spec.rb +396 -0
- data/spec/extensions/optimistic_locking_spec.rb +100 -0
- data/spec/extensions/pagination_spec.rb +99 -0
- data/spec/extensions/pretty_table_spec.rb +91 -0
- data/spec/extensions/query_spec.rb +85 -0
- data/spec/extensions/rcte_tree_spec.rb +205 -0
- data/spec/extensions/schema_dumper_spec.rb +357 -0
- data/spec/extensions/schema_spec.rb +127 -0
- data/spec/extensions/serialization_spec.rb +209 -0
- data/spec/extensions/single_table_inheritance_spec.rb +96 -0
- data/spec/extensions/spec_helper.rb +91 -0
- data/spec/extensions/sql_expr_spec.rb +89 -0
- data/spec/extensions/string_date_time_spec.rb +93 -0
- data/spec/extensions/subclasses_spec.rb +52 -0
- data/spec/extensions/tactical_eager_loading_spec.rb +65 -0
- data/spec/extensions/thread_local_timezones_spec.rb +45 -0
- data/spec/extensions/timestamps_spec.rb +150 -0
- data/spec/extensions/touch_spec.rb +155 -0
- data/spec/extensions/typecast_on_load_spec.rb +69 -0
- data/spec/extensions/validation_class_methods_spec.rb +984 -0
- data/spec/extensions/validation_helpers_spec.rb +438 -0
- data/spec/integration/associations_test.rb +281 -0
- data/spec/integration/database_test.rb +26 -0
- data/spec/integration/dataset_test.rb +963 -0
- data/spec/integration/eager_loader_test.rb +734 -0
- data/spec/integration/model_test.rb +130 -0
- data/spec/integration/plugin_test.rb +814 -0
- data/spec/integration/prepared_statement_test.rb +213 -0
- data/spec/integration/schema_test.rb +361 -0
- data/spec/integration/spec_helper.rb +73 -0
- data/spec/integration/timezone_test.rb +55 -0
- data/spec/integration/transaction_test.rb +122 -0
- data/spec/integration/type_test.rb +96 -0
- data/spec/model/association_reflection_spec.rb +175 -0
- data/spec/model/associations_spec.rb +2633 -0
- data/spec/model/base_spec.rb +418 -0
- data/spec/model/dataset_methods_spec.rb +78 -0
- data/spec/model/eager_loading_spec.rb +1391 -0
- data/spec/model/hooks_spec.rb +240 -0
- data/spec/model/inflector_spec.rb +26 -0
- data/spec/model/model_spec.rb +593 -0
- data/spec/model/plugins_spec.rb +236 -0
- data/spec/model/record_spec.rb +1500 -0
- data/spec/model/spec_helper.rb +97 -0
- data/spec/model/validations_spec.rb +153 -0
- data/spec/rcov.opts +6 -0
- data/spec/spec_config.rb.example +10 -0
- metadata +346 -0
data/Rakefile
ADDED
@@ -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
|
data/bin/sequel
ADDED
@@ -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).
|