sequel 4.7.0 → 4.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +46 -0
  3. data/README.rdoc +25 -1
  4. data/doc/active_record.rdoc +1 -1
  5. data/doc/advanced_associations.rdoc +143 -17
  6. data/doc/association_basics.rdoc +80 -59
  7. data/doc/release_notes/4.8.0.txt +175 -0
  8. data/lib/sequel/adapters/odbc.rb +1 -1
  9. data/lib/sequel/adapters/odbc/mssql.rb +4 -2
  10. data/lib/sequel/adapters/shared/postgres.rb +19 -3
  11. data/lib/sequel/adapters/shared/sqlite.rb +3 -3
  12. data/lib/sequel/ast_transformer.rb +1 -1
  13. data/lib/sequel/dataset/actions.rb +1 -1
  14. data/lib/sequel/dataset/graph.rb +23 -9
  15. data/lib/sequel/dataset/misc.rb +2 -2
  16. data/lib/sequel/dataset/sql.rb +3 -3
  17. data/lib/sequel/extensions/columns_introspection.rb +1 -1
  18. data/lib/sequel/extensions/mssql_emulate_lateral_with_apply.rb +1 -1
  19. data/lib/sequel/extensions/pg_array.rb +1 -1
  20. data/lib/sequel/extensions/pg_array_ops.rb +6 -0
  21. data/lib/sequel/extensions/pg_hstore_ops.rb +7 -0
  22. data/lib/sequel/extensions/pg_json_ops.rb +5 -0
  23. data/lib/sequel/extensions/query.rb +8 -2
  24. data/lib/sequel/extensions/to_dot.rb +1 -1
  25. data/lib/sequel/model/associations.rb +476 -152
  26. data/lib/sequel/plugins/class_table_inheritance.rb +11 -3
  27. data/lib/sequel/plugins/dataset_associations.rb +21 -18
  28. data/lib/sequel/plugins/many_through_many.rb +87 -20
  29. data/lib/sequel/plugins/nested_attributes.rb +12 -0
  30. data/lib/sequel/plugins/pg_array_associations.rb +31 -12
  31. data/lib/sequel/plugins/single_table_inheritance.rb +9 -1
  32. data/lib/sequel/sql.rb +1 -0
  33. data/lib/sequel/version.rb +1 -1
  34. data/spec/adapters/mssql_spec.rb +2 -2
  35. data/spec/adapters/postgres_spec.rb +7 -0
  36. data/spec/core/object_graph_spec.rb +250 -196
  37. data/spec/extensions/core_refinements_spec.rb +1 -1
  38. data/spec/extensions/dataset_associations_spec.rb +100 -6
  39. data/spec/extensions/many_through_many_spec.rb +1002 -19
  40. data/spec/extensions/nested_attributes_spec.rb +24 -0
  41. data/spec/extensions/pg_array_associations_spec.rb +17 -12
  42. data/spec/extensions/pg_array_spec.rb +4 -2
  43. data/spec/extensions/spec_helper.rb +1 -1
  44. data/spec/integration/associations_test.rb +1003 -48
  45. data/spec/integration/dataset_test.rb +12 -5
  46. data/spec/integration/prepared_statement_test.rb +1 -1
  47. data/spec/integration/type_test.rb +1 -1
  48. data/spec/model/associations_spec.rb +467 -130
  49. data/spec/model/eager_loading_spec.rb +332 -5
  50. metadata +5 -3
@@ -59,10 +59,18 @@ module Sequel
59
59
  #
60
60
  # # Set up class table inheritance in the parent class
61
61
  # # (Not in the subclasses)
62
- # Employee.plugin :class_table_inheritance
62
+ # class Employee < Sequel::Model
63
+ # plugin :class_table_inheritance
64
+ # end
63
65
  #
