sequel 3.5.0 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/CHANGELOG +108 -0
  2. data/README.rdoc +25 -14
  3. data/Rakefile +20 -1
  4. data/doc/advanced_associations.rdoc +61 -64
  5. data/doc/cheat_sheet.rdoc +16 -7
  6. data/doc/opening_databases.rdoc +3 -3
  7. data/doc/prepared_statements.rdoc +1 -1
  8. data/doc/reflection.rdoc +2 -1
  9. data/doc/release_notes/3.6.0.txt +366 -0
  10. data/doc/schema.rdoc +19 -14
  11. data/lib/sequel/adapters/amalgalite.rb +5 -27
  12. data/lib/sequel/adapters/jdbc.rb +13 -3
  13. data/lib/sequel/adapters/jdbc/h2.rb +17 -0
  14. data/lib/sequel/adapters/jdbc/mysql.rb +20 -7
  15. data/lib/sequel/adapters/mysql.rb +4 -3
  16. data/lib/sequel/adapters/oracle.rb +1 -1
  17. data/lib/sequel/adapters/postgres.rb +87 -28
  18. data/lib/sequel/adapters/shared/mssql.rb +47 -6
  19. data/lib/sequel/adapters/shared/mysql.rb +12 -31
  20. data/lib/sequel/adapters/shared/postgres.rb +15 -12
  21. data/lib/sequel/adapters/shared/sqlite.rb +18 -0
  22. data/lib/sequel/adapters/sqlite.rb +1 -16
  23. data/lib/sequel/connection_pool.rb +1 -1
  24. data/lib/sequel/core.rb +1 -1
  25. data/lib/sequel/database.rb +1 -1
  26. data/lib/sequel/database/schema_generator.rb +2 -0
  27. data/lib/sequel/database/schema_sql.rb +1 -1
  28. data/lib/sequel/dataset.rb +5 -179
  29. data/lib/sequel/dataset/actions.rb +123 -0
  30. data/lib/sequel/dataset/convenience.rb +18 -10
  31. data/lib/sequel/dataset/features.rb +65 -0
  32. data/lib/sequel/dataset/prepared_statements.rb +29 -23
  33. data/lib/sequel/dataset/query.rb +429 -0
  34. data/lib/sequel/dataset/sql.rb +67 -435
  35. data/lib/sequel/model/associations.rb +77 -13
  36. data/lib/sequel/model/base.rb +30 -8
  37. data/lib/sequel/model/errors.rb +4 -4
  38. data/lib/sequel/plugins/caching.rb +38 -15
  39. data/lib/sequel/plugins/force_encoding.rb +18 -7
  40. data/lib/sequel/plugins/hook_class_methods.rb +4 -0
  41. data/lib/sequel/plugins/many_through_many.rb +1 -1
  42. data/lib/sequel/plugins/nested_attributes.rb +40 -11
  43. data/lib/sequel/plugins/serialization.rb +17 -3
  44. data/lib/sequel/plugins/validation_helpers.rb +65 -18
  45. data/lib/sequel/sql.rb +23 -1
  46. data/lib/sequel/version.rb +1 -1
  47. data/spec/adapters/mssql_spec.rb +96 -10
  48. data/spec/adapters/mysql_spec.rb +19 -0
  49. data/spec/adapters/postgres_spec.rb +65 -2
  50. data/spec/adapters/sqlite_spec.rb +10 -0
  51. data/spec/core/core_sql_spec.rb +9 -0
  52. data/spec/core/database_spec.rb +8 -4
  53. data/spec/core/dataset_spec.rb +122 -29
  54. data/spec/core/expression_filters_spec.rb +17 -0
  55. data/spec/extensions/caching_spec.rb +43 -3
  56. data/spec/extensions/force_encoding_spec.rb +43 -1
  57. data/spec/extensions/nested_attributes_spec.rb +55 -2
  58. data/spec/extensions/validation_helpers_spec.rb +71 -0
  59. data/spec/integration/associations_test.rb +281 -0
  60. data/spec/integration/dataset_test.rb +383 -9
  61. data/spec/integration/eager_loader_test.rb +0 -65
  62. data/spec/integration/model_test.rb +110 -0
  63. data/spec/integration/plugin_test.rb +306 -0
  64. data/spec/integration/prepared_statement_test.rb +32 -0
  65. data/spec/integration/schema_test.rb +8 -3
  66. data/spec/integration/spec_helper.rb +1 -25
  67. data/spec/model/association_reflection_spec.rb +38 -0
  68. data/spec/model/associations_spec.rb +184 -8
  69. data/spec/model/eager_loading_spec.rb +23 -0
  70. data/spec/model/model_spec.rb +8 -0
  71. data/spec/model/record_spec.rb +84 -1
  72. metadata +9 -2
