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
@@ -1,6 +1,12 @@
1
- # The query extension adds Sequel::Dataset#query which allows
1
+ # The query extension adds a query method which allows
2
2
  # a different way to construct queries instead of the usual
3
- # method chaining. See Sequel::Dataset#query for details.
3
+ # method chaining:
4
+ #
5
+ # dataset = DB[:items].query do
6
+ # select :x, :y, :z
7
+ # filter{(x > 1) & (y > 2)}
8
+ # reverse :z
9
+ # end
4
10
  #
5
11
  # You can load this extension into specific datasets:
6
12
  #
@@ -91,7 +91,7 @@ module Sequel
91
91
  when SQL::AliasedExpression
92
92
  dot "AliasedExpression"
93
93
  v(e.expression, :expression)
94
- v(e.aliaz, :alias)
94
+ v(e.alias, :alias)
95
95
  when SQL::CaseExpression
96
96
  dot "CaseExpression"
97
97
  v(e.expression, :expression) if e.expression
@@ -70,12 +70,84 @@ module Sequel
70
70
  end
71
71
  ds = ds.order(*self[:order]) if self[:order]
72
72
  ds = ds.limit(*self[:limit]) if self[:limit]
73
- ds = ds.limit(1) if !returns_array? && self[:key]
73
+ ds = ds.limit(1) if limit_to_single_row?
74
74
  ds = ds.eager(*self[:eager]) if self[:eager]
75
75
  ds = ds.distinct if self[:distinct]
76
76
  ds
77
77
  end
78
-
78
+
79
+ # Apply the eager graph limit strategy to the dataset to graph into the current dataset, or return
80
+ # the dataset unmodified if no SQL limit strategy is needed.
81
+ def apply_eager_graph_limit_strategy(strategy, ds)
82
+ case strategy
83
+ when :distinct_on
84
+ apply_distinct_on_eager_limit_strategy(ds.order_prepend(*self[:order]))
85
+ when :window_function
86
+ apply_window_function_eager_limit_strategy(ds.order_prepend(*self[:order])).select(*ds.columns)
87
+ else
88
+ ds
89
+ end
90
+ end
91
+
92
+ # Apply an eager limit strategy to the dataset, or return the dataset
93
+ # unmodified if it doesn't need an eager limit strategy.
94
+ def apply_eager_limit_strategy(ds)
95
+ case eager_limit_strategy
96
+ when :distinct_on
97
+ apply_distinct_on_eager_limit_strategy(ds)
98
+ when :window_function
99
+ apply_window_function_eager_limit_strategy(ds)
100
+ else
101
+ ds
102
+ end
103
+ end
104
+
105
+ # Use DISTINCT ON and ORDER BY clauses to limit the results to the first record with matching keys.
106
+ def apply_distinct_on_eager_limit_strategy(ds)
107
+ keys = predicate_key
108
+ ds.distinct(*keys).order_prepend(*keys)
109
+ end
110
+
111
+ # Use a window function to limit the results of the eager loading dataset.
112
+ def apply_window_function_eager_limit_strategy(ds)
113
+ rn = ds.row_number_column
114
+ limit, offset = limit_and_offset
115
+ ds = ds.unordered.select_append{|o| o.row_number{}.over(:partition=>predicate_key, :order=>ds.opts[:order]).as(rn)}.from_self
116
+ ds = if !returns_array?
117
+ ds.where(rn => offset ? offset+1 : 1)
118
+ elsif offset
119
+ offset += 1
120
+ if limit
121
+ ds.where(rn => (offset...(offset+limit)))
122
+ else
123
+ ds.where{SQL::Identifier.new(rn) >= offset}
124
+ end
125
+ else
126
+ ds.where{SQL::Identifier.new(rn) <= limit}
127
+ end
128
+ end
129
+
130
+ # If the ruby eager limit strategy is being used, slice the array using the slice
131
+ # range to return the object(s) at the correct offset/limit.
132
+ def apply_ruby_eager_limit_strategy(rows)
133
+ if eager_limit_strategy == :ruby
134
+ name = self[:name]
135
+ if returns_array?
136
+ range = slice_range
137
+ rows.each{|o| o.associations[name] = o.associations[name][range] || []}
138
+ elsif slice_range
139
+ offset = slice_range.begin
140
+ rows.each{|o| o.associations[name] = o.associations[name][offset]}
141
+ end
142
+ end
143
+ end
144
+
145
+ # Whether the associations cache should use an array when storing the
146
+ # associated records during eager loading.
147
+ def assign_singular?
148
+ !returns_array?
149
+ end
150
+
79
151
  # Whether this association can have associated objects, given the current
80
152
  # object. Should be false if obj cannot have associated objects because
81
153
  # the necessary key columns are NULL.
@@ -83,6 +155,12 @@ module Sequel
83
155
  true
84
156
  end
85
157
 
158
+ # Whether you are able to clone from the given association type to the current
159
+ # association type, true by default only if the types match.
160
+ def cloneable?(ref)
161
+ ref[:type] == self[:type]
162
+ end
163
+
86
164
  # Name symbol for the dataset association method
87
165
  def dataset_method
88
166
  :"#{self[:name]}_dataset"
@@ -93,23 +171,41 @@ module Sequel
93
171
  true
94
172
  end
95
173
 
174
+ # Return the symbol used for the row number column if the window function
175
+ # eager limit strategy is being used, or nil otherwise.
176
+ def delete_row_number_column(ds)
177
+ if eager_limit_strategy == :window_function
178
+ ds.row_number_column
179
+ end
180
+ end
181
+
182
+ # The eager_graph limit strategy to use for this dataset
183
+ def eager_graph_limit_strategy(strategy)
184
+ if self[:limit] || !returns_array?
185
+ strategy = strategy[self[:name]] if strategy.is_a?(Hash)
186
+ case strategy
187
+ when true
188
+ true_eager_limit_strategy
189
+ when Symbol
190
+ strategy
191
+ else
192
+ if returns_array? || offset
193
+ :ruby
194
+ end
195
+ end
196
+ end
197
+ end
198
+
96
199
  # The eager limit strategy to use for this dataset.
97
200
  def eager_limit_strategy
98
201
  cached_fetch(:_eager_limit_strategy) do
99
- if self[:limit]
100
- case s = cached_fetch(:eager_limit_strategy){self[:model].default_eager_limit_strategy || :ruby}
202
+ if self[:limit] || !returns_array?
203
+ case s = cached_fetch(:eager_limit_strategy){default_eager_limit_strategy}
101
204
  when true
102
- ds = associated_class.dataset
103
- if ds.supports_window_functions?
104
- :window_function
105
- else
106
- :ruby
107
- end
205
+ true_eager_limit_strategy
108
206
  else