64
- # # Set the +kind+ column to hold the class name, and
65
- # # set the subclass table to map to for each subclass
66
+ # # Have subclasses inherit from the appropriate class
67
+ # class Staff < Employee; end
68
+ # class Manager < Employee; end
69
+ # class Executive < Manager; end
70
+ #
71
+ # # You can also set options when loading the plugin:
72
+ # # :kind :: column to hold the class name
73
+ # # :table_map :: map of class name symbols to table name symbols
66
74
  # Employee.plugin :class_table_inheritance, :key=>:kind, :table_map=>{:Staff=>:staff}
67
75
  module ClassTableInheritance
68
76
  # The class_table_inheritance plugin requires the lazy_attributes plugin
@@ -17,12 +17,10 @@ module Sequel
17
17
  # # WHERE ((id >= 1) AND (id <= 100))))
18
18
  #
19
19
  # This works for all of the association types that ship with Sequel,
20
- # including the many_through_many type. Most association options that
20
+ # including ones implemented in other plugins. Most association options that
21
21
  # are supported when eager loading are supported when using a
22
- # dataset association. However, associations that use :limit or
23
- # one_to_one associations that are really one_to_many relationships
24
- # in the database will not work correctly, returning all associated
25
- # objects.
22
+ # dataset association. However, it will only work for limited associations or
23
+ # *_one associations with orders if the database supports window functions.
26
24
  #
27
25
  # As the dataset methods return datasets, you can easily chain the
28
26
  # methods to get associated datasets of associated datasets:
@@ -66,10 +64,9 @@ module Sequel
66
64
  # such that it would return the union of calling the association method on
67
65
  # all objects returned by the current dataset.
68
66
  #
69
- # This supports most options that are supported when eager loading. It doesn't
70
- # support limits on the associations, or one_to_one associations that are really
71
- # one_to_many and use an order to select the first matching object. In both of
72
- # those cases, this will return an array of all matching objects.
67
+ # This supports most options that are supported when eager loading. However, it
68
+ # will only work for limited associations or *_one associations with orders if the
69
+ # database supports window functions.
73
70
  def associated(name)
74
71
  raise Error, "unrecognized association name: #{name.inspect}" unless r = model.association_reflection(name)
75
72
  ds = r.associated_class.dataset
@@ -78,17 +75,23 @@ module Sequel
78
75
  when :many_to_one
79
76
  ds.filter(r.qualified_primary_key=>sds.select(*Array(r[:qualified_key])))
80
77
  when :one_to_one, :one_to_many
81
- ds.filter(r.qualified_key=>sds.select(*Array(r.qualified_primary_key)))
82
- when :many_to_many
83
- ds.filter(r.qualified_right_primary_key=>sds.select(*Array(r.qualified_right_key)).
84
- join(r[:join_table], r[:left_keys].zip(r[:left_primary_keys]), :implicit_qualifier=>model.table_name))
85
- when :many_through_many
78
+ r.send(:apply_filter_by_associations_limit_strategy, ds.filter(r.qualified_key=>sds.select(*Array(r.qualified_primary_key))))
79
+ when :many_to_many, :one_through_one
80
+ mds = r.associated_class.dataset.
81
+ join(r[:join_table], r[:right_keys].zip(r.right_primary_keys)).
82
+ select(*Array(r.qualified_right_key)).
83
+ where(r.qualify(r.join_table_alias, r[:left_keys])=>sds.select(*r.qualify(model.table_name, r[:left_primary_key_columns])))
84
+ ds.filter(r.qualified_right_primary_key=>r.send(:apply_filter_by_associations_limit_strategy, mds))
85
+ when :many_through_many, :one_through_many
86
86
  fre = r.reverse_edges.first
87
87
  fe, *edges = r.edges