data/CHANGELOG CHANGED
@@ -1,3 +1,111 @@
1
+ === 3.6.0 (2009-11-02)
2
+
3
+ * Make the MSSQL shared adapter correctly parse the column schema information for tables in the non-default database schema (rohit.namjoshi)
4
+
5
+ * Use save_changes instead of save when updating existing associated objects in the nested_attributes plugin (jeremyevans)
6
+
7
+ * Allow Model#save_changes to accept an option hash that is passed to save, so you can save changes without validating (jeremyevans)
8
+
9
+ * Make nested_attributes plugin add newly created objects to cached association array immediately (jeremyevans)
10
+
11
+ * Make add_ association method not add the associated object to the cached array if it's already there (jeremyevans)
12
+
13
+ * Add Model#modified! for explicitly marking an object as modified, so save_changes/update will run callbacks even if no columns have been modified (jeremyevans)
14
+
15
+ * Add support for a :fields option in the nested attributes plugin, and only allow updating of the fields specified (jeremyevans)
16
+
17
+ * Don't allow modifying keys related to the association when updating existing objects in the nested_attributes plugin (jeremyevans)
18
+
19
+ * Add associated_object_keys method to AssociationReflection objects, specifying the key(s) in the associated model table related to the association (jeremyevans)
20
+
21
+ * Support the memcached protocol in the caching plugin via the new :ignore_exceptions option (EppO, jeremyevans)
22
+
23
+ * Don't modify array with a string and placeholders passed to Dataset#filter or related methods (jeremyevans)
24
+
25
+ * Speed up Amalgalite adapter (copiousfreetime)
26
+
27
+ * Fix bound variables on PostgreSQL when using nil and potentially other values (jeremyevans)
28
+
29
+ * Allow easier overriding of default options used in the validation_helpers plugin (jeremyevans)
30
+
31
+ * Have Dataset#literal_other call sql_literal on the object if it responds to it (heda, michaeldiamond)
32
+
33
+ * Fix Dataset#explain in the amalgalite adapter (jeremyevans)
34
+
35
+ * Have Model.table_name respect table aliases (jeremyevans)
36
+
37
+ * Allow marshalling of saved model records after calling #marshallable! (jeremyevans)
38
+
39
+ * one_to_many association methods now make sure that the removed object is currently associated to the receiver (jeremyevans)
40
+
41
+ * Model association add_ and remove_ methods now have more descriptive error messages (jeremyevans)
42
+
43
+ * Model association add_ and remove_ methods now make sure passed object is of the correct class (jeremyevans)
44
+
45
+ * Model association remove_ methods now accept a primary key value and disassociate the associated model object (natewiger, jeremyevans)
46
+
47
+ * Model association add_ methods now accept a hash and create a new associated model object (natewiger, jeremyevans)
48
+
49
+ * Dataset#window for PostgreSQL datasets now respects previous windows (jeremyevans)
50
+
51
+ * Dataset#simple_select_all? now ignores options that don't affect the SQL being issued (jeremyevans)
52
+
53
+ * Account for table aliases in eager_graph (mluu)
54
+
55
+ * Add support for MSSQL clustered index creation (mluu)
56
+
57
+ * Implement insert_select in the MSSQL adapter via OUTPUT. Can be disabled via disable_insert_output. (jfirebaugh, mluu)
58
+
59
+ * Correct error handling when beginning a transaction fails (jfirebaugh, mluu)
60
+
61
+ * Correct JDBC binding for Time objects in prepared statements (jfirebaugh, jeremyevans)
62
+
63
+ * Emulate JOIN USING clause poorly using JOIN ON if the database doesn't support JOIN USING (e.g. MSSQL, H2) (jfirebaugh, jeremyevans)
64
+
65
+ * Support column aliases in Dataset#group_and_count (jfirebaugh)
66
+
67
+ * Support preparing insert statements of the form insert(1,2,3) and insert(columns, values) (jfirebaugh)
68
+
69
+ * Fix add_index for tables in non-default schema (jfirebaugh)
70
+
71
+ * Allow named placeholders in placeholder literal strings (jeremyevans)
72
+
73
+ * Allow the force_encoding plugin to work when refreshing (jeremyevans)
74
+
75
+ * Add Dataset#bind for setting bound variable values before calling #call (jeremyevans)
76
+
77
+ * Add additional join methods to Dataset: (cross|natural|(natural_)?(full|left|right))_join (jeremyevans)
78
+
79
+ * Fix use a dataset aggregate methods (e.g. sum) on limited/grouped/etc. datasets (jeremyevans)
80
+
81
+ * Clear changed_columns when saving new model objects with a database adapter that supports insert_select, such as postgres (jeremyevans)
82
+
83
+ * Fix Dataset#replace with default values on MySQL, and respect insert-related options (jeremyevans)
84
+
85
+ * Fix Dataset#lock on PostgreSQL (jeremyevans)
86
+
87
+ * Fix Dataset#explain on SQLite (jeremyevans)
88
+
89
+ * Add Dataset#use_cursor to the native postgres adapter, for processing large datasets (jeremyevans)
90
+
91
+ * Don't ignore Class.inherited in Sequel::Model.inherited (antage) (#277)
92
+
93
+ * Optimize JDBC::MySQL::DatabaseMethods#last_insert_id to prevent additional queries (tmm1)
94
+
95
+ * Fix use of MSSQL with ruby 1.9 (cult hero)
96
+
97
+ * Don't try to load associated objects when the current object has NULL for one of the key fields (jeremyevans)
98
+
99
+ * No longer require GROUP BY to use HAVING, except on SQLite (jeremyevans)
100
+
101
+ * Add emulated support for the lack of multiple column IN/NOT IN support in MSSQL and SQLite (jeremyevans)
102
+
103
+ * Add emulated support for #ilike on MSSQL and H2 (jeremyevans)
104
+
105
+ * Add a :distinct option for all associations, which uses the SQL DISTINCT clause (jeremyevans)
106
+
107
+ * Don't require :: prefix for constant lookups in instance_evaled virtual row blocks on ruby 1.9 (jeremyevans)
108
+
1
109
  === 3.5.0 (2009-10-01)