109
207
  s
110
208
  end
111
- else
112
- nil
113
209
  end
114
210
  end
115
211
  end
@@ -140,7 +236,7 @@ module Sequel
140
236
  # Whether additional conditions should be added when using the filter
141
237
  # by associations support.
142
238
  def filter_by_associations_add_conditions?
143
- self[:conditions] || self[:eager_block]
239
+ self[:conditions] || self[:eager_block] || self[:limit]
144
240
  end
145
241
 
146
242
  # The expression to use for the additional conditions to be added for
@@ -158,6 +254,16 @@ module Sequel
158
254
  false
159
255
  end
160
256
 
257
+ # Initialize the associations cache for the current association for the given objects.
258
+ def initialize_association_cache(objects)
259
+ name = self[:name]
260
+ if assign_singular?
261
+ objects.each{|object| object.associations[name] = nil}
262
+ else
263
+ objects.each{|object| object.associations[name] = []}
264
+ end
265
+ end
266
+
161
267
  # The limit and offset for this association (returned as a two element array).
162
268
  def limit_and_offset
163
269
  if (v = self[:limit]).is_a?(Array)
@@ -319,6 +425,48 @@ module Sequel
319
425
  end
320
426
  end
321
427
 
428
+ # Apply a limit strategy to the given dataset so that filter by
429
+ # associations works with a limited dataset.
430
+ def apply_filter_by_associations_limit_strategy(ds)
431
+ case eager_limit_strategy
432
+ when :distinct_on
433
+ apply_filter_by_associations_distinct_on_limit_strategy(ds)
434
+ when :window_function
435
+ apply_filter_by_associations_window_function_limit_strategy(ds)
436
+ else
437
+ ds
438
+ end
439
+ end
440
+
441
+ # Apply a distinct on eager limit strategy using IN with a subquery
442
+ # that uses DISTINCT ON to ensure only the first matching record for
443
+ # each key is included.
444
+ def apply_filter_by_associations_distinct_on_limit_strategy(ds)
445
+ k = filter_by_associations_limit_key
446
+ ds.where(k=>apply_distinct_on_eager_limit_strategy(filter_by_associations_limit_subquery.select(*k)))
447
+ end
448
+
449
+ # Apply a distinct on eager limit strategy using IN with a subquery
450
+ # that uses a filter on the row_number window function to ensure
451
+ # that only rows inside the limit are returned.
452
+ def apply_filter_by_associations_window_function_limit_strategy(ds)
453
+ ds.where(filter_by_associations_limit_key=>apply_window_function_eager_limit_strategy(filter_by_associations_limit_subquery.select(*filter_by_associations_limit_alias_key)).select(*filter_by_associations_limit_aliases))
454
+ end
455
+
456
+ # The associated_dataset with the eager_block callback already applied.
457
+ def associated_eager_dataset
458
+ cached_fetch(:associated_eager_dataset) do
459
+ ds = associated_dataset
460
+ ds = self[:eager_block].call(ds) if self[:eager_block]
461
+ ds
462
+ end
463
+ end
464
+
465
+ # The default eager limit strategy to use for this association
466
+ def default_eager_limit_strategy
467
+ self[:model].default_eager_limit_strategy || :ruby
468
+ end
469
+
322
470
  # The conditions to add to the filter by associations conditions
323
471
  # subquery to restrict it to to the object(s) that was used as the
324
472
  # filter value.
@@ -339,13 +487,28 @@ module Sequel
339
487
  # values.
340
488
  def filter_by_associations_conditions_dataset
341
489
  cached_fetch(:filter_by_associations_conditions_dataset) do
342
- ds = associated_dataset.unordered.unlimited
490
+ ds = associated_eager_dataset.unordered.unlimited
343
491
  ds = filter_by_associations_add_conditions_dataset_filter(ds)
344
- ds = self[:eager_block].call(ds) if self[:eager_block]
492
+ ds = apply_filter_by_associations_limit_strategy(ds)
345
493
  ds
346
494
  end
347
495
  end
348
496
 
497
+ # The base subquery to use when filtering by limited associations
498
+ def filter_by_associations_limit_subquery
499
+ associated_eager_dataset.unlimited
500
+ end
501
+
502
+ # Whether to limit the associated dataset to a single row.
503
+ def limit_to_single_row?
504
+ !returns_array?
505
+ end
506
+
507
+ # Any offset to use for this association (or nil if there is no offset).
508
+ def offset
509
+ limit_and_offset.last
510
+ end
511
+
349
512
  # Whether the given association reflection is possible reciprocal
350
513
  # association for the current association reflection.
351
514
  def reciprocal_association?(assoc_reflect)
@@ -360,6 +523,16 @@ module Sequel
360
523
  def transform(s)
361
524
  s.is_a?(Array) ? s.map(&Proc.new) : (yield s)
362
525
  end
526
+
527
+ # The eager limit strategy used when true is given as the value, choosing the
528
+ # best strategy based on what the database supports.
529
+ def true_eager_limit_strategy
530
+ if associated_class.dataset.supports_window_functions?
531
+ :window_function
532
+ else
533
+ :ruby
534
+ end
535
+ end
363
536
  end
364
537
 
365
538
  class ManyToOneAssociationReflection < AssociationReflection
@@ -388,6 +561,11 @@ module Sequel
388
561
  self[:key].nil?
389
562
  end
390
563
 
564
+ # many_to_one associations don't need an eager_graph limit strategy
565
+ def eager_graph_limit_strategy(_)
566
+ nil
567
+ end
568
+
391
569
  # many_to_one associations don't need an eager limit strategy
392
570
  def eager_limit_strategy
393
571
  nil
@@ -454,6 +632,12 @@ module Sequel
454
632
  qualify(self[:model].table_name, self[:key_column])
455
633
  end
456
634
 
635
+ # many_to_one associations do not need to be limited to a single row if they
636
+ # explicitly do not have a key.
637
+ def limit_to_single_row?
638
+ super && self[:key]
639
+ end
640
+
457
641
  def reciprocal_association?(assoc_reflect)
458
642
  super && self[:keys] == assoc_reflect[:keys] && primary_key == assoc_reflect.primary_key
459
643
  end
@@ -479,6 +663,11 @@ module Sequel
479
663
  !self[:primary_keys].any?{|k| obj.send(k).nil?}
480
664
  end
481
665
 
666
+ # one_to_many and one_to_one associations can be clones
667
+ def cloneable?(ref)
668
+ ref[:type] == :one_to_many || ref[:type] == :one_to_one
669
+ end
670
+
482
671
  # Default foreign key name symbol for key in associated table that points to
483
672
  # current table's primary key.
484
673
  def default_key