88
- sds = sds.select(*Array(r.qualify(fre[:table], fre[:left]))).
89
- join(fe[:table], Array(fe[:right]).zip(Array(fe[:left])), :implicit_qualifier=>model.table_name)
90
- edges.each{|e| sds = sds.join(e[:table], Array(e[:right]).zip(Array(e[:left])))}
91
- ds.filter(r.qualified_right_primary_key=>sds)
88
+ edges << r.final_edge
89
+ mds = model.
90
+ select(*Array(r.qualify(fre[:table], fre[:left]))).
91
+ join(fe[:table], Array(fe[:right]).zip(Array(fe[:left])), :implicit_qualifier=>model.table_name).
92
+ where(r.qualify(fe[:table], fe[:right])=>sds.select(*r.qualify(model.table_name, r[:left_primary_key_columns])))
93
+ edges.each{|e| mds = mds.join(e[:table], Array(e[:right]).zip(Array(e[:left])))}
94
+ ds.filter(r.qualified_right_primary_key=>r.send(:apply_filter_by_associations_limit_strategy, mds))
92
95
  when :pg_array_to_many
93
96
  ds.filter(Sequel.expr(r.primary_key=>sds.select{Sequel.pg_array_op(r.qualify(r[:model].table_name, r[:key])).unnest}))
94
97
  when :many_to_pg_array
@@ -1,6 +1,6 @@
1
1
  module Sequel
2
2
  module Plugins
3
- # The many_through_many plugin allow you to create an association to multiple objects using multiple join tables.
3
+ # The many_through_many plugin allow you to create an association using multiple join tables.
4
4
  # For example, assume the following associations:
5
5
  #
6
6
  # Artist.many_to_many :albums
@@ -65,11 +65,28 @@ module Sequel
65
65
  #
66
66
  # Artist.many_through_many :artists, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_artists, :album_id, :artist_id]],
67
67
  # :distinct=>true
68
+ #
69
+ # In addition to many_through_many, this plugin also adds one_through_many, for an association to a single object through multiple join tables.
70
+ # This is useful if there are unique constraints on the foreign keys in the join tables that reference back to the current table, or if you want
71
+ # to set an order on the association and just want the first record.
72
+ #
73
+ # Usage:
74
+ #
75
+ # # Make all model subclasses support many_through_many associations
76
+ # Sequel::Model.plugin :many_through_many
77
+ #
78
+ # # Make the Album class support many_through_many associations
79
+ # Album.plugin :many_through_many
68
80
  module ManyThroughMany
69
81
  # The AssociationReflection subclass for many_through_many associations.
70
82
  class ManyThroughManyAssociationReflection < Sequel::Model::Associations::ManyToManyAssociationReflection
71
83
  Sequel::Model::Associations::ASSOCIATION_TYPES[:many_through_many] = self
72
84
 
85
+ # many_through_many and one_through_many associations can be clones
86
+ def cloneable?(ref)
87
+ ref[:type] == :many_through_many || ref[:type] == :one_through_many
88
+ end
89
+
73
90
  # The default associated key alias(es) to use when eager loading
74
91
  # associations via eager.
75
92
  def default_associated_key_alias
@@ -142,9 +159,28 @@ module Sequel
142
159
  def filter_by_associations_add_conditions_dataset_filter(ds)
143
160
  reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias], :qualify=>:deep)}
144
161
  ft = final_reverse_edge
162
+ k = qualify(ft[:alias], Array(self[:left_key]))
145
163
  ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])), :table_alias=>ft[:alias], :qualify=>:deep).
146
- select(*qualify(ft[:alias], Array(self[:left_key])))
164
+ where(Sequel.negate(k.zip([]))).
165
+ select(*k)
147
166
  end
167
+
168
+ def filter_by_associations_limit_key
169
+ fe = edges.first
170
+ Array(qualify(fe[:table], fe[:right])) + Array(qualify(associated_class.table_name, associated_class.primary_key))
171
+ end
172
+
173
+ def filter_by_associations_limit_subquery
174
+ subquery = associated_eager_dataset.unlimited
175
+ reverse_edges.each{|t| subquery = subquery.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias], :qualify=>:deep)}
176
+ ft = final_reverse_edge
177
+ subquery.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])), :table_alias=>ft[:alias], :qualify=>:deep)
178
+ end
179
+ end
180
+
181
+ class OneThroughManyAssociationReflection < ManyThroughManyAssociationReflection
182
+ Sequel::Model::Associations::ASSOCIATION_TYPES[:one_through_many] = self
183
+ include Sequel::Model::Associations::SingularAssociationReflection
148
184
  end