2
110
 
3
111
  * Correctly literalize timezones in timestamps when using Oracle (jeremyevans)
@@ -75,13 +75,13 @@ Sequel is designed to take the hassle away from connecting to databases and mani
75
75
 
76
76
  Sequel uses the concept of datasets to retrieve data. A Dataset object encapsulates an SQL query and supports chainability, letting you fetch data using a convenient Ruby DSL that is both concise and flexible.
77
77
 
78
- For example, the following one-liner returns the average GDP for the five biggest countries in the middle east region:
78
+ For example, the following one-liner returns the average GDP for countries in the middle east region:
79
79
 
80
- DB[:countries].filter(:region => 'Middle East').reverse_order(:area).limit(5).avg(:GDP)
80
+ DB[:countries].filter(:region => 'Middle East').avg(:GDP)
81
81
 
82
82
  Which is equivalent to:
83
83
 
84
- SELECT avg(GDP) FROM countries WHERE region = 'Middle East' ORDER BY area DESC LIMIT 5
84
+ SELECT avg(GDP) FROM countries WHERE region = 'Middle East'
85
85
 
86
86
  Since datasets retrieve records only when needed, they can be stored and later reused. Records are fetched as hashes (or custom model objects), and are accessed using an Enumerable interface:
87
87
 
@@ -120,8 +120,10 @@ You can specify a block to connect, which will disconnect from the database afte
120
120
 