@@ -538,6 +727,18 @@ module Sequel
538
727
  qualify(self[:model].table_name, self[:primary_key_column])
539
728
  end
540
729
 
730
+ def filter_by_associations_limit_alias_key
731
+ Array(filter_by_associations_limit_key)
732
+ end
733
+
734
+ def filter_by_associations_limit_aliases
735
+ filter_by_associations_limit_alias_key.map{|v| v.column}
736
+ end
737
+
738
+ def filter_by_associations_limit_key
739
+ qualify(associated_class.table_name, associated_class.primary_key)
740
+ end
741
+
541
742
  def reciprocal_association?(assoc_reflect)
542
743
  super && self[:keys] == assoc_reflect[:keys] && primary_key == assoc_reflect.primary_key
543
744
  end
@@ -547,46 +748,48 @@ module Sequel
547
748
  :many_to_one
548
749
  end
549
750
  end
550
-
551
- class OneToOneAssociationReflection < OneToManyAssociationReflection
552
- ASSOCIATION_TYPES[:one_to_one] = self
553
-
554
- # one_to_one associations don't use an eager limit strategy by default, but
555
- # support both DISTINCT ON and window functions as strategies.
556
- def eager_limit_strategy
557
- cached_fetch(:_eager_limit_strategy) do
558
- offset = limit_and_offset.last
559
- case s = self.fetch(:eager_limit_strategy){(self[:model].default_eager_limit_strategy || :ruby) if offset}
560
- when Symbol
561
- s
562
- when true
563
- ds = associated_class.dataset
564
- if ds.supports_ordered_distinct_on? && offset.nil?
565
- :distinct_on
566
- elsif ds.supports_window_functions?
567
- :window_function
568
- else
569
- :ruby
570
- end
571
- else
572
- nil
573
- end
574
- end
751
+
752
+ # Methods that turn an association that returns multiple objects into an association that
753
+ # returns a single object.
754
+ module SingularAssociationReflection
755
+ # Singular associations do not assign singular if they are using the ruby eager limit strategy
756
+ # and have a slice range, since they need to store the array of associated objects in order to
757
+ # pick the correct one with an offset.
758
+ def assign_singular?
759
+ super && (eager_limit_strategy != :ruby || !slice_range)
575
760
  end
576
761
 
577
- # The limit and offset for this association (returned as a two element array).
578
- def limit_and_offset
579
- if (v = self[:limit]).is_a?(Array)
580
- v
581
- else
582
- [v, nil]
583
- end
762
+ # Add conditions when filtering by singular associations with orders, since the
763
+ # underlying relationship is probably not one-to-one.
764
+ def filter_by_associations_add_conditions?
765
+ super || self[:order]
584
766
  end
585
767
 
586
- # one_to_one associations return a single object, not an array
768
+ # Singular associations always return a single object, not an array.
587
769
  def returns_array?
588
770
  false
589
771
  end
772
+
773
+ private
774
+
775
+ # Only use a eager limit strategy by default if there is an offset or an order.
776
+ def default_eager_limit_strategy
777
+ super if self[:order] || offset
778
+ end
779
+
780
+ # Use the DISTINCT ON eager limit strategy for true if the database supports it.
781
+ def true_eager_limit_strategy
782
+ if associated_class.dataset.supports_ordered_distinct_on? && !offset
783
+ :distinct_on
784
+ else
785
+ super
786
+ end
787
+ end
788
+ end
789
+
790
+ class OneToOneAssociationReflection < OneToManyAssociationReflection
791
+ ASSOCIATION_TYPES[:one_to_one] = self
792
+ include SingularAssociationReflection
590
793
  end
591
794
 
592
795
  class ManyToManyAssociationReflection < AssociationReflection
@@ -597,6 +800,17 @@ module Sequel
597
800
  self[:left_key_alias]
598
801
  end
599
802
 
803
+ # Array of associated keys used when eagerly loading.
804
+ def associated_key_array
805
+ cached_fetch(:associated_key_array) do
806
+ if self[:uses_left_composite_keys]
807
+ associated_key_alias.zip(predicate_keys).map{|a, k| SQL::AliasedExpression.new(k, a)}
808
+ else
809
+ [SQL::AliasedExpression.new(predicate_key, associated_key_alias)]
810
+ end
811
+ end
812
+ end
813
+
600
814
  # The column to use for the associated key when eagerly loading
601
815
  def associated_key_column
602
816
  self[:left_key]
@@ -613,6 +827,11 @@ module Sequel
613
827
  !self[:left_primary_keys].any?{|k| obj.send(k).nil?}
614
828
  end
615
829
 
830
+ # one_through_one and many_to_many associations can be clones
831
+ def cloneable?(ref)
832
+ ref[:type] == :many_to_many || ref[:type] == :one_through_one
833
+ end
834
+
616
835
  # The default associated key alias(es) to use when eager loading
617
836
  # associations via eager.
618
837
  def default_associated_key_alias
@@ -710,7 +929,9 @@ module Sequel
710
929
  private
711
930
 
712
931
  def filter_by_associations_add_conditions_dataset_filter(ds)
713
- ds.select(*qualify(join_table_alias, self[:left_keys])).
932
+ k = qualify(join_table_alias, self[:left_keys])
933
+ ds.select(*k).
934
+ where(Sequel.negate(k.zip([]))).
714
935
  inner_join(self[:join_table], Array(self[:right_keys]).zip(right_primary_keys), :qualify=>:deep)
715
936
  end
716
937
 
@@ -718,6 +939,23 @@ module Sequel
718
939
  qualify(self[:model].table_name, self[:left_primary_key_column])
719
940
  end
720
941
 
942
+ def filter_by_associations_limit_alias_key
943
+ aliaz = 'a'
944
+ filter_by_associations_limit_key.map{|c| c.as(Sequel.identifier(aliaz = aliaz.next))}
945
+ end
946
+
947
+ def filter_by_associations_limit_aliases
948
+ filter_by_associations_limit_alias_key.map{|v| v.alias}
949
+ end
950
+
951
+ def filter_by_associations_limit_key
952
+ qualify(join_table_alias, self[:left_keys]) + Array(qualify(associated_class.table_name, associated_class.primary_key))
953
+ end
954
+
955
+ def filter_by_associations_limit_subquery
956
+ super.inner_join(self[:join_table], Array(self[:right_keys]).zip(right_primary_keys))
957
+ end
958
+
721
959
  def reciprocal_association?(assoc_reflect)
722
960
  super && assoc_reflect[:left_keys] == self[:right_keys] &&
723
961
  assoc_reflect[:right_keys] == self[:left_keys] &&
@@ -736,6 +974,22 @@ module Sequel
736
974
  end
737
975
  end
738
976
 