149
185
 
150
186
  module ClassMethods
@@ -168,15 +204,21 @@ module Sequel
168
204
  associate(:many_through_many, name, opts.merge(through.is_a?(Hash) ? through : {:through=>through}), &block)
169
205
  end
170
206
 
207
+ # Creates a one_through_many association. See many_through_many for arguments.
208
+ def one_through_many(name, through, opts=OPTS, &block)
209
+ associate(:one_through_many, name, opts.merge(through.is_a?(Hash) ? through : {:through=>through}), &block)
210
+ end
211
+
171
212
  private
172
213
 
173
214
  # Create the association methods and :eager_loader and :eager_grapher procs.
174
215
  def def_many_through_many(opts)
216
+ one_through_many = opts[:type] == :one_through_many
175
217
  name = opts[:name]
176
218
  model = self
177
219
  opts[:read_only] = true
178
220
  opts[:after_load].unshift(:array_uniq!) if opts[:uniq]
179
- opts[:cartesian_product_number] ||= 2
221
+ opts[:cartesian_product_number] ||= one_through_many ? 0 : 2
180
222
  opts[:through] = opts[:through].map do |e|
181
223
  case e
182
224
  when Array
@@ -197,7 +239,7 @@ module Sequel
197
239
  opts[:eager_loader_key] = left_pk unless opts.has_key?(:eager_loader_key)
198
240
  left_pks = opts[:left_primary_keys] = Array(left_pk)
199
241
  lpkc = opts[:left_primary_key_column] ||= left_pk
200
- opts[:left_primary_key_columns] ||= Array(lpkc)
242
+ lpkcs = opts[:left_primary_key_columns] ||= Array(lpkc)
201
243
  opts[:dataset] ||= lambda do
202
244
  ds = opts.associated_dataset
203
245
  opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias], :qualify=>:deep)}
@@ -210,30 +252,36 @@ module Sequel
210
252
  opts[:eager_loader] ||= lambda do |eo|
211
253
  h = eo[:id_map]
212
254
  rows = eo[:rows]
213
- rows.each{|object| object.associations[name] = []}
214
255
  ds = opts.associated_class
215
256
  opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias], :qualify=>:deep)}
216
257
  ft = opts.final_reverse_edge
258
+
217
259
  ds = ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + [[opts.predicate_key, h.keys]], :table_alias=>ft[:alias], :qualify=>:deep)
218
260
  ds = model.eager_loading_dataset(opts, ds, nil, eo[:associations], eo)
219
- if opts.eager_limit_strategy == :window_function
220
- delete_rn = true
221
- rn = ds.row_number_column
222
- ds = apply_window_function_eager_limit_strategy(ds, opts)
223
- end
261
+ ds = opts.apply_eager_limit_strategy(ds)
262
+ opts.initialize_association_cache(rows)
263
+
264
+ assign_singular = opts.assign_singular?
265
+ delete_rn = opts.delete_row_number_column(ds)
224
266
  ds.all do |assoc_record|
225
- assoc_record.values.delete(rn) if delete_rn
267
+ assoc_record.values.delete(delete_rn) if delete_rn
226
268
  hash_key = if uses_lcks
227
269
  left_key_alias.map{|k| assoc_record.values.delete(k)}
228
270
  else
229
271
  assoc_record.values.delete(left_key_alias)
230
272
  end
231
273
  next unless objects = h[hash_key]
232
- objects.each{|object| object.associations[name].push(assoc_record)}
233
- end
234
- if opts.eager_limit_strategy == :ruby
235
- rows.each{|o| o.associations[name] = o.associations[name][slice_range] || []}
274
+ if assign_singular
275
+ objects.each do |object|
276
+ object.associations[name] ||= assoc_record
277
+ end
278
+ else
279
+ objects.each do |object|
280
+ object.associations[name].push(assoc_record)
281
+ end
282
+ end
236
283
  end