121
121
  === Arbitrary SQL queries
122
122
 
123
- DB << "create table t (a text, b text)"
124
- DB << "insert into t values ('a', 'b')"
123
+ You can execute arbitrary SQL code using Database#run:
124
+
125
+ DB.run("create table t (a text, b text)")
126
+ DB.run("insert into t values ('a', 'b')")
125
127
 
126
128
  You can also create datasets based on raw SQL:
127
129
 
@@ -137,6 +139,7 @@ You can also fetch records with raw SQL through the dataset:
137
139
 
138
140
  You can use placeholders in your SQL string as well:
139
141
 
142
+ name = 'Jim'
140
143
  DB['select * from items where name = ?', name].each do |row|
141
144
  p row
142
145
  end
@@ -407,7 +410,7 @@ Sequel models allow you to use any column as a primary key, and even composite k
407
410
  post = Post['ruby', 'hello world']
408
411
  post.pk #=> ['ruby', 'hello world']
409
412
 
410
- You can also define a model class that does not have a primary key, but then you lose the ability to update records.
413
+ You can also define a model class that does not have a primary key, but then you lose the ability to easily update records.
411
414
 
412
415
  A model instance can also be fetched by specifying a condition:
413
416
 
@@ -439,9 +442,14 @@ You can read the record values as object attributes (assuming the attribute name
439
442
  You can also change record values:
440
443
 
441
444
  post.title = 'hey there'
445
+ # or
446
+ post.set(:title=>'hey there')
447
+
448
+ That will just change the value for the object, it will not persist the changes to the database. To persist the record, call the #save method:
449
+
442
450
  post.save
443
451
 
444
- Another way to change values by using the #update method:
452
+ If you want to modify record values and save the object after doing so, use the #update method:
445
453
 
446
454
  post.update(:title => 'hey there')
447
455
 
@@ -451,7 +459,7 @@ New records can be created by calling Model.create:
451
459
 
452
460
  post = Post.create(:title => 'hello world')
453
461
 
454
- Another way is to construct a new instance and save it:
462
+ Another way is to construct a new instance and save it later:
455
463
 
456
464
  post = Post.new
457
465
  post.title = 'hello world'
@@ -459,27 +467,30 @@ Another way is to construct a new instance and save it:
459
467
 
460
468
  You can also supply a block to Model.new and Model.create:
461
469
 
462
- post = Post.create{|p| p.title = 'hello world'}
463
-
464
470
  post = Post.new do |p|
465
471
  p.title = 'hello world'
466
- p.save
467
472
  end
468
473
 
474
+ post = Post.create{|p| p.title = 'hello world'}
475
+
469
476
  === Hooks
470
477
 
471
478
  You can execute custom code when creating, updating, or deleting records by defining hook methods. The before_create and after_create hook methods wrap record creation. The before_update and after_update hook methods wrap record updating. The before_save and after_save hook methods wrap record creation and updating. The before_destroy and after_destroy hook methods wrap destruction. The before_validation and after_validation hook methods wrap validation. Example:
472
479
 
473
480
  class Post < Sequel::Model
474
481
  def after_create
482
+ super
475
483
  author.increase_post_count
476
484
  end
477
485
 
478
486
  def after_destroy
487
+ super
479
488
  author.decrease_post_count
480
489
  end
481
490
  end
482
491
 
492
+ Note the use of super if you define your own hook methods. Almost all Sequel::Model class and instance methods (not just hook methods) can be overridden safely, but you have to make sure to call super when doing so, otherwise you risk breaking things.
493
+
483
494
  For the example above, you should probably use a database trigger if you can. Hooks can be used for data integrity, but they will only enforce that integrity when you are using the model. If you plan on allowing any other access to the database, it's best to use database triggers for data integrity.
484
495
 
485
496
  === Deleting records
@@ -636,7 +647,7 @@ Sequel models also provide a short hand notation for filters:
636
647
 
637
648
  === Model Validations
638
649
 
639
- You can define a validate method for your model, which save
650
+ You can define a validate method for your model, which #save
640
651
  will check before attempting to save the model in the database.
641
652
  If an attribute of the model isn't valid, you should add a error
642
653
  message for that attribute to the model object's errors. If an
@@ -645,8 +656,8 @@ raise an error or return false depending on how it is configured.
645
656
 
646
657
  class Post < Sequel::Model
647
658
  def validate
648
- errors[:name] << "can't be empty" if name.empty?
649
- errors[:written_on] << "should be in the past" if written_on >= Time.now
659
+ errors.add(:name, "can't be empty") if name.empty?
660
+ errors.add(:written_on, "should be in the past") if written_on >= Time.now
650
661
  end
651
662
  end
652
663
 
data/Rakefile CHANGED
@@ -168,12 +168,26 @@ begin
168
168
  t.spec_opts = spec_opts.call
169
169
  end
170
170
 
171
+ desc "Run integration tests with coverage"
172
+ Spec::Rake::SpecTask.new("integration_cov") do |t|
173
+ t.spec_files = Dir["spec/integration/*_test.rb"]
174
+ t.spec_opts = spec_opts.call
175
+ t.rcov, t.rcov_opts = rcov_opts.call
176
+ end
177
+
171
178
  %w'postgres sqlite mysql informix oracle firebird mssql'.each do |adapter|
172
- desc "Run #{adapter} specs without coverage"
179
+ desc "Run #{adapter} specs"
173
180
  Spec::Rake::SpecTask.new("spec_#{adapter}") do |t|
174
181
  t.spec_files = ["spec/adapters/#{adapter}_spec.rb"] + Dir["spec/integration/*_test.rb"]
175
182
  t.spec_opts = spec_opts.call
176
183
  end
184
+
185
+ desc "Run #{adapter} specs with coverage"
186
+ Spec::Rake::SpecTask.new("spec_#{adapter}_cov") do |t|
187
+ t.spec_files = ["spec/adapters/#{adapter}_spec.rb"] + Dir["spec/integration/*_test.rb"]
188
+ t.spec_opts = spec_opts.call
189
+ t.rcov, t.rcov_opts = rcov_opts.call
190
+ end
177
191
  end
178
192
  rescue LoadError
179
193
  end
@@ -197,3 +211,8 @@ desc "Print Sequel version"
197
211
  task :version do
198
212
  puts VERS.call
199
213
  end
214
+
215
+ desc "Check syntax of all .rb files"
216
+ task :check_syntax do
217
+ Dir['**/*.rb'].each{|file| print `#{ENV['RUBY'] || :ruby} -c #{file} | fgrep -v "Syntax OK"`}
218
+ end
@@ -196,8 +196,8 @@ Sequel::Model:
196
196
  @author = Author.first
197
197
  @author.books
198
198
 
199
- If you use an association other than belongs_to in the associated model, things
200
- are a bit more involved (has_many :through a has_many association):
199
+ If you use an association other than belongs_to in the associated model, you'll have
200
+ to specify some of the :*key options and write a short method.
201
201
 
202
202
  ActiveRecord:
203
203
 
@@ -222,23 +222,7 @@ Sequel::Model:
222
222
 
223
223
  class Firm < Sequel::Model
224
224
  one_to_many :clients
225
- one_to_many :invoices, :read_only=>true, \
226
- :dataset=>proc{Invoice.eager_graph(:client).filter(:client__firm_id=>pk)}, \
227
- :after_load=>(proc do |firm, invs|
228
- invs.each do |inv|
229
- inv.client.associations[:firm] = inv.associations[:firm] = firm
230
- end
231
- end), \
232
- :eager_loader=>(proc do |key_hash, firms, associations|
233
- id_map = key_hash[Firm.primary_key]
234
- firms.each{|firm| firm.associations[:invoices] = []}
235
- Invoice.eager_graph(:client).filter(:client__firm_id=>id_map.keys).all do |inv|
236
- id_map[inv.client.firm_id].each do |firm|
237
- inv.client.associations[:firm] = inv.associations[:firm] = firm
238
- firm.associations[:invoices] << inv
239
- end
240
- end
241
- end)
225
+ many_to_many :invoices, :join_table=>:clients, :right_key=>:id, :right_primary_key=>:client_id
242
226
  end
243
227
 
244
228
  class Client < Sequel::Model
@@ -248,55 +232,28 @@ Sequel::Model:
248
232
 
249
233
  class Invoice < Sequel::Model
250
234
  many_to_one :client
251
- many_to_one :firm, :key=>nil, :read_only=>true, \
252
- :dataset=>proc{Firm.eager_graph(:clients).filter(:clients__id=>client_id)}, \
253
- :after_load=>(proc do |inv, firm|
254
- # Delete the cached associations from firm, because it only has the
255
- # client with this invoice, instead of all clients of the firm
256
- inv.associations[:client] = firm.associations.delete(:clients).first
257
- end), \
258
- :eager_loader=>(proc do |key_hash, invoices, associations|
259
- id_map = {}
260
- invoices.each do |inv|
261
- inv.associations[:firm] = nil
262
- inv.associations[:client] = nil
263
- (id_map[inv.client_id] ||= []) << inv
264
- end
265
- Firm.eager_graph(:clients).filter(:clients__id=>id_map.keys).all do |firm|
266
- # Delete the cached associations from firm, because it only has the
267
- # clients related the invoices being eagerly loaded, instead of all
268
- # clients of the firm.
269
- firm.associations.delete(:clients).each do |client|
270
- id_map[client.pk].each do |inv|
271
- inv.associations[:firm] = firm
272
- inv.associations[:client] = client
273
- end
274
- end
275
- end
276
- end)
235
+
236
+ def firm
237
+ client.firm if client
238
+ end
277
239
  end
278
- Firm.find(:first).invoices
279
240
 
280
- It is significantly more code in Sequel Model, but quite a bit of it is setting
281
- the intermediate associated record (the client) and the reciprocal association
282
- in the associations cache for each object, which ActiveRecord won't do for you.
283
- The reason you would want to do this is that then firm.invoices.first.firm or
284
- firm.invoices.first.client doesn't do another query to get the firm/client.
241
+ Firm.first.invoices
285
242
 
286
243
  === Polymorphic Associations
287
244
 
288
- Polymorphic associations are really a design flaw. The only advantage
289
- polymorphic associations offer is that they require fewer join tables.
245
+ Sequel discourages the use of polymorphic associations, which is the reason they
246
+ are not supported by default. All polymorphic associations can be made non-polymorphic
247
+ by using additional tables and/or columns instead of having a column
248
+ containing the associated class name as a string.
290
249
 
291
- Proof by Reductio ad absurdum: If fewer join tables are preferable, then surely
292
- fewer tables and columns are preferrable, so you might as well store all of
293
- your data in a single column in a single table if you think polymorphic
294
- associations are a good idea.
250
+ Polymorphic associations break referential integrity and are significantly more
251
+ complex than non-polymorphic associations, so their use is not recommended unless
252
+ you are stuck with an existing design that uses them.
295
253
 
296
- Compelling Argument: Polymorphic associations are more complex than normal
297
- associations, and they break referential integrity, so the only reason you
298
- should use them is if you are already stuck with an existing design that
299
- uses them. You should never use them in new code.
254
+ If you must use them, look for the sequel_polymorphic plugin, as it makes using
255
+ polymorphic associations in Sequel about as easy as it is in ActiveRecord. However,
256
+ here's how they can be done using Sequel's custom associations:
300
257
 
301
258
  ActiveRecord:
302
259
 
@@ -464,12 +421,22 @@ design, but sometimes you have to play with the cards you are dealt).
464
421
  === Joining on multiple keys