977
+ class OneThroughOneAssociationReflection < ManyToManyAssociationReflection
978
+ ASSOCIATION_TYPES[:one_through_one] = self
979
+ include SingularAssociationReflection
980
+
981
+ # one_through_one associations should not singularize the association name when
982
+ # creating the foreign key.
983
+ def default_right_key
984
+ :"#{self[:name]}_id"
985
+ end
986
+
987
+ # one_through_one associations have no reciprocals
988
+ def reciprocal
989
+ nil
990
+ end
991
+ end
992
+
739
993
  # This module contains methods added to all association datasets
740
994
  module AssociationDatasetMethods
741
995
  # The model object that created the association dataset
@@ -839,6 +1093,9 @@ module Sequel
839
1093
  # model's primary key. Each current model object can be associated with
840
1094
  # more than one associated model objects. Each associated model object
841
1095
  # can be associated with only one current model object.
1096
+ # :one_through_one :: Similar to many_to_many in terms of foreign keys, but only one object
1097
+ # is associated to the current object through the association.
1098
+ # Provides only getter methods, no setter or modification methods.
842
1099
  # :one_to_one :: Similar to one_to_many in terms of foreign keys, but
843
1100
  # only one object is associated to the current object through the
844
1101
  # association. The methods created are similar to many_to_one, except
@@ -871,11 +1128,11 @@ module Sequel
871
1128
  # before an item is set using the association setter method.
872
1129
  # :cartesian_product_number :: the number of joins completed by this association that could cause more
873
1130
  # than one row for each row in the current table (default: 0 for
874
- # many_to_one and one_to_one associations, 1 for one_to_many and
875
- # many_to_many associations).
1131
+ # many_to_one, one_to_one, and one_through_one associations, 1
1132
+ # for one_to_many and many_to_many associations).
876
1133
  # :class :: The associated class or its name as a string or symbol. If not
877
1134
  # given, uses the association's name, which is camelized (and
878
- # singularized unless the type is :many_to_one or :one_to_one). If this is specified
1135
+ # singularized unless the type is :many_to_one, :one_to_one, or one_through_one). If this is specified
879
1136
  # as a string or symbol, you must specify the full class name (e.g. "SomeModule::MyModel").
880
1137
  # :clearer :: Proc used to define the private _remove_all_* method for doing the database work
881
1138
  # to remove all objects associated to the current object (*_to_many assocations).
@@ -895,10 +1152,9 @@ module Sequel
895
1152
  # :eager_graph :: The associations to eagerly load via +eager_graph+ when loading the associated object(s).
896
1153
  # many_to_many associations with this option cannot be eagerly loaded via +eager+.
897
1154
  # :eager_grapher :: A proc to use to implement eager loading via +eager_graph+, overriding the default.
898
- # Takes an options hash with the entries :self (the receiver of the eager_graph call),
899
- # :table_alias (the alias to use for table to graph into the association), :implicit_qualifier
900
- # (the alias that was used for the current table), and possibly :eager_block (a callback
901
- # proc accepting the associated dataset, for per-call customization).
1155
+ # Takes an options hash with at least the entries :self (the receiver of the eager_graph call),
1156
+ # :table_alias (the alias to use for table to graph into the association), and :implicit_qualifier
1157
+ # (the alias that was used for the current table).
902
1158
  # Should return a copy of the dataset with the association graphed into it.
903
1159
  # :eager_limit_strategy :: Determines the strategy used for enforcing limits and offsets when eager loading
904
1160
  # associations via the +eager+ method.
@@ -926,6 +1182,10 @@ module Sequel
926
1182
  # :graph_only_conditions :: The conditions to use on the SQL join when eagerly loading
927
1183
  # the association via +eager_graph+, instead of the default conditions specified by the
928
1184
  # foreign/primary keys. This option causes the :graph_conditions option to be ignored.
1185
+ # :graph_order :: Over the order to use when using eager_graph, instead of the default order. This should be used
1186
+ # in the case where :order contains an identifier qualified by the table's name, which may not match
1187
+ # the alias used when eager graphing. By setting this to the unqualified identifier, it will be
1188
+ # automatically qualified when using eager_graph.
929
1189
  # :graph_select :: A column or array of columns to select from the associated table
930
1190
  # when eagerly loading the association via +eager_graph+. Defaults to all
931
1191
  # columns in the associated table.
@@ -939,7 +1199,8 @@ module Sequel
939
1199
  # :order_eager_graph :: Whether to add the association's order to the graphed dataset's order when graphing
940
1200
  # via +eager_graph+. Defaults to true, so set to false to disable.
941
1201
  # :read_only :: Do not add a setter method (for many_to_one or one_to_one associations),
942
- # or add_/remove_/remove_all_ methods (for one_to_many and many_to_many associations).
1202
+ # or add_/remove_/remove_all_ methods (for one_to_many and many_to_many associations). Always
1203
+ # true for one_through_one associations.
943
1204
  # :reciprocal :: the symbol name of the reciprocal association,
944
1205
  # if it exists. By default, Sequel will try to determine it by looking at the
945
1206
  # associated model's assocations for a association that matches
@@ -988,7 +1249,7 @@ module Sequel
988
1249
  # conjunction with defining a model alias method for the primary key column.
989
1250
  # :raise_on_save_failure :: Do not raise exceptions for hook or validation failures when saving associated
990
1251
  # objects in the add/remove methods (return nil instead) [one_to_many only].
991
- # === :many_to_many
1252
+ # === :many_to_many and :one_through_one
992
1253
  # :graph_join_table_block :: The block to pass to +join_table+ for
993
1254
  # the join table when eagerly loading the association via +eager_graph+.
994
1255
  # :graph_join_table_conditions :: The additional conditions to use on the SQL join for
@@ -1034,14 +1295,20 @@ module Sequel
1034
1295
 
1035
1296
  # dup early so we don't modify opts
1036
1297
  orig_opts = opts.dup
1298
+
1037
1299
  if opts[:clone]
1038
1300
  cloned_assoc = association_reflection(opts[:clone])
1039
- raise(Error, "cannot clone an association to an association of different type (association #{name} with type #{type} cloning #{opts[:clone]} with type #{cloned_assoc[:type]})") unless cloned_assoc[:type] == type || [cloned_assoc[:type], type].all?{|t| [:one_to_many, :one_to_one].include?(t)}
1040
1301
  orig_opts = cloned_assoc[:orig_opts].merge(orig_opts)
1041
1302
  end
1303
+
1042
1304
  opts = orig_opts.merge(:type => type, :name => name, :cache=>{}, :model => self)
1043
1305
  opts[:block] = block if block
1044
1306
  opts = assoc_class.new.merge!(opts)