284
+ opts.apply_ruby_eager_limit_strategy(rows)
237
285
  end
238
286
 
239
287
  join_type = opts[:graph_join_type]
@@ -245,16 +293,34 @@ module Sequel
245
293
  opts[:eager_grapher] ||= proc do |eo|
246
294
  ds = eo[:self]
247
295
  iq = eo[:implicit_qualifier]
248
- opts.edges.each do |t|
249
- ds = ds.graph(t[:table], t.fetch(:only_conditions, (Array(t[:right]).zip(Array(t[:left])) + t[:conditions])), :select=>false, :table_alias=>ds.unused_table_alias(t[:table]), :join_type=>t[:join_type], :qualify=>:deep, :implicit_qualifier=>iq, &t[:block])
250
- iq = nil
296
+ egls = eo[:limit_strategy]
297
+ if egls && egls != :ruby
298
+ associated_key_array = opts.associated_key_array
299
+ orig_egds = egds = eager_graph_dataset(opts, eo)
300
+ opts.reverse_edges.each{|t| egds = egds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias], :qualify=>:deep)}
301
+ ft = opts.final_reverse_edge
302
+ egds = egds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])), :table_alias=>ft[:alias], :qualify=>:deep).
303
+ select_all(egds.first_source).
304
+ select_append(*associated_key_array)
305
+ egds = opts.apply_eager_graph_limit_strategy(egls, egds)
306
+ ds.graph(egds, associated_key_array.map{|v| v.alias}.zip(Array(lpkcs)) + conditions, :qualify=>:deep, :table_alias=>eo[:table_alias], :implicit_qualifier=>iq, :join_type=>eo[:join_type]||join_type, :from_self_alias=>eo[:from_self_alias], :select=>select||orig_egds.columns, &graph_block)
307
+ else
308
+ opts.edges.each do |t|
309
+ ds = ds.graph(t[:table], t.fetch(:only_conditions, (Array(t[:right]).zip(Array(t[:left])) + t[:conditions])), :select=>false, :table_alias=>ds.unused_table_alias(t[:table]), :join_type=>eo[:join_type]||t[:join_type], :qualify=>:deep, :implicit_qualifier=>iq, :from_self_alias=>eo[:from_self_alias], &t[:block])
310
+ iq = nil
311
+ end
312
+ fe = opts.final_edge
313
+ ds.graph(opts.associated_class, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>eo[:join_type]||join_type, &graph_block)
251
314
  end
252
- fe = opts.final_edge
253
- ds.graph(opts.associated_class, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>join_type, &graph_block)
254
315
  end
255
316
 
256
317
  def_association_dataset_methods(opts)
257
318
  end
319
+
320
+ # Use def_many_through_many, since they share pretty much the same code.
321
+ def def_one_through_many(opts)
322
+ def_many_through_many(opts)
323
+ end
258
324
  end
259
325
 
260
326
  module DatasetMethods
@@ -291,6 +357,7 @@ module Sequel
291
357
 
292
358
  association_filter_handle_inversion(op, expr, Array(lpks))
293
359
  end
360
+ alias one_through_many_association_filter_expression many_through_many_association_filter_expression
294
361
  end
295
362
  end
296
363
  end
@@ -286,7 +286,19 @@ module Sequel
286
286
  def validate_associated_object(reflection, obj)
287
287
  return if reflection[:validate] == false
288
288
  association = reflection[:name]
289
+ if (reflection[:type] == :one_to_many || reflection[:type] == :one_to_one) && (key = reflection[:key]).is_a?(Symbol) && !obj.values[key]
290
+ # There could be a presence validation on the foreign key in the associated model,
291
+ # which will fail if we validate before saving the current object. If there is
292
+ # no value for the foreign key, set it to the current primary key value, or a dummy
293
+ # value of 0 if we haven't saved the current object.
294
+ obj.values[key] = pk || 0
295
+ key = nil if pk
296
+ end
289
297
  obj.errors.full_messages.each{|m| errors.add(association, m)} unless obj.valid?