465
422
 
466
423
  Let's say you have two tables that are associated with each other with multiple
467
- keys. For example:
424
+ keys. This can now be handled using Sequel's built in composite key support for
425
+ associations:
468
426
 
469
427
  # Both of these models have an album_id, number, and disc_number fields.
470
428
  # All FavoriteTracks have an associated track, but not all tracks have an
471
429
  # associated favorite track
472
430
 
431
+ class Track < Sequel::Model
432
+ many_to_one :favorite_track, :key=>[:disc_number, :number, :album_id], :primary_key=>[:disc_number, :number, :album_id]
433
+ end
434
+ class FavoriteTrack < Sequel::Model
435
+ one_to_many :tracks, :key=>[:disc_number, :number, :album_id], :primary_key=>[:disc_number, :number, :album_id], :one_to_one=>true
436
+ end
437
+
438
+ Here's the old way to do it via custom associations:
439
+
473
440
  class Track < Sequel::Model
474
441
  many_to_one :favorite_track, \
475
442
  :dataset=>(proc do
@@ -572,6 +539,37 @@ without knowing the depth of the tree?
572
539
  end)
573
540
  end
574
541
 
542
+ Note that unlike ActiveRecord, Sequel supports common table expressions, which allows you to use recursive queries.
543
+ The results are not the same as in the above case, as all descendents are stored in a single association,
544
+ but all descendants can be both lazy loaded or eager loaded in a single query (assuming your database
545
+ supports recursive common table expressions):
546
+
547
+ class Node < Sequel::Model
548
+ one_to_many :descendants, :class=>Node, :dataset=>(proc do
549
+ Node.from(:t).
550
+ with_recursive(:t, Node.filter(:parent_id=>pk),
551
+ Node.join(:t, :id=>:parent_id).
552
+ select(:nodes.*))
553
+ end),
554
+ :eager_loader=>(proc do |key_hash, nodes, associations|
555
+ id_map = key_hash[:id]
556
+ nodes.each{|n| n.associations[:descendants] = []}
557
+ Node.from(:t).
558
+ with_recursive(:t, Node.filter(:parent_id=>id_map.keys).
559
+ select(:parent_id___root, :id, :parent_id),
560
+ Node.join(:t, :id=>:parent_id).
561
+ select(:t__root, :nodes.*)).
562
+ all.each do |node|
563
+ if root = id_map[node.values.delete(:root)].first
564
+ root.associations[:descendants] << node
565
+ end
566
+ end
567
+ end)
568
+ end
569
+
570
+ You could modify the code to also store direct children relationships at the same time,
571
+ for functionality similar to the non-common table expression case.
572
+
575
573
  === Joining multiple keys to a single key, through a third table
576
574
 
577
575
  Let's say you have a database, of songs, lyrics, and artists. Each song
@@ -614,7 +612,6 @@ tickets, and each ticket has a number of hours associated with it. You can use
614
612
  association support to create a Project association that gives the sum of hours for all
615
613
  associated tickets.
616
614
 
617
-
618
615
  class Project < Sequel::Model
619
616
  one_to_many :tickets
620
617
  many_to_one :ticket_hours, :read_only=>true, :key=>:id,
@@ -644,6 +641,6 @@ associated tickets.
644
641
  end
645
642
 
646
643
  Note that it is often better to use a sum cache instead of this approach. You can implement
647
- a sum cache using before or after save and delete hooks, or using a database trigger
644
+ a sum cache using after_create and after_delete hooks, or using a database trigger
648
645
  (the preferred method if you only have to support one database and that database supports
649
646
  triggers).