1307
+
1308
+ if opts[:clone] && !opts.cloneable?(cloned_assoc)
1309
+ raise(Error, "cannot clone an association to an association of different type (association #{name} with type #{type} cloning #{opts[:clone]} with type #{cloned_assoc[:type]})")
1310
+ end
1311
+
1045
1312
  opts[:eager_block] = opts[:block] unless opts.include?(:eager_block)
1046
1313
  if !opts.has_key?(:predicate_key) && opts.has_key?(:eager_loading_predicate_key)
1047
1314
  opts[:predicate_key] = opts[:eager_loading_predicate_key]
@@ -1091,11 +1358,7 @@ module Sequel
1091
1358
  ds = ds.eager(associations) unless Array(associations).empty?
1092
1359
  ds = eager_options[:eager_block].call(ds) if eager_options[:eager_block]
1093
1360
  if opts.eager_loading_use_associated_key?
1094
- ds = if opts[:uses_left_composite_keys]
1095
- ds.select_append(*opts.associated_key_alias.zip(opts.predicate_keys).map{|a, k| SQL::AliasedExpression.new(k, a)})
1096
- else
1097
- ds.select_append(SQL::AliasedExpression.new(opts.predicate_key, opts.associated_key_alias))
1098
- end
1361
+ ds = ds.select_append(*opts.associated_key_array)
1099
1362
  end
1100
1363
  ds
1101
1364
  end
@@ -1110,6 +1373,11 @@ module Sequel
1110
1373
  associate(:many_to_one, name, opts, &block)
1111
1374
  end
1112
1375
 
1376
+ # Shortcut for adding a one_through_one association, see #associate.
1377
+ def one_through_one(name, opts=OPTS, &block)
1378
+ associate(:one_through_one, name, opts, &block)
1379
+ end
1380
+
1113
1381
  # Shortcut for adding a one_to_many association, see #associate
1114
1382
  def one_to_many(name, opts=OPTS, &block)
1115
1383
  associate(:one_to_many, name, opts, &block)
@@ -1121,29 +1389,10 @@ module Sequel
1121
1389
  end
1122
1390
 
1123
1391
  Plugins.inherited_instance_variables(self, :@association_reflections=>:dup, :@autoreloading_associations=>:hash_dup, :@default_eager_limit_strategy=>nil)
1124
- Plugins.def_dataset_methods(self, [:eager, :eager_graph])
1392
+ Plugins.def_dataset_methods(self, [:eager, :eager_graph, :eager_graph_with_options, :association_join, :association_full_join, :association_inner_join, :association_left_join, :association_right_join])
1125
1393
 
1126
1394
  private
1127
1395
 
1128
- # Use a window function to limit the results of the eager loading dataset.
1129
- def apply_window_function_eager_limit_strategy(ds, opts)
1130
- rn = ds.row_number_column
1131
- limit, offset = opts.limit_and_offset
1132
- ds = ds.unordered.select_append{row_number{}.over(:partition=>opts.predicate_key, :order=>ds.opts[:order]).as(rn)}.from_self
1133
- ds = if opts[:type] == :one_to_one
1134
- ds.where(rn => offset ? offset+1 : 1)
1135
- elsif offset
1136
- offset += 1
1137
- if limit
1138
- ds.where(rn => (offset...(offset+limit)))
1139
- else
1140
- ds.where{SQL::Identifier.new(rn) >= offset}
1141
- end
1142
- else
1143
- ds.where{SQL::Identifier.new(rn) <= limit}
1144
- end
1145
- end
1146
-
1147
1396
  # The module to use for the association's methods. Defaults to
1148
1397
  # the overridable_methods_module.
1149
1398
  def association_module(opts=OPTS)
@@ -1179,8 +1428,10 @@ module Sequel
1179
1428
  association_module_def(opts.association_method, opts){|*dynamic_opts, &block| load_associated_objects(opts, dynamic_opts[0], &block)}
1180
1429
  end
1181
1430
 
1182
- # Configures many_to_many association reflection and adds the related association methods
1431
+ # Configures many_to_many and one_through_one association reflection and adds the related association methods
1183
1432
  def def_many_to_many(opts)
1433
+ one_through_one = opts[:type] == :one_through_one
1434
+ opts[:read_only] = true if one_through_one
1184
1435
  name = opts[:name]
1185
1436
  model = self
1186
1437
  left = (opts[:left_key] ||= opts.default_left_key)
@@ -1199,7 +1450,7 @@ module Sequel
1199
1450
  end
1200
1451
  uses_lcks = opts[:uses_left_composite_keys] = lcks.length > 1
1201
1452
  opts[:uses_right_composite_keys] = rcks.length > 1
1202
- opts[:cartesian_product_number] ||= 1
1453
+ opts[:cartesian_product_number] ||= one_through_one ? 0 : 1
1203
1454
  join_table = (opts[:join_table] ||= opts.default_join_table)
1204
1455
  left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
1205
1456
  graph_jt_conds = opts[:graph_join_table_conditions] = opts.fetch(:graph_join_table_conditions, []).to_a
@@ -1211,28 +1462,34 @@ module Sequel
1211
1462
  opts[:eager_loader] ||= proc do |eo|
1212
1463
  h = eo[:id_map]
1213
1464
  rows = eo[:rows]
1214
- rows.each{|object| object.associations[name] = []}
1215
1465
  r = rcks.zip(opts.right_primary_keys)
1216
1466
  l = [[opts.predicate_key, h.keys]]
1467
+
1217
1468
  ds = model.eager_loading_dataset(opts, opts.associated_class.inner_join(join_table, r + l, :qualify=>:deep), nil, eo[:associations], eo)
1218
- if opts.eager_limit_strategy == :window_function
1219
- delete_rn = true
1220
- rn = ds.row_number_column
1221
- ds = apply_window_function_eager_limit_strategy(ds, opts)
1222
- end
1469
+ ds = opts.apply_eager_limit_strategy(ds)
1470
+ opts.initialize_association_cache(rows)
1471
+
1472
+ assign_singular = opts.assign_singular?
1473
+ delete_rn = opts.delete_row_number_column(ds)
1223
1474
  ds.all do |assoc_record|
1224
- assoc_record.values.delete(rn) if delete_rn
1475
+ assoc_record.values.delete(delete_rn) if delete_rn
1225
1476
  hash_key = if uses_lcks
1226
1477
  left_key_alias.map{|k| assoc_record.values.delete(k)}
1227
1478
  else
1228
1479
  assoc_record.values.delete(left_key_alias)
1229
1480
  end
1230
1481
  next unless objects = h[hash_key]
