sequel 3.27.0 → 3.28.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/CHANGELOG +96 -0
  2. data/README.rdoc +2 -2
  3. data/Rakefile +1 -1
  4. data/doc/association_basics.rdoc +48 -0
  5. data/doc/opening_databases.rdoc +29 -5
  6. data/doc/prepared_statements.rdoc +1 -0
  7. data/doc/release_notes/3.28.0.txt +304 -0
  8. data/doc/testing.rdoc +42 -0
  9. data/doc/transactions.rdoc +97 -0
  10. data/lib/sequel/adapters/db2.rb +95 -65
  11. data/lib/sequel/adapters/firebird.rb +25 -219
  12. data/lib/sequel/adapters/ibmdb.rb +440 -0
  13. data/lib/sequel/adapters/jdbc.rb +12 -0
  14. data/lib/sequel/adapters/jdbc/as400.rb +0 -7
  15. data/lib/sequel/adapters/jdbc/db2.rb +49 -0
  16. data/lib/sequel/adapters/jdbc/firebird.rb +34 -0
  17. data/lib/sequel/adapters/jdbc/oracle.rb +2 -27
  18. data/lib/sequel/adapters/jdbc/transactions.rb +34 -0
  19. data/lib/sequel/adapters/mysql.rb +10 -15
  20. data/lib/sequel/adapters/odbc.rb +1 -2
  21. data/lib/sequel/adapters/odbc/db2.rb +5 -5
  22. data/lib/sequel/adapters/postgres.rb +71 -11
  23. data/lib/sequel/adapters/shared/db2.rb +290 -0
  24. data/lib/sequel/adapters/shared/firebird.rb +214 -0
  25. data/lib/sequel/adapters/shared/mssql.rb +18 -75
  26. data/lib/sequel/adapters/shared/mysql.rb +13 -0
  27. data/lib/sequel/adapters/shared/postgres.rb +52 -36
  28. data/lib/sequel/adapters/shared/sqlite.rb +32 -36
  29. data/lib/sequel/adapters/sqlite.rb +4 -8
  30. data/lib/sequel/adapters/tinytds.rb +7 -3
  31. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +55 -0
  32. data/lib/sequel/core.rb +1 -1
  33. data/lib/sequel/database/connecting.rb +1 -1
  34. data/lib/sequel/database/misc.rb +6 -5
  35. data/lib/sequel/database/query.rb +1 -1
  36. data/lib/sequel/database/schema_generator.rb +2 -1
  37. data/lib/sequel/dataset/actions.rb +149 -33
  38. data/lib/sequel/dataset/features.rb +44 -7
  39. data/lib/sequel/dataset/misc.rb +9 -1
  40. data/lib/sequel/dataset/prepared_statements.rb +2 -2
  41. data/lib/sequel/dataset/query.rb +63 -10
  42. data/lib/sequel/dataset/sql.rb +22 -5
  43. data/lib/sequel/model.rb +3 -3
  44. data/lib/sequel/model/associations.rb +250 -27
  45. data/lib/sequel/model/base.rb +10 -16
  46. data/lib/sequel/plugins/many_through_many.rb +34 -2
  47. data/lib/sequel/plugins/prepared_statements_with_pk.rb +1 -1
  48. data/lib/sequel/sql.rb +94 -51
  49. data/lib/sequel/version.rb +1 -1
  50. data/spec/adapters/db2_spec.rb +146 -0
  51. data/spec/adapters/postgres_spec.rb +74 -6
  52. data/spec/adapters/spec_helper.rb +1 -0
  53. data/spec/adapters/sqlite_spec.rb +11 -0
  54. data/spec/core/database_spec.rb +7 -0
  55. data/spec/core/dataset_spec.rb +180 -17
  56. data/spec/core/expression_filters_spec.rb +107 -41
  57. data/spec/core/spec_helper.rb +11 -0
  58. data/spec/extensions/many_through_many_spec.rb +115 -1
  59. data/spec/extensions/prepared_statements_with_pk_spec.rb +3 -3
  60. data/spec/integration/associations_test.rb +193 -15
  61. data/spec/integration/database_test.rb +4 -2
  62. data/spec/integration/dataset_test.rb +215 -19
  63. data/spec/integration/plugin_test.rb +8 -5
  64. data/spec/integration/prepared_statement_test.rb +91 -98
  65. data/spec/integration/schema_test.rb +27 -11
  66. data/spec/integration/spec_helper.rb +10 -0
  67. data/spec/integration/type_test.rb +2 -2
  68. data/spec/model/association_reflection_spec.rb +91 -0
  69. data/spec/model/associations_spec.rb +13 -0
  70. data/spec/model/base_spec.rb +8 -21
  71. data/spec/model/eager_loading_spec.rb +243 -9
  72. data/spec/model/model_spec.rb +15 -2
  73. metadata +16 -4
@@ -6,9 +6,6 @@ module Sequel
6
6
  # dataset supports a feature.
7
7
  # ---------------------
8
8
 
9
- # Method used to check if WITH is supported
10
- WITH_SUPPORTED=:select_with_sql
11
-
12
9
  # Whether this dataset quotes identifiers.
13
10
  def quote_identifiers?
14
11
  if defined?(@quote_identifiers)
@@ -34,11 +31,20 @@ module Sequel
34
31
  end
35
32
 
36
33
  # Whether the dataset supports common table expressions (the WITH clause).
37
- def supports_cte?
38
- select_clause_methods.include?(WITH_SUPPORTED)
34
+ # If given, +type+ can be :select, :insert, :update, or :delete, in which case it
35
+ # determines whether WITH is supported for the respective statement type.
36
+ def supports_cte?(type=:select)
37
+ send(:"#{type}_clause_methods").include?(:"#{type}_with_sql")
39
38
  end
