sequel 4.7.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG +46 -0
- data/README.rdoc +25 -1
- data/doc/active_record.rdoc +1 -1
- data/doc/advanced_associations.rdoc +143 -17
- data/doc/association_basics.rdoc +80 -59
- data/doc/release_notes/4.8.0.txt +175 -0
- data/lib/sequel/adapters/odbc.rb +1 -1
- data/lib/sequel/adapters/odbc/mssql.rb +4 -2
- data/lib/sequel/adapters/shared/postgres.rb +19 -3
- data/lib/sequel/adapters/shared/sqlite.rb +3 -3
- data/lib/sequel/ast_transformer.rb +1 -1
- data/lib/sequel/dataset/actions.rb +1 -1
- data/lib/sequel/dataset/graph.rb +23 -9
- data/lib/sequel/dataset/misc.rb +2 -2
- data/lib/sequel/dataset/sql.rb +3 -3
- data/lib/sequel/extensions/columns_introspection.rb +1 -1
- data/lib/sequel/extensions/mssql_emulate_lateral_with_apply.rb +1 -1
- data/lib/sequel/extensions/pg_array.rb +1 -1
- data/lib/sequel/extensions/pg_array_ops.rb +6 -0
- data/lib/sequel/extensions/pg_hstore_ops.rb +7 -0
- data/lib/sequel/extensions/pg_json_ops.rb +5 -0
- data/lib/sequel/extensions/query.rb +8 -2
- data/lib/sequel/extensions/to_dot.rb +1 -1
- data/lib/sequel/model/associations.rb +476 -152
- data/lib/sequel/plugins/class_table_inheritance.rb +11 -3
- data/lib/sequel/plugins/dataset_associations.rb +21 -18
- data/lib/sequel/plugins/many_through_many.rb +87 -20
- data/lib/sequel/plugins/nested_attributes.rb +12 -0
- data/lib/sequel/plugins/pg_array_associations.rb +31 -12
- data/lib/sequel/plugins/single_table_inheritance.rb +9 -1
- data/lib/sequel/sql.rb +1 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +2 -2
- data/spec/adapters/postgres_spec.rb +7 -0
- data/spec/core/object_graph_spec.rb +250 -196
- data/spec/extensions/core_refinements_spec.rb +1 -1
- data/spec/extensions/dataset_associations_spec.rb +100 -6
- data/spec/extensions/many_through_many_spec.rb +1002 -19
- data/spec/extensions/nested_attributes_spec.rb +24 -0
- data/spec/extensions/pg_array_associations_spec.rb +17 -12
- data/spec/extensions/pg_array_spec.rb +4 -2
- data/spec/extensions/spec_helper.rb +1 -1
- data/spec/integration/associations_test.rb +1003 -48
- data/spec/integration/dataset_test.rb +12 -5
- data/spec/integration/prepared_statement_test.rb +1 -1
- data/spec/integration/type_test.rb +1 -1
- data/spec/model/associations_spec.rb +467 -130
- data/spec/model/eager_loading_spec.rb +332 -5
- metadata +5 -3
@@ -1,6 +1,12 @@
|
|
1
|
-
# The query extension adds
|
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
|
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
|
#
|
@@ -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
|
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){
|
202
|
+
if self[:limit] || !returns_array?
|
203
|
+
case s = cached_fetch(:eager_limit_strategy){default_eager_limit_strategy}
|
101
204
|
when true
|
102
|
-
|
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 =
|
490
|
+
ds = associated_eager_dataset.unordered.unlimited
|
343
491
|
ds = filter_by_associations_add_conditions_dataset_filter(ds)
|
344
|
-
ds =
|
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
|
-
|
552
|
-
|
553
|
-
|
554
|
-
#
|
555
|
-
#
|
556
|
-
|
557
|
-
|
558
|
-
|
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
|
-
#
|
578
|
-
|
579
|
-
|
580
|
-
|
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
|
-
#
|
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
|
-
|
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
|
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
|
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)
|
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 =
|
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
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
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(
|
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
|
-
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
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
|
-
|
1251
|
-
|
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
|
-
|
1315
|
-
|
1316
|
-
|
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=>
|
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
|
-
|
1375
|
-
ds = model.eager_loading_dataset(opts, klass.where(
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1380
|
-
|
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(
|
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
|
-
|
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=>
|
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
|
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
|
1970
|
-
# :reflections
|
1971
|
-
# :requirements
|
1972
|
-
|
1973
|
-
|
1974
|
-
|
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.
|
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
|
-
|
2024
|
-
|
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
|
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.
|
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
|
-
|
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
|
-
|
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
|