1231
- objects.each{|object| object.associations[name].push(assoc_record)}
1232
- end
1233
- if opts.eager_limit_strategy == :ruby
1234
- rows.each{|o| o.associations[name] = o.associations[name][slice_range] || []}
1482
+ if assign_singular
1483
+ objects.each do |object|
1484
+ object.associations[name] ||= assoc_record
1485
+ end
1486
+ else
1487
+ objects.each do |object|
1488
+ object.associations[name].push(assoc_record)
1489
+ end
1490
+ end
1235
1491
  end
1492
+ opts.apply_ruby_eager_limit_strategy(rows)
1236
1493
  end
1237
1494
 
1238
1495
  join_type = opts[:graph_join_type]
@@ -1247,13 +1504,25 @@ module Sequel
1247
1504
  jt_graph_block = opts[:graph_join_table_block]
1248
1505
  opts[:eager_grapher] ||= proc do |eo|
1249
1506
  ds = eo[:self]
1250
- ds = ds.graph(join_table, use_jt_only_conditions ? jt_only_conditions : lcks.zip(lpkcs) + graph_jt_conds, :select=>false, :table_alias=>ds.unused_table_alias(join_table, [eo[:table_alias]]), :join_type=>jt_join_type, :implicit_qualifier=>eo[:implicit_qualifier], :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master], &jt_graph_block)
1251
- ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.right_primary_keys.zip(rcks) + conditions, :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>join_type, &graph_block)
1507
+ egls = eo[:limit_strategy]
1508
+ if egls && egls != :ruby
1509
+ associated_key_array = opts.associated_key_array
1510
+ orig_egds = egds = eager_graph_dataset(opts, eo)
1511
+ egds = egds.
1512
+ inner_join(join_table, rcks.zip(opts.right_primary_keys) + graph_jt_conds, :qualify=>:deep).
1513
+ select_all(egds.first_source).
1514
+ select_append(*associated_key_array)
1515
+ egds = opts.apply_eager_graph_limit_strategy(egls, egds)
1516
+ ds.graph(egds, associated_key_array.map{|v| v.alias}.zip(lpkcs) + conditions, :qualify=>:deep, :table_alias=>eo[:table_alias], :implicit_qualifier=>eo[:implicit_qualifier], :join_type=>eo[:join_type]||join_type, :from_self_alias=>eo[:from_self_alias], :select=>select||orig_egds.columns, &graph_block)
1517
+ else
1518
+ ds = ds.graph(join_table, use_jt_only_conditions ? jt_only_conditions : lcks.zip(lpkcs) + graph_jt_conds, :select=>false, :table_alias=>ds.unused_table_alias(join_table, [eo[:table_alias]]), :join_type=>eo[:join_type]||jt_join_type, :implicit_qualifier=>eo[:implicit_qualifier], :qualify=>:deep, :from_self_alias=>eo[:from_self_alias], &jt_graph_block)
1519
+ ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.right_primary_keys.zip(rcks) + conditions, :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>eo[:join_type]||join_type, &graph_block)
1520
+ end
1252
1521
  end
1253
1522
 
1254
1523
  def_association_dataset_methods(opts)
1255
1524
 
1256
- return if opts[:read_only]
1525
+ return if opts[:read_only] || one_through_one
1257
1526
 
1258
1527
  adder = opts[:adder] || proc do |o|
1259
1528
  h = {}
@@ -1311,9 +1580,9 @@ module Sequel
1311
1580
  opts[:eager_loader] ||= proc do |eo|
1312
1581
  h = eo[:id_map]
1313
1582
  keys = h.keys
1314
- # Default the cached association to nil, so any object that doesn't have it
1315
- # populated will have cached the negative lookup.
1316
- eo[:rows].each{|object| object.associations[name] = nil}
1583
+
1584
+ opts.initialize_association_cache(eo[:rows])
1585
+
1317
1586
  # Skip eager loading if no objects have a foreign key for this association
1318
1587
  unless keys.empty?
1319
1588
  klass = opts.associated_class
@@ -1334,7 +1603,7 @@ module Sequel
1334
1603
  graph_cks = opts[:graph_keys]
1335
1604
  opts[:eager_grapher] ||= proc do |eo|
1336
1605
  ds = eo[:self]
1337
- ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.primary_keys.zip(graph_cks) + conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
1606
+ ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.primary_keys.zip(graph_cks) + conditions, eo.merge(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep, :from_self_alias=>eo[:from_self_alias]), &graph_block)
1338
1607
  end
1339
1608
 
1340
1609
  def_association_dataset_methods(opts)
@@ -1371,26 +1640,15 @@ module Sequel
1371
1640
  rows = eo[:rows]
1372
1641
  reciprocal = opts.reciprocal
1373
1642
  klass = opts.associated_class
1374
- filter_keys = opts.predicate_key
1375
- ds = model.eager_loading_dataset(opts, klass.where(filter_keys=>h.keys), nil, eo[:associations], eo)
1376
- assign_singular = true if one_to_one
1377
- case opts.eager_limit_strategy
1378
- when :distinct_on
1379
- ds = ds.distinct(*filter_keys).order_prepend(*filter_keys)
1380
- when :window_function
1381
- delete_rn = true
1382
- rn = ds.row_number_column
1383
- ds = apply_window_function_eager_limit_strategy(ds, opts)
1384
- when :ruby
1385
- assign_singular = false if one_to_one && slice_range
1386
- end
1387
- if assign_singular
1388
- rows.each{|object| object.associations[name] = nil}
1389
- else
1390
- rows.each{|object| object.associations[name] = []}
1391
- end
1643
+
1644
+ ds = model.eager_loading_dataset(opts, klass.where(opts.predicate_key=>h.keys), nil, eo[:associations], eo)
1645
+ ds = opts.apply_eager_limit_strategy(ds)
1646
+ opts.initialize_association_cache(rows)
1647
+
1648
+ assign_singular = opts.assign_singular?
1649
+ delete_rn = opts.delete_row_number_column(ds)
1392
1650
  ds.all do |assoc_record|
1393
- assoc_record.values.delete(rn) if delete_rn
1651
+ assoc_record.values.delete(delete_rn) if delete_rn
1394
1652
  hash_key = uses_cks ? km.map{|k| assoc_record.send(k)} : assoc_record.send(km)
1395
1653
  next unless objects = h[hash_key]
1396
1654
  if assign_singular
@@ -1407,15 +1665,7 @@ module Sequel
1407
1665
  end
1408
1666
  end
1409
1667
  end
1410
- if opts.eager_limit_strategy == :ruby
1411
- if one_to_one
1412
- if slice_range
1413
- rows.each{|o| o.associations[name] = o.associations[name][slice_range.begin]}
1414
- end
1415
- else
1416
- rows.each{|o| o.associations[name] = o.associations[name][slice_range] || []}
1417
- end
1418
- end
1668
+ opts.apply_ruby_eager_limit_strategy(rows)
1419
1669
  end