40
39
 
41
- # Whether the dataset supports the DISTINCT ON clause, false by default.
40
+ # Whether the dataset supports common table expressions (the WITH clause)
41
+ # in subqueries. If false, applies the WITH clause to the main query, which can cause issues
42
+ # if multiple WITH clauses use the same name.
43
+ def supports_cte_in_subqueries?
44
+ false
45
+ end
46
+
47
+ # Whether the dataset supports or can emulate the DISTINCT ON clause, false by default.
42
48
  def supports_distinct_on?
43
49
  false
44
50
  end
@@ -46,7 +52,7 @@ module Sequel
46
52
  # Whether this dataset supports the +insert_select+ method for returning all columns values
47
53
  # directly from an insert query.
48
54
  def supports_insert_select?
49
- false
55
+ supports_returning?(:insert)
50
56
  end
51
57
 
52
58
  # Whether the dataset supports the INTERSECT and EXCEPT compound operations, true by default.
@@ -79,7 +85,24 @@ module Sequel
79
85
  def supports_multiple_column_in?
80
86
  true
81
87
  end
88
+
89
+ # Whether the dataset supports or can fully emulate the DISTINCT ON clause,
90
+ # including respecting the ORDER BY clause, false by default
91
+ def supports_ordered_distinct_on?
92
+ supports_distinct_on?
93
+ end
82
94
 
95
+ # Whether the RETURNING clause is supported for the given type of query.
96
+ # +type+ can be :insert, :update, or :delete.
97
+ def supports_returning?(type)
98
+ send(:"#{type}_clause_methods").include?(:"#{type}_returning_sql")
99
+ end
100
+
101
+ # Whether the database supports SELECT *, column FROM table
102
+ def supports_select_all_and_column?
103
+ true
104
+ end
105
+
83
106
  # Whether the dataset supports timezones in literal timestamps
84
107
  def supports_timestamp_timezones?
85
108
  false
@@ -94,5 +117,19 @@ module Sequel
94
117
  def supports_window_functions?
95
118
  false
96
119
  end
120
+
121
+ # Whether the dataset supports WHERE TRUE (or WHERE 1 for databases that
122
+ # that use 1 for true).
123
+ def supports_where_true?
124
+ true
125
+ end
126
+
127
+ private
128
+
129
+ # Whether the RETURNING clause is used for the given dataset.
130
+ # +type+ can be :insert, :update, or :delete.
131
+ def uses_returning?(type)
132
+ opts[:returning] && !@opts[:sql] && supports_returning?(type)
133
+ end
97
134
  end
98
135
  end
@@ -140,7 +140,13 @@ module Sequel
140
140
  "#<#{self.class}: #{sql.inspect}>"
141
141
  end
142
142
 
143
- # Splits a possible implicit alias in C, handling both SQL::AliasedExpressions
143
+ # The alias to use for the row_number column, used when emulating OFFSET
144
+ # support and for eager limit strategies
145
+ def row_number_column
146
+ :x_sequel_row_number_x
147
+ end
148
+
149
+ # Splits a possible implicit alias in +c+, handling both SQL::AliasedExpressions
144
150
  # and Symbols. Returns an array of two elements, with the first being the
145
151
  # main expression, and the second being the alias.
146
152
  def split_alias(c)
@@ -150,6 +156,8 @@ module Sequel
150
156
  [c_table ? SQL::QualifiedIdentifier.new(c_table, column.to_sym) : column.to_sym, aliaz]
151
157
  when SQL::AliasedExpression
152
158
  [c.expression, c.aliaz]
159
+ when SQL::JoinClause
160
+ [c.table, c.table_alias]
153
161
  else
154
162
  [c, nil]
155
163
  end
@@ -80,9 +80,9 @@ module Sequel
80
80
  when :select, :all
81
81
  select_sql
82
82
  when :first
83
- clone(:limit=>1).select_sql
83
+ limit(1).select_sql
84
84
  when :insert_select
85
- clone(:returning=>nil).insert_sql(*@prepared_modify_values)
85
+ returning.insert_sql(*@prepared_modify_values)
86
86
  when :insert
87
87
  insert_sql(*@prepared_modify_values)
88
88
  when :update
@@ -433,6 +433,11 @@ module Sequel
433
433
  # # SELECT * FROM a NATURAL JOIN b INNER JOIN c
434
434
  # # ON ((c.d > b.e) AND (c.f IN (SELECT g FROM b)))
435
435
  def join_table(type, table, expr=nil, options={}, &block)
436
+ if table.is_a?(Dataset) && table.opts[:with] && !supports_cte_in_subqueries?
437
+ s, ds = hoist_cte(table)
438
+ return s.join_table(type, ds, expr, options, &block)
439
+ end
440
+
436
441
  using_join = expr.is_a?(Array) && !expr.empty? && expr.all?{|x| x.is_a?(Symbol)}
437
442
  if using_join && !supports_join_using?
438
443
  h = {}
@@ -653,6 +658,18 @@ module Sequel
653
658
  qualify_to(first_source)
654
659
  end
655
660
 
661
+ # Modify the RETURNING clause, only supported on a few databases. If returning
662
+ # is used, instead of insert returning the autogenerated primary key or
663
+ # update/delete returning the number of modified rows, results are
664
+ # returned using +fetch_rows+.
665
+ #
666
+ # DB[:items].returning # RETURNING *
667
+ # DB[:items].returning(nil) # RETURNING NULL
668
+ # DB[:items].returning(:id, :name) # RETURNING id, name
669
+ def returning(*values)
670
+ clone(:returning=>values)
671
+ end
672
+
656
673
  # Returns a copy of the dataset with the order reversed. If no order is
