sequel 3.27.0 → 3.28.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.
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