1420
1670
 
1421
1671
  join_type = opts[:graph_join_type]
@@ -1427,7 +1677,7 @@ module Sequel
1427
1677
  graph_block = opts[:graph_block]
1428
1678
  opts[:eager_grapher] ||= proc do |eo|
1429
1679
  ds = eo[:self]
1430
- ds = ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : cks.zip(pkcs) + conditions, eo.merge(:select=>select, :join_type=>join_type, :qualify=>:deep, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
1680
+ ds = ds.graph(opts.apply_eager_graph_limit_strategy(eo[:limit_strategy], eager_graph_dataset(opts, eo)), use_only_conditions ? only_conditions : cks.zip(pkcs) + conditions, eo.merge(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep, :from_self_alias=>eo[:from_self_alias]), &graph_block)
1431
1681
  # We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
1432
1682
  ds.opts[:eager_graph][:reciprocals][eo[:table_alias]] = opts.reciprocal
1433
1683
  ds
@@ -1481,6 +1731,11 @@ module Sequel
1481
1731
  end
1482
1732
  end
1483
1733
 
1734
+ # Alias of def_many_to_many, since they share pretty much the same code.
1735
+ def def_one_through_one(opts)
1736
+ def_many_to_many(opts)
1737
+ end
1738
+
1484
1739
  # Alias of def_one_to_many, since they share pretty much the same code.
1485
1740
  def def_one_to_one(opts)
1486
1741
  def_one_to_many(opts)
@@ -1844,6 +2099,26 @@ module Sequel
1844
2099
  # Artist.eager(:albums => {proc{|ds| ds.where{year > 1990}}=>{:tracks => :genre}})
1845
2100
  module DatasetMethods
1846
2101
  Sequel::Dataset.def_mutation_method(:eager, :eager_graph, :module=>self)
2102
+
2103
+ %w'inner left right full'.each do |type|
2104
+ class_eval <<END, __FILE__, __LINE__+1
2105
+ def association_#{type}_join(*associations)
2106
+ _association_join(:#{type}, associations)
2107
+ end
2108
+ END
2109
+ end
2110
+
2111
+ # Adds one or more INNER JOINs to the existing dataset using the keys and conditions
2112
+ # specified by the given association. The following methods also exist for specifying
2113
+ # a different type of JOIN:
2114
+ #
2115
+ # association_full_join :: FULL JOIN
2116
+ # association_inner_join :: INNER JOIN
2117
+ # association_left_join :: LEFT JOIN
2118
+ # association_right_join :: RIGHT JOIN
2119
+ def association_join(*associations)
2120
+ association_inner_join(*associations)
2121
+ end
1847
2122
 
1848
2123
  # If the expression is in the form <tt>x = y</tt> where +y+ is a <tt>Sequel::Model</tt>
1849
2124
  # instance, array of <tt>Sequel::Model</tt> instances, or a <tt>Sequel::Model</tt> dataset,
@@ -1951,7 +2226,7 @@ module Sequel
1951
2226
  #
1952
2227
  # Each association's order, if definied, is respected. +eager_graph+ probably
1953
2228
  # won't work correctly on a limited dataset, unless you are
1954
- # only graphing many_to_one and one_to_one associations.
2229
+ # only graphing many_to_one, one_to_one, and one_through_one associations.
1955
2230
  #
1956
2231
  # Does not use the block defined for the association, since it does a single query for
1957
2232
  # all objects. You can use the :graph_* association options to modify the SQL query.
@@ -1959,22 +2234,48 @@ module Sequel
1959
2234
  # Like +eager+, you need to call +all+ on the dataset for the eager loading to work. If you just
1960
2235
  # call +each+, it will yield plain hashes, each containing all columns from all the tables.
1961
2236
  def eager_graph(*associations)
2237
+ eager_graph_with_options(associations)
2238
+ end
2239
+
2240
+ # Run eager_graph with some options specific to just this call. Unlike eager_graph, this takes
2241
+ # the associations as a single argument instead of multiple arguments.
2242
+ #
2243
+ # Options:
2244
+ #
2245
+ # :join_type :: Override the join type specified in the association
2246
+ # :limit_strategy :: Use a strategy for handling limits on associations.
2247
+ # Appropriate :limit_strategy values are:
2248
+ # true :: Pick the most appropriate based on what the database supports
2249
+ # :distinct_on :: Force use of DISTINCT ON stategy (*_one associations only)
2250
+ # :window_function :: Force use of window function strategy
2251
+ # :ruby :: Don't modify the SQL, implement limits/offsets with array slicing
2252
+ #
2253
+ # This can also be a hash with association name symbol keys and one of the above values,
2254
+ # to use different strategies per association.
2255
+ #
2256
+ # The default is the :ruby strategy. Choosing a different strategy can make your code
2257
+ # significantly slower in some cases (perhaps even the majority of cases), so you should
2258
+ # only use this if you have benchmarked that it is faster for your use cases.
2259
+ def eager_graph_with_options(associations, opts=OPTS)
2260
+ associations = [associations] unless associations.is_a?(Array)
1962
2261
  if eg = @opts[:eager_graph]
1963
2262
  eg = eg.dup
1964
- [:requirements, :reflections, :reciprocals].each{|k| eg[k] = eg[k].dup}
2263
+ [:requirements, :reflections, :reciprocals, :limits].each{|k| eg[k] = eg[k].dup}
2264
+ eg[:local] = opts
1965
2265
  ds = clone(:eager_graph=>eg)
1966
2266
  ds.eager_graph_associations(ds, model, ds.opts[:eager_graph][:master], [], *associations)
1967
2267
  else
1968
2268
  # Each of the following have a symbol key for the table alias, with the following values:
1969
- # :reciprocals - the reciprocal instance variable to use for this association
1970
- # :reflections - AssociationReflection instance related to this association
1971
- # :requirements - array of requirements for this association
1972
- ds = clone(:eager_graph=>{:requirements=>{}, :master=>alias_symbol(first_source), :reflections=>{}, :reciprocals=>{}, :cartesian_product_number=>0, :row_proc=>row_proc})
1973
- ds.eager_graph_associations(ds, model, ds.opts[:eager_graph][:master], [], *associations).
1974
- naked
2269
+ # :reciprocals :: the reciprocal value to use for this association
2270
+ # :reflections :: AssociationReflection instance related to this association
2271
+ # :requirements :: array of requirements for this association
2272
+ # :limits :: Any limit/offset array slicing that need to be handled in ruby land after loading
2273
+ opts = {:requirements=>{}, :master=>alias_symbol(first_source), :reflections=>{}, :reciprocals=>{}, :limits=>{}, :local=>opts, :cartesian_product_number=>0, :row_proc=>row_proc}
2274
+ ds = clone(:eager_graph=>opts)
2275
+ ds.eager_graph_associations(ds, model, ds.opts[:eager_graph][:master], [], *associations).naked
1975
2276
  end
1976
2277
  end
1977
-
2278
+
1978
2279
  # Do not attempt to split the result set into associations,
1979
2280
  # just return results as simple objects. This is useful if you
1980
2281
  # want to use eager_graph as a shortcut to have all of the joins
@@ -2004,7 +2305,7 @@ module Sequel
2004
2305
  # *associations :: any associations dependent on this one
2005
2306
  def eager_graph_association(ds, model, ta, requirements, r, *associations)
2006
2307
  if r.is_a?(SQL::AliasedExpression)
2007
- alias_base = r.aliaz
2308
+ alias_base = r.alias
2008
2309
  r = r.expression
2009
2310
  else
2010
2311
  alias_base = r[:graph_alias_base]
@@ -2020,11 +2321,19 @@ module Sequel
2020
2321
  associations = assoc.is_a?(Array) ? assoc : [assoc]
2021
2322
  end
2022
2323
  end
2023
- ds = loader.call(:self=>ds, :table_alias=>assoc_table_alias, :implicit_qualifier=>ta, :callback=>callback)
2024
- ds = ds.order_more(*qualified_expression(r[:order], assoc_table_alias)) if r[:order] and r[:order_eager_graph]
2324
+ orig_ds = ds
2325
+ local_opts = ds.opts[:eager_graph][:local]
2326
+ limit_strategy = r.eager_graph_limit_strategy(local_opts[:limit_strategy])
2327
+ ds = loader.call(:self=>ds, :table_alias=>assoc_table_alias, :implicit_qualifier=>ta, :callback=>callback, :join_type=>local_opts[:join_type], :limit_strategy=>limit_strategy, :from_self_alias=>ds.opts[:eager_graph][:master])
2328
+ if r[:order_eager_graph] && (order = r.fetch(:graph_order, r[:order]))
2329
+ ds = ds.order_more(*qualified_expression(order, assoc_table_alias))
2330
+ end
2025
2331
  eager_graph = ds.opts[:eager_graph]
2026
2332
  eager_graph[:requirements][assoc_table_alias] = requirements.dup
2027
2333
  eager_graph[:reflections][assoc_table_alias] = r
2334
+ if limit_strategy == :ruby
2335
+ eager_graph[:limits][assoc_table_alias] = r.limit_and_offset
2336
+ end
2028
2337
  eager_graph[:cartesian_product_number] += r[:cartesian_product_number] || 2
2029
2338
  ds = ds.eager_graph_associations(ds, r.associated_class, assoc_table_alias, requirements + [assoc_table_alias], *associations) unless associations.empty?
2030
2339
  ds
@@ -2065,12 +2374,18 @@ module Sequel
2065
2374
 
2066
2375
  private
2067
2376
 
2377
+ # Return a new dataset with JOINs of the given type added, using the tables and
2378
+ # conditions specified by the associations.
2379
+ def _association_join(type, associations)
2380
+ clone(:join=>clone(:graph_from_self=>false).eager_graph_with_options(associations, :join_type=>type).opts[:join])
2381
+ end
2382
+
2068
2383
  # If the association has conditions itself, then it requires additional filters be
2069
2384
  # added to the current dataset to ensure that the passed in object would also be
2070
2385
  # included by the association's conditions.
2071
2386
  def add_association_filter_conditions(ref, obj, expr)
2072
2387
  if expr != SQL::Constants::FALSE && ref.filter_by_associations_add_conditions?
2073
- Sequel.&(expr, ref.filter_by_associations_conditions_expression(obj))
2388
+ Sequel.expr(ref.filter_by_associations_conditions_expression(obj))
2074
2389
  else
2075
2390
  expr
2076
2391
  end
@@ -2130,7 +2445,7 @@ module Sequel
2130
2445
  # per-call determining of the alias base.
2131
2446
  def eager_graph_check_association(model, association)
2132
2447
  if association.is_a?(SQL::AliasedExpression)
2133
- SQL::AliasedExpression.new(check_association(model, association.expression), association.aliaz)
2448
+ SQL::AliasedExpression.new(check_association(model, association.expression), association.alias)
2134
2449
  else
2135
2450
  check_association(model, association)
2136
2451
  end
@@ -2210,6 +2525,7 @@ module Sequel
2210
2525
 
2211
2526
  association_filter_handle_inversion(op, expr, Array(lpks))
2212
2527
  end
2528
+ alias one_through_one_association_filter_expression many_to_many_association_filter_expression
2213
2529
 
2214
2530
  # Return a simple equality expression for filering by a many_to_one association
2215
2531
  def many_to_one_association_filter_expression(op, ref, obj)
@@ -2304,17 +2620,20 @@ module Sequel
2304
2620
  requirements = eager_graph[:requirements]
2305
2621
  reflection_map = @reflection_map = eager_graph[:reflections]
2306
2622
  reciprocal_map = @reciprocal_map = eager_graph[:reciprocals]
2623
+ limit_map = @limit_map = eager_graph[:limits]
2307
2624
  @unique = eager_graph[:cartesian_product_number] > 1
2308
2625
 
2309
2626
  alias_map = @alias_map = {}
2310
2627
  type_map = @type_map = {}
2311
2628
  after_load_map = @after_load_map = {}
2312
- limit_map = @limit_map = {}
2313
2629
  reflection_map.each do |k, v|
2314
2630
  alias_map[k] = v[:name]
2315
- type_map[k] = v.returns_array?
2316
2631
  after_load_map[k] = v[:after_load] unless v[:after_load].empty?
2317
- limit_map[k] = v.limit_and_offset if v[:limit]
2632
+ type_map[k] = if v.returns_array?
2633
+ true
2634
+ elsif (limit_and_offset = limit_map[k]) && !limit_and_offset.last.nil?
2635
+ :offset
2636
+ end
2318
2637
  end
2319
2638
 
2320
2639
  # Make dependency map hash out of requirements array for each association.
@@ -2523,9 +2842,14 @@ module Sequel
2523
2842
  if lo = limit_map[ta]
2524
2843
  limit, offset = lo
2525
2844
  offset ||= 0
2526
- list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)])
2845
+ if type_map[ta] == :offset
2846
+ [record.associations[assoc_name] = list[offset]]
2847
+ else
2848
+ list.replace(list[(offset)..(limit ? (offset)+limit-1 : -1)] || [])
2849
+ end
2850
+ else
2851
+ list
2527
2852
  end
2528
- list
2529
2853
  elsif list
2530
2854
  [list]
2531
2855
  else