657
674
  # given, the existing order is inverted.
658
675
  #
@@ -695,7 +712,7 @@ module Sequel
695
712
  if tables.empty?
696
713
  clone(:select => nil)
697
714
  else
698
- select(*tables.map{|t| SQL::ColumnAll.new(t)})
715
+ select(*tables.map{|t| i, a = split_alias(t); a || i}.map{|t| SQL::ColumnAll.new(t)})
699
716
  end
700
717
  end
701
718
 
@@ -708,7 +725,12 @@ module Sequel
708
725
  # DB[:items].select_append(:b) # SELECT *, b FROM items
709
726
  def select_append(*columns, &block)
710
727
  cur_sel = @opts[:select]
711
- cur_sel = [WILDCARD] if !cur_sel || cur_sel.empty?
728
+ if !cur_sel || cur_sel.empty?
729
+ unless supports_select_all_and_column?
730
+ return select_all(*(Array(@opts[:from]) + Array(@opts[:join]))).select_more(*columns, &block)
731
+ end
732
+ cur_sel = [WILDCARD]
733
+ end
712
734
  select(*(cur_sel + columns), &block)
713
735
  end
714
736
 
@@ -888,8 +910,20 @@ module Sequel
888
910
  # to keep the same row_proc/graph, but change the SQL used to custom SQL.
889
911
  #
890
912
  # DB[:items].with_sql('SELECT * FROM foo') # SELECT * FROM foo
913
+ #
914
+ # You can use placeholders in your SQL and provide arguments for those placeholders:
915
+ #
916
+ # DB[:items].with_sql('SELECT ? FROM foo', 1) # SELECT 1 FROM foo
917
+ #
918
+ # You can also provide a method name and arguments to call to get the SQL:
919
+ #
920
+ # DB[:items].with_sql(:insert_sql, :b=>1) # INSERT INTO items (b) VALUES (1)
891
921
  def with_sql(sql, *args)
892
- sql = SQL::PlaceholderLiteralString.new(sql, args) unless args.empty?
922
+ if sql.is_a?(Symbol)
923
+ sql = send(sql, *args)
924
+ else
925
+ sql = SQL::PlaceholderLiteralString.new(sql, args) unless args.empty?
926
+ end
893
927
  clone(:sql=>sql)
894
928
  end
895
929
 
@@ -926,12 +960,6 @@ module Sequel
926
960
  _filter_or_exclude(false, clause, *cond, &block)
927
961
  end
928
962
 
929
- # Treat the +block+ as a virtual_row block if not +nil+ and
930
- # add the resulting columns to the +columns+ array (modifies +columns+).
931
- def virtual_row_columns(columns, block)
932
- columns.concat(Array(Sequel.virtual_row(&block))) if block
933
- end
934
-
935
963
  # Add the dataset to the list of compounds
936
964
  def compound_clone(type, dataset, opts)
937
965
  ds = compound_from_self.clone(:compounds=>Array(@opts[:compounds]).map{|x| x.dup} + [[type, dataset.compound_from_self, opts[:all]]])
@@ -964,7 +992,13 @@ module Sequel
964
992
  when Symbol, SQL::Expression
965
993
  expr
966
994
  when TrueClass, FalseClass
967
- SQL::BooleanExpression.new(:NOOP, expr)
995
+ if supports_where_true?
996
+ SQL::BooleanExpression.new(:NOOP, expr)
997
+ elsif expr
998
+ SQL::Constants::SQLTRUE
999
+ else
1000
+ SQL::Constants::SQLFALSE
1001
+ end
968
1002
  when String
969
1003
  LiteralString.new("(#{expr})")
970
1004
  else
@@ -972,6 +1006,13 @@ module Sequel
972
1006
  end
973
1007
  end
974
1008
 
1009
+ # Return two datasets, the first a clone of the receiver with the WITH
1010
+ # clause from the given dataset added to it, and the second a clone of
1011
+ # the given dataset with the WITH clause removed.
1012
+ def hoist_cte(ds)
1013
+ [clone(:with => (opts[:with] || []) + ds.opts[:with]), ds.clone(:with => nil)]
1014
+ end
1015
+
975
1016
  # Inverts the given order by breaking it into a list of column references
976
1017
  # and inverting them.
977
1018
  #
@@ -989,5 +1030,17 @@ module Sequel
989
1030
  end
990
1031
  end
991
1032
  end
1033
+
1034
+ # Return self if the dataset already has a server, or a cloned dataset with the
1035
+ # default server otherwise.
1036
+ def default_server
1037
+ @opts[:server] ? self : clone(:server=>:default)
1038
+ end
1039
+
1040
+ # Treat the +block+ as a virtual_row block if not +nil+ and
1041
+ # add the resulting columns to the +columns+ array (modifies +columns+).
1042
+ def virtual_row_columns(columns, block)
1043
+ columns.concat(Array(Sequel.virtual_row(&block))) if block
1044
+ end
992
1045
  end
993
1046
  end
@@ -100,10 +100,8 @@ module Sequel
100
100
  literal_false
101
101
  when Array
102
102
  literal_array(v)
103
- when SQLTime
104
- literal_sqltime(v)
105
103
  when Time
106
- literal_time(v)
104
+ v.is_a?(SQLTime) ? literal_sqltime(v) : literal_time(v)
107
105
  when DateTime
108
106
  literal_datetime(v)
109
107
  when Date
@@ -214,7 +212,11 @@ module Sequel
214
212
 