298
+ if key
299
+ # If we used a dummy value of 0, remove it so it doesn't accidently remain.
300
+ obj.values.delete(key)
301
+ end
290
302
  end
291
303
  end
292
304
  end
@@ -95,6 +95,18 @@ module Sequel
95
95
  :"#{underscore(demodulize(self[:model].name))}_ids"
96
96
  end
97
97
 
98
+ # Always use the ruby eager_graph limit strategy if association is limited.
99
+ def eager_graph_limit_strategy(_)
100
+ :ruby if self[:limit]
101
+ end
102
+
103
+ # Always use the ruby eager limit strategy
104
+ def eager_limit_strategy
105
+ cached_fetch(:_eager_limit_strategy) do
106
+ :ruby if self[:limit]
107
+ end
108
+ end
109
+
98
110
  # Handle silent failure of add/remove methods if raise_on_save_failure is false.
99
111
  def handle_silent_modification_failure?
100
112
  self[:raise_on_save_failure] == false
@@ -166,6 +178,18 @@ module Sequel
166
178
  :"#{singularize(self[:name])}_ids"
167
179
  end
168
180
 
181
+ # Always use the ruby eager_graph limit strategy if association is limited.
182
+ def eager_graph_limit_strategy(_)
183
+ :ruby if self[:limit]
184
+ end
185
+
186
+ # Always use the ruby eager limit strategy
187
+ def eager_limit_strategy
188
+ cached_fetch(:_eager_limit_strategy) do
189
+ :ruby if self[:limit]
190
+ end
191
+ end
192
+
169
193
  # Handle silent failure of add/remove methods if raise_on_save_failure is false
170
194
  # and save_after_modify is true.
171
195
  def handle_silent_modification_failure?
@@ -247,9 +271,7 @@ module Sequel
247
271
  opts[:eager_loader] ||= proc do |eo|
248
272
  id_map = eo[:id_map]
249
273
  rows = eo[:rows]
250
- rows.each do |object|
251
- object.associations[name] = []
252
- end
274
+ opts.initialize_association_cache(rows)
253
275
 
254
276
  klass = opts.associated_class
255
277
  ds = model.eager_loading_dataset(opts, klass.where(Sequel.pg_array_op(opts.predicate_key).overlaps(id_map.keys)), nil, eo[:associations], eo)
@@ -263,9 +285,7 @@ module Sequel
263
285
  end
264
286
  end
265
287
  end
266
- if slice_range
267
- rows.each{|o| o.associations[name] = o.associations[name][slice_range] || []}
268
- end
288
+ opts.apply_ruby_eager_limit_strategy(rows)
269
289
  end
270
290
 
271
291
  join_type = opts[:graph_join_type]
@@ -292,7 +312,7 @@ module Sequel
292
312
 
293
313
  opts[:eager_grapher] ||= proc do |eo|
294
314
  ds = eo[:self]
295
- ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
315
+ ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep, :from_self_alias=>eo[:from_self_alias]), &graph_block)
296
316
  ds
297
317
  end
298
318
 
@@ -348,8 +368,9 @@ module Sequel
348
368
  rows = eo[:rows]
349
369
  id_map = {}
350
370
  pkm = opts.primary_key_method
371
+ opts.initialize_association_cache(rows)
372
+
351
373
  rows.each do |object|
352
- object.associations[name] = []
353
374
  if associated_pks = object.send(key)
354
375
  associated_pks.each do |apk|
355
376
  (id_map[apk] ||= []) << object
@@ -366,9 +387,7 @@ module Sequel
366
387
  end
367
388
  end
368
389
  end
369
- if slice_range
370
- rows.each{|o| o.associations[name] = o.associations[name][slice_range] || []}
371
- end
390
+ opts.apply_ruby_eager_limit_strategy(rows)
372
391
  end
373
392
 
374
393
  join_type = opts[:graph_join_type]
@@ -395,7 +414,7 @@ module Sequel
395
414
 
396
415
  opts[:eager_grapher] ||= proc do |eo|
397
416
  ds = eo[:self]