215
213
  # SQL fragment for BooleanConstants
216
214
  def boolean_constant_sql(constant)
217
- literal(constant)
215
+ if (constant == true || constant == false) && !supports_where_true?
216
+ constant == true ? '(1 = 1)' : '(1 = 0)'
217
+ else
218
+ literal(constant)
219
+ end
218
220
  end
219
221
 
220
222
  # SQL fragment for CaseExpression
@@ -303,6 +305,8 @@ module Sequel
303
305
  literal(args.at(0))
304
306
  when :'B~'
305
307
  "~#{literal(args.at(0))}"
308
+ when :extract
309
+ "extract(#{args.at(0)} FROM #{literal(args.at(1))})"
306
310
  else
307
311
  raise(InvalidOperation, "invalid operator #{op}")
308
312
  end
@@ -437,7 +441,7 @@ module Sequel
437
441
  when :all
438
442
  "ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING"
439
443
  when :rows
440
- "ROWS UNBOUNDED PRECEDING"
444
+ "ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW"
441
445
  when String
442
446
  opts[:frame]
443
447
  else
@@ -639,6 +643,15 @@ module Sequel
639
643
  end
640
644
  end
641
645
 
646
+ # SQL fragment specifying the values to return.
647
+ def insert_returning_sql(sql)
648
+ if opts.has_key?(:returning)
649
+ sql << " RETURNING #{column_list(Array(opts[:returning]))}"
650
+ end
651
+ end
652
+ alias delete_returning_sql insert_returning_sql
653
+ alias update_returning_sql insert_returning_sql
654
+
642
655
  # SQL fragment specifying a JOIN type, converts underscores to
643
656
  # spaces and upcases.
644
657
  def join_type_sql(join_type)
@@ -870,6 +883,10 @@ module Sequel
870
883
  return if !ws || ws.empty?
871
884
  sql.replace("#{select_with_sql_base}#{ws.map{|w| "#{quote_identifier(w[:name])}#{"(#{argument_list(w[:args])})" if w[:args]} AS #{literal_dataset(w[:dataset])}"}.join(COMMA_SEPARATOR)} #{sql}")
872
885
  end
886
+ alias delete_with_sql select_with_sql
887
+ alias insert_with_sql select_with_sql
888
+ alias update_with_sql select_with_sql
889
+
873
890
 
874
891
  # The base keyword to use for the SQL WITH clause
875
892
  def select_with_sql_base
@@ -11,9 +11,9 @@ module Sequel
11
11
  # with the +Database+ in +source+ to create the
12
12
  # dataset to use)
13
13
  # Dataset :: Sets the dataset for this model to +source+.
14
- # Symbol :: Sets the table name for this model to +source+. The
15
- # class will use the default database for model
16
- # classes in order to create the dataset.
14
+ # other :: Sets the table name for this model to +source+. The
15
+ # class will use the default database for model
16
+ # classes in order to create the dataset.
17
17
  #
18
18
  # The purpose of this method is to set the dataset/database automatically
19
19
  # for a model class, if the table name doesn't match the implicit
@@ -79,6 +79,27 @@ module Sequel
79
79
  true
80
80
  end
81
81
 
82
+ # The eager limit strategy to use for this dataset.
83
+ def eager_limit_strategy
84
+ fetch(:_eager_limit_strategy) do
85
+ self[:_eager_limit_strategy] = if self[:limit]
86
+ case s = self.fetch(:eager_limit_strategy){self[:model].default_eager_limit_strategy || :ruby}
87
+ when true
88
+ ds = associated_class.dataset
89
+ if ds.supports_window_functions?
90
+ :window_function
91
+ else
92
+ :ruby
93
+ end
94
+ else
95
+ s
96
+ end
97
+ else
98
+ nil
99
+ end
100
+ end
101
+ end
102
+
82
103
  # By default associations do not need to select a key in an associated table
83
104
  # to eagerly load.
84
105
  def eager_loading_use_associated_key?
@@ -92,6 +113,15 @@ module Sequel
92
113
  true
93
114
  end
94
115
 
116
+ # The limit and offset for this association (returned as a two element array).
117
+ def limit_and_offset
118
+ if (v = self[:limit]).is_a?(Array)
119
+ v
120
+ else
121
+ [v, nil]
122
+ end
123
+ end
124
+
95
125
  # Whether the associated object needs a primary key to be added/removed,
96
126
  # false by default.
97
127
  def need_associated_primary_key?
@@ -193,6 +223,11 @@ module Sequel
193
223
  self[:key].nil?
194
224
  end
195
225
 
226
+ # many_to_one associations don't need an eager limit strategy
227
+ def eager_limit_strategy
228
+ nil
229
+ end
230
+
196
231
  # The key to use for the key hash when eager loading
197
232
  def eager_loader_key
198
233
  self[:eager_loader_key] ||= self[:key]
@@ -248,7 +283,7 @@ module Sequel
248
283
  def can_have_associated_objects?(obj)
249
284
  !self[:primary_keys].any?{|k| obj.send(k).nil?}
250
285
  end
251
-
286
+
252
287
  # Default foreign key name symbol for key in associated table that points to
253
288
  # current table's primary key.
254
289
  def default_key
@@ -259,6 +294,11 @@ module Sequel
259
294
  def eager_loader_key
260
295
  self[:eager_loader_key] ||= primary_key
261
296
  end
297
+
298
+ # The hash key to use for the eager loading predicate (left side of IN (1, 2, 3))
299
+ def eager_loading_predicate_key
300
+ self[:eager_loading_predicate_key] ||= self[:uses_composite_keys] ? self[:keys].map{|k| SQL::QualifiedIdentifier.new(associated_class.table_name, k)} : SQL::QualifiedIdentifier.new(associated_class.table_name, self[:key])
301
+ end
262
302
 
263
303
  # The column in the current table that the key in the associated table references.
264
304
  def primary_key
@@ -297,6 +337,31 @@ module Sequel
297
337
  class OneToOneAssociationReflection < OneToManyAssociationReflection
298
338
  ASSOCIATION_TYPES[:one_to_one] = self
299
339
 
340
+ # one_to_one associations don't use an eager limit strategy by default, but
341
+ # support both DISTINCT ON and window functions as strategies.
342
+ def eager_limit_strategy
343
+ fetch(:_eager_limit_strategy) do
344
+ self[:_eager_limit_strategy] = case s = self[:eager_limit_strategy]
345
+ when Symbol
346
+ s
347
+ when true
348
+ ds = associated_class.dataset
349
+ if ds.supports_ordered_distinct_on?
350
+ :distinct_on
351
+ elsif ds.supports_window_functions?
352
+ :window_function
353
+ end
354
+ else
355
+ nil
356
+ end
357
+ end
358
+ end
359
+
360
+ # The limit and offset for this association (returned as a two element array).
361
+ def limit_and_offset
362
+ [1, nil]
363
+ end
364
+
300
365
  # one_to_one associations return a single object, not an array
301
366
  def returns_array?
302
367
  false
@@ -362,6 +427,11 @@ module Sequel
362
427
  self[:eager_loader_key] ||= self[:left_primary_key]
363
428
  end
364
429
 
430
+ # The hash key to use for the eager loading predicate (left side of IN (1, 2, 3))
431
+ def eager_loading_predicate_key
432
+ self[:eager_loading_predicate_key] ||= self[:uses_left_composite_keys] ? self[:left_keys].map{|k| SQL::QualifiedIdentifier.new(self[:join_table], k)} : SQL::QualifiedIdentifier.new(self[:join_table], self[:left_key])
433
+ end
434
+
365
435
  # many_to_many associations need to select a key in an associated table to eagerly load
366
436
  def eager_loading_use_associated_key?
367
437
  true
@@ -457,6 +527,12 @@ module Sequel
457
527
  # Project.association_reflection(:portfolio)
458
528
  # => {:type => :many_to_one, :name => :portfolio, ...}
459
529
  #
530
+ # Associations should not have the same names as any of the columns in the
531
+ # model's current table they reference. If you are dealing with an existing schema that
532
+ # has a column named status, you can't name the association status, you'd
533
+ # have to name it foo_status or something else. If you give an association the same name
534
+ # as a column, you will probably end up with an association that doesn't work, or a SystemStackError.
535
+ #
460
536
  # For a more in depth general overview, as well as a reference guide,
461
537
  # see the {Association Basics guide}[link:files/doc/association_basics_rdoc.html].
462
538
  # For examples of advanced usage, see the {Advanced Associations guide}[link:files/doc/advanced_associations_rdoc.html].
@@ -464,6 +540,9 @@ module Sequel
464
540
  # All association reflections defined for this model (default: {}).
465
541
  attr_reader :association_reflections
466
542
 
543
+ # The default :eager_limit_strategy option to use for *_many associations (default: nil)
544
+ attr_accessor :default_eager_limit_strategy
545
+
467
546
  # Array of all association reflections for this model class
468
547
  def all_association_reflections
469
548
  association_reflections.values
@@ -495,8 +574,7 @@ module Sequel
495
574
  # :after_add :: Symbol, Proc, or array of both/either specifying a callback to call
496
575
  # after a new item is added to the association.
497
576
  # :after_load :: Symbol, Proc, or array of both/either specifying a callback to call
498
- # after the associated record(s) have been retrieved from the database. Not called
499
- # when eager loading via eager_graph, but called when eager loading via eager.
577
+ # after the associated record(s) have been retrieved from the database.
500
578
  # :after_remove :: Symbol, Proc, or array of both/either specifying a callback to call
501
579
  # after an item is removed from the association.
502
580
  # :after_set :: Symbol, Proc, or array of both/either specifying a callback to call
@@ -538,6 +616,14 @@ module Sequel
538
616
  # additional key :eager_block, a callback accepting one argument, the associated dataset. This
539
617
  # is used to customize the association at query time.
540
618
  # Should return a copy of the dataset with the association graphed into it.
619
+ # :eager_limit_strategy :: Determines the strategy used for enforcing limits when eager loading associations via
620
+ # the +eager+ method. For one_to_one associations, no strategy is used by default, and
621
+ # for *_many associations, the :ruby strategy is used by default, which still retrieves
622
+ # all records but slices the resulting array after the association is retrieved. You
623
+ # can pass a +true+ value for this option to have Sequel pick what it thinks is the best
624
+ # choice for the database, or specify a specific symbol to manually select a strategy.
625
+ # one_to_one associations support :distinct_on, :window_function, and :correlated_subquery.
626
+ # *_many associations support :ruby, :window_function, and :correlated_subquery.
541
627
  # :eager_loader :: A proc to use to implement eager loading, overriding the default. Takes one or three arguments.
542
628
  # If three arguments, the first should be a key hash (used solely to enhance performance), the
543
629
  # second an array of records, and the third a hash of dependent associations. If one argument, is
@@ -705,7 +791,8 @@ module Sequel
705
791
  # Copy the association reflections to the subclass
706
792
  def inherited(subclass)
707
793
  super
708
- subclass.instance_variable_set(:@association_reflections, @association_reflections.dup)
794
+ subclass.instance_variable_set(:@association_reflections, association_reflections.dup)
795
+ subclass.default_eager_limit_strategy = default_eager_limit_strategy
709
796
  end
710
797
 
711
798
  # Shortcut for adding a many_to_many association, see #associate
@@ -730,6 +817,73 @@ module Sequel
730
817
 
731
818
  private
732
819
 
820
+ # Use a correlated subquery to limit the results of the eager loading dataset.
821
+ def apply_correlated_subquery_eager_limit_strategy(ds, opts)
822
+ klass = opts.associated_class
823
+ kds = klass.dataset
824
+ dsa = ds.send(:dataset_alias, 1)
825
+ raise Error, "can't use a correlated subquery if the associated class (#{opts.associated_class.inspect}) does not have a primary key" unless pk = klass.primary_key
826
+ pka = Array(pk)
827
+ raise Error, "can't use a correlated subquery if the associated class (#{opts.associated_class.inspect}) has a composite primary key and the database does not support multiple column IN" if pka.length > 1 && !ds.supports_multiple_column_in?
828
+ table = kds.opts[:from]
829
+ raise Error, "can't use a correlated subquery unless the associated class (#{opts.associated_class.inspect}) uses a single FROM table" unless table && table.length == 1
830
+ table = table.first
831
+ if order = ds.opts[:order]
832
+ oproc = lambda do |x|
833
+ case x
834
+ when Symbol
835
+ t, c, a = ds.send(:split_symbol, x)
836
+ if t && t.to_sym == table
837
+ SQL::QualifiedIdentifier.new(dsa, c)
838
+ else
839
+ x
840
+ end
841
+ when SQL::QualifiedIdentifier
842
+ if x.table == table
843
+ SQL::QualifiedIdentifier.new(dsa, x.column)
844
+ else
845
+ x
846
+ end
847
+ when SQL::OrderedExpression
848
+ SQL::OrderedExpression.new(oproc.call(x.expression), x.descending, :nulls=>x.nulls)
849
+ else
850
+ x
851
+ end
852
+ end
853
+ order = order.map(&oproc)
854
+ end
855
+ limit, offset = opts.limit_and_offset
856
+
857
+ subquery = yield kds.
858
+ unlimited.
859
+ from(SQL::AliasedExpression.new(table, dsa)).
860
+ select(*pka.map{|k| SQL::QualifiedIdentifier.new(dsa, k)}).
861
+ order(*order).
862
+ limit(limit, offset)
863
+
864
+ pk = if pk.is_a?(Array)
865
+ pk.map{|k| SQL::QualifiedIdentifier.new(table, k)}
866
+ else
867
+ SQL::QualifiedIdentifier.new(table, pk)
868
+ end
869
+ ds.filter(pk=>subquery)
870
+ end
871
+
872
+ # Use a window function to limit the results of the eager loading dataset.
873
+ def apply_window_function_eager_limit_strategy(ds, opts)
874
+ rn = ds.row_number_column
875
+ limit, offset = opts.limit_and_offset
876
+ ds = ds.unordered.select_append{row_number(:over, :partition=>opts.eager_loading_predicate_key, :order=>ds.opts[:order]){}.as(rn)}.from_self
877
+ ds = if opts[:type] == :one_to_one
878
+ ds.where(rn => 1)
879
+ elsif offset
880
+ offset += 1
881
+ ds.where(rn => (offset...(offset+limit)))
882
+ else
883
+ ds.where{SQL::Identifier.new(rn) <= limit}
884
+ end
885
+ end
886
+
733
887
  # The module to use for the association's methods. Defaults to
734
888
  # the overridable_methods_module.
735
889
  def association_module(opts={})
@@ -807,10 +961,24 @@ module Sequel
807
961
 
808
962
  opts[:eager_loader] ||= proc do |eo|
809
963
  h = eo[:key_hash][left_pk]
810
- eo[:rows].each{|object| object.associations[name] = []}
964
+ rows = eo[:rows]
965
+ rows.each{|object| object.associations[name] = []}
811
966
  r = uses_rcks ? rcks.zip(opts.right_primary_keys) : [[right, opts.right_primary_key]]
812
967
  l = uses_lcks ? [[lcks.map{|k| SQL::QualifiedIdentifier.new(join_table, k)}, h.keys]] : [[left, h.keys]]
813
- model.eager_loading_dataset(opts, opts.associated_class.inner_join(join_table, r + l), Array(opts.select), eo[:associations], eo).all do |assoc_record|
968
+ ds = model.eager_loading_dataset(opts, opts.associated_class.inner_join(join_table, r + l), Array(opts.select), eo[:associations], eo)
969
+ case opts.eager_limit_strategy
970
+ when :window_function
971
+ delete_rn = true
972
+ rn = ds.row_number_column
973
+ ds = apply_window_function_eager_limit_strategy(ds, opts)
974
+ when :correlated_subquery
975
+ ds = apply_correlated_subquery_eager_limit_strategy(ds, opts) do |xds|
976
+ dsa = ds.send(:dataset_alias, 2)
977
+ xds.inner_join(join_table, r + lcks.map{|k| [k, SQL::QualifiedIdentifier.new(join_table, k)]}, :table_alias=>dsa)
978
+ end
979
+ end
980
+ ds.all do |assoc_record|
981
+ assoc_record.values.delete(rn) if delete_rn
814
982
  hash_key = if uses_lcks
815
983
  left_key_alias.map{|k| assoc_record.values.delete(k)}
816
984
  else
@@ -819,6 +987,10 @@ module Sequel
819
987
  next unless objects = h[hash_key]
820
988
  objects.each{|object| object.associations[name].push(assoc_record)}
821
989
  end
990
+ if opts.eager_limit_strategy == :ruby
991
+ limit, offset = opts.limit_and_offset
992
+ rows.each{|o| o.associations[name] = o.associations[name].slice(offset||0, limit) || []}
993
+ end
822
994
  end
823
995
 
824
996
  join_type = opts[:graph_join_type]
@@ -928,20 +1100,38 @@ module Sequel
928
1100
  end
929
1101
  opts[:eager_loader] ||= proc do |eo|
930
1102
  h = eo[:key_hash][primary_key]
1103
+ rows = eo[:rows]
931
1104
  if one_to_one
932
- eo[:rows].each{|object| object.associations[name] = nil}
1105
+ rows.each{|object| object.associations[name] = nil}
933
1106
  else
934
- eo[:rows].each{|object| object.associations[name] = []}
1107
+ rows.each{|object| object.associations[name] = []}
935
1108
  end
936
1109
  reciprocal = opts.reciprocal
937
1110
  klass = opts.associated_class
938
- model.eager_loading_dataset(opts, klass.filter(uses_cks ? {cks.map{|k| SQL::QualifiedIdentifier.new(klass.table_name, k)}=>h.keys} : {SQL::QualifiedIdentifier.new(klass.table_name, key)=>h.keys}), opts.select, eo[:associations], eo).all do |assoc_record|
1111
+ filter_keys = opts.eager_loading_predicate_key
1112
+ ds = model.eager_loading_dataset(opts, klass.filter(filter_keys=>h.keys), opts.select, eo[:associations], eo)
1113
+ case opts.eager_limit_strategy
1114
+ when :distinct_on
1115
+ ds = ds.distinct(*filter_keys).order_prepend(*filter_keys)
1116
+ when :window_function
1117
+ delete_rn = true
1118
+ rn = ds.row_number_column
1119
+ ds = apply_window_function_eager_limit_strategy(ds, opts)
1120
+ when :correlated_subquery
1121
+ ds = apply_correlated_subquery_eager_limit_strategy(ds, opts) do |xds|
1122
+ xds.where(opts.associated_object_keys.map{|k| [SQL::QualifiedIdentifier.new(xds.first_source_alias, k), SQL::QualifiedIdentifier.new(xds.first_source_table, k)]})
1123
+ end
1124
+ end
1125
+ ds.all do |assoc_record|
1126
+ assoc_record.values.delete(rn) if delete_rn
939
1127
  hash_key = uses_cks ? cks.map{|k| assoc_record.send(k)} : assoc_record.send(key)
940
1128
  next unless objects = h[hash_key]
941
1129
  if one_to_one
942
1130
  objects.each do |object|
943
- object.associations[name] = assoc_record
944
- assoc_record.associations[reciprocal] = object if reciprocal
1131
+ unless object.associations[name]
1132
+ object.associations[name] = assoc_record
1133
+ assoc_record.associations[reciprocal] = object if reciprocal
1134
+ end
945
1135
  end
946
1136
  else
947
1137
  objects.each do |object|
@@ -950,6 +1140,10 @@ module Sequel
950
1140
  end
951
1141
  end
952
1142
  end
1143
+ if opts.eager_limit_strategy == :ruby
1144
+ limit, offset = opts.limit_and_offset
1145
+ rows.each{|o| o.associations[name] = o.associations[name].slice(offset||0, limit) || []}
1146
+ end
953
1147
  end
954
1148
 
955
1149
  join_type = opts[:graph_join_type]
@@ -1170,7 +1364,6 @@ module Sequel
1170
1364
  associations[name]
1171
1365
  else
1172
1366
  objs = _load_associated_objects(opts, dynamic_opts)
1173
- run_association_callbacks(opts, :after_load, objs)
1174
1367
  if opts.set_reciprocal_to_self?
1175
1368
  if opts.returns_array?
1176
1369
  objs.each{|o| add_reciprocal_object(opts, o)}
@@ -1179,6 +1372,8 @@ module Sequel
1179
1372
  end
1180
1373
  end
1181
1374
  associations[name] = objs
1375
+ run_association_callbacks(opts, :after_load, objs)
1376
+ associations[name]
1182
1377
  end
1183
1378
  end
1184
1379
 
@@ -1453,10 +1648,9 @@ module Sequel
1453
1648
  else
1454
1649
  # Each of the following have a symbol key for the table alias, with the following values:
1455
1650
  # :reciprocals - the reciprocal instance variable to use for this association
1651
+ # :reflections - AssociationReflection instance related to this association
1456
1652
  # :requirements - array of requirements for this association
1457
- # :alias_association_type_map - the type of association for this association
1458
- # :alias_association_name_map - the name of the association for this association
1459
- clone(:eager_graph=>{:requirements=>{}, :master=>alias_symbol(first_source), :alias_association_type_map=>{}, :alias_association_name_map=>{}, :reciprocals=>{}, :cartesian_product_number=>0})
1653
+ clone(:eager_graph=>{:requirements=>{}, :master=>alias_symbol(first_source), :reflections=>{}, :reciprocals=>{}, :cartesian_product_number=>0})
1460
1654
  end
1461
1655
  ds.eager_graph_associations(ds, model, ds.opts[:eager_graph][:master], [], *associations)
1462
1656
  end
@@ -1504,8 +1698,7 @@ module Sequel
1504
1698
  ds = ds.order_more(*qualified_expression(r[:order], assoc_table_alias)) if r[:order] and r[:order_eager_graph]
1505
1699
  eager_graph = ds.opts[:eager_graph]
1506
1700
  eager_graph[:requirements][assoc_table_alias] = requirements.dup
1507
- eager_graph[:alias_association_name_map][assoc_table_alias] = assoc_name
1508
- eager_graph[:alias_association_type_map][assoc_table_alias] = r.returns_array?
1701
+ eager_graph[:reflections][assoc_table_alias] = r
1509
1702
  eager_graph[:cartesian_product_number] += r[:cartesian_product_number] || 2
1510
1703
  ds = ds.eager_graph_associations(ds, r.associated_class, assoc_table_alias, requirements + [assoc_table_alias], *associations) unless associations.empty?
1511
1704
  ds
@@ -1682,6 +1875,9 @@ module Sequel
1682
1875
  # hashes and returning an array of model objects with all eager_graphed associations already set in the
1683
1876
  # association cache.
1684
1877
  class EagerGraphLoader
1878
+ # Hash with table alias symbol keys and after_load hook values
1879
+ attr_reader :after_load_map
1880
+
1685
1881
  # Hash with table alias symbol keys and association name values
1686
1882
  attr_reader :alias_map
1687
1883
 
@@ -1692,6 +1888,10 @@ module Sequel
1692
1888
  # Recursive hash with table alias symbol keys mapping to hashes with dependent table alias symbol keys.
1693
1889
  attr_reader :dependency_map
1694
1890
 
1891
+ # Hash with table alias symbol keys and [limit, offset] values
1892
+ attr_reader :limit_map
1893
+
1894
+ # Hash with table alias symbol keys and callable values used to create model instances
1695
1895
  # The table alias symbol for the primary model
1696
1896
  attr_reader :master
1697
1897
 
@@ -1707,6 +1907,9 @@ module Sequel
1707
1907
  # to model instances. Used so that only a single model instance is created for each object.
1708
1908
  attr_reader :records_map
1709
1909
 
1910
+ # Hash with table alias symbol keys and AssociationReflection values
1911
+ attr_reader :reflection_map
1912
+
1710
1913
  # Hash with table alias symbol keys and callable values used to create model instances
1711
1914
  attr_reader :row_procs
1712
1915
 
@@ -1721,11 +1924,21 @@ module Sequel
1721
1924
  eager_graph = opts[:eager_graph]
1722
1925
  @master = eager_graph[:master]
1723
1926
  requirements = eager_graph[:requirements]
1724
- alias_map = @alias_map = eager_graph[:alias_association_name_map]
1725
- type_map = @type_map = eager_graph[:alias_association_type_map]
1927
+ reflection_map = @reflection_map = eager_graph[:reflections]
1726
1928
  reciprocal_map = @reciprocal_map = eager_graph[:reciprocals]
1727
1929
  @unique = eager_graph[:cartesian_product_number] > 1
1728
1930
 
1931
+ alias_map = @alias_map = {}
1932
+ type_map = @type_map = {}
1933
+ after_load_map = @after_load_map = {}
1934
+ limit_map = @limit_map = {}
1935
+ reflection_map.each do |k, v|
1936
+ alias_map[k] = v[:name]
1937
+ type_map[k] = v.returns_array?
1938
+ after_load_map[k] = v[:after_load] unless v[:after_load].empty?
1939
+ limit_map[k] = v.limit_and_offset if v[:limit]
1940
+ end
1941
+
1729
1942
  # Make dependency map hash out of requirements array for each association.
1730
1943
  # This builds a tree of dependencies that will be used for recursion
1731
1944
  # to ensure that all parts of the object graph are loaded into the
@@ -1830,8 +2043,9 @@ module Sequel
1830
2043
  end
1831
2044
 
1832
2045
  # Remove duplicate records from all associations if this graph could possibly be a cartesian product
1833
- unique(records, dm) if @unique
1834
-
2046
+ # Run after_load procs if there are any
2047
+ post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty?
2048
+
1835
2049
  records
1836
2050
  end
1837
2051
 
@@ -1863,7 +2077,7 @@ module Sequel
1863
2077
  assoc[assoc_name].push(rec)
1864
2078
  rec.associations[rcm] = current if rcm
1865
2079
  else
1866
- current.associations[assoc_name] = rec
2080
+ current.associations[assoc_name] ||= rec
1867
2081
  end
1868
2082
  # Recurse into dependencies of the current object
1869
2083
  _load(deps, rec, h) unless deps.empty?
@@ -1921,16 +2135,25 @@ module Sequel
1921
2135
  # In that case, for each object in all associations loaded via +eager_graph+, run
1922
2136
  # uniq! on the association to make sure no duplicate records show up.
1923
2137
  # Note that this can cause legitimate duplicate records to be removed.
1924
- def unique(records, dependency_map)
2138
+ def post_process(records, dependency_map)
1925
2139
  records.each do |record|
1926
2140
  dependency_map.each do |ta, deps|
1927
- list = record.send(alias_map[ta])
1928
- list = if type_map[ta]
2141
+ assoc_name = alias_map[ta]
2142
+ list = record.send(assoc_name)
2143
+ rec_list = if type_map[ta]
1929
2144
  list.uniq!
1930
- unique(list, deps) if !list.empty? && !deps.empty?
2145
+ if lo = limit_map[ta]
2146
+ limit, offset = lo
2147
+ list.replace(list[offset||0, limit])
2148
+ end
2149
+ list
1931
2150
  elsif list
1932
- unique([list], deps) unless deps.empty?
2151
+ [list]
2152
+ else
2153
+ []
1933
2154
  end
2155
+ record.send(:run_association_callbacks, reflection_map[ta], :after_load, list) if after_load_map[ta]
2156
+ post_process(rec_list, deps) if !rec_list.empty? && !deps.empty?
1934
2157
  end
1935
2158
  end
1936
2159
  end