398
- ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
417
+ ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep, :from_self_alias=>eo[:from_self_alias]), &graph_block)
399
418
  ds
400
419
  end
401
420
 
@@ -24,7 +24,15 @@ module Sequel
24
24
  #
25
25
  # # Use the default of storing the class name in the sti_key
26
26
  # # column (:kind in this case)
27
- # Employee.plugin :single_table_inheritance, :kind
27
+ # class Employee < Sequel::Model
28
+ # plugin :single_table_inheritance, :kind
29
+ # end
30
+ #
31
+ # # Have subclasses inherit from the appropriate class
32
+ # class Staff < Employee; end
33
+ # class Manager < Employee; end
34
+ #
35
+ # # You can also use many different options to configure the plugin:
28
36
  #
29
37
  # # Using integers to store the class type, with a :model_map hash
30
38
  # # and an sti_key of :type
@@ -935,6 +935,7 @@ module Sequel
935
935
  # The alias to use for the expression, not +alias+ since that is
936
936
  # a keyword in ruby.
937
937
  attr_reader :aliaz
938
+ alias_method :alias, :aliaz
938
939
 
939
940
  # Create an object with the given expression and alias.
940
941
  def initialize(expression, aliaz)
@@ -3,7 +3,7 @@ module Sequel
3
3
  MAJOR = 4
4
4
  # The minor version of Sequel. Bumped for every non-patch level
5
5
  # release, generally around once a month.
6
- MINOR = 7
6
+ MINOR = 8
7
7
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
8
8
  # releases that fix regressions from previous versions.
9
9
  TINY = 0
@@ -16,12 +16,12 @@ describe "A MSSQL database" do
16
16
  @db = DB
17
17
  end
18
18
 
19
- cspecify "should be able to read fractional part of timestamp", :odbc do
19
+ specify "should be able to read fractional part of timestamp" do
20
20
  rs = @db["select getutcdate() as full_date, cast(datepart(millisecond, getutcdate()) as int) as milliseconds"].first
21
21
  rs[:milliseconds].should == rs[:full_date].usec/1000
22
22
  end
23
23
 
24
- cspecify "should be able to write fractional part of timestamp", :odbc do
24
+ specify "should be able to write fractional part of timestamp" do
25
25
  t = Time.utc(2001, 12, 31, 23, 59, 59, 997000)
26
26
  (t.usec/1000).should == @db["select cast(datepart(millisecond, ?) as int) as milliseconds", t].get
27
27
  end
@@ -850,6 +850,13 @@ describe "A PostgreSQL database" do
850
850
 
851
851
  @db[:posts].full_text_search(:title, :$n).call(:select, :n=>'rails').should == [{:title=>'ruby rails', :body=>'yowsa'}]
852
852
  @db[:posts].full_text_search(:title, :$n).prepare(:select, :fts_select).call(:n=>'rails').should == [{:title=>'ruby rails', :body=>'yowsa'}]
853
+
854
+ @db[:posts].insert(:title=>'jruby rubinius ruby maglev mri iron')
855
+ @db[:posts].insert(:title=>'ruby jruby maglev mri rubinius iron')
856
+ @db[:posts].full_text_search(:title, 'rubinius ruby', :phrase=>true).select_order_map(:title).should == ['jruby rubinius ruby maglev mri iron']
857
+ @db[:posts].full_text_search(:title, 'jruby maglev', :phrase=>true).select_order_map(:title).should == ['ruby jruby maglev mri rubinius iron']
858
+ @db[:posts].full_text_search(:title, 'rubinius ruby', :plain=>true).select_order_map(:title).should == ['jruby rubinius ruby maglev mri iron', 'ruby jruby maglev mri rubinius iron']
859
+ @db[:posts].full_text_search(:title, 'jruby maglev', :plain=>true).select_order_map(:title).should == ['jruby rubinius ruby maglev mri iron', 'ruby jruby maglev mri rubinius iron']
853
860
  end
854
861
 
855
862
  specify "should support spatial indexes" do