sequel 3.21.0 → 3.24.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 (104) hide show
  1. data/CHANGELOG +112 -0
  2. data/README.rdoc +15 -1
  3. data/doc/association_basics.rdoc +159 -40
  4. data/doc/model_hooks.rdoc +64 -27
  5. data/doc/prepared_statements.rdoc +8 -4
  6. data/doc/reflection.rdoc +8 -2
  7. data/doc/release_notes/3.22.0.txt +39 -0
  8. data/doc/release_notes/3.23.0.txt +172 -0
  9. data/doc/release_notes/3.24.0.txt +420 -0
  10. data/doc/virtual_rows.rdoc +2 -2
  11. data/lib/sequel/adapters/ado.rb +2 -1
  12. data/lib/sequel/adapters/db2.rb +8 -1
  13. data/lib/sequel/adapters/firebird.rb +25 -9
  14. data/lib/sequel/adapters/informix.rb +4 -19
  15. data/lib/sequel/adapters/jdbc/h2.rb +5 -0
  16. data/lib/sequel/adapters/jdbc/informix.rb +31 -0
  17. data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
  18. data/lib/sequel/adapters/jdbc/mssql.rb +0 -32
  19. data/lib/sequel/adapters/jdbc/mysql.rb +9 -0
  20. data/lib/sequel/adapters/jdbc/sqlserver.rb +46 -0
  21. data/lib/sequel/adapters/jdbc.rb +39 -20
  22. data/lib/sequel/adapters/odbc.rb +2 -0
  23. data/lib/sequel/adapters/oracle.rb +12 -0
  24. data/lib/sequel/adapters/postgres.rb +30 -1
  25. data/lib/sequel/adapters/shared/access.rb +10 -0
  26. data/lib/sequel/adapters/shared/informix.rb +45 -0
  27. data/lib/sequel/adapters/shared/mssql.rb +106 -11
  28. data/lib/sequel/adapters/shared/mysql.rb +25 -7
  29. data/lib/sequel/adapters/shared/postgres.rb +39 -6
  30. data/lib/sequel/adapters/shared/sqlite.rb +57 -5
  31. data/lib/sequel/adapters/sqlite.rb +8 -3
  32. data/lib/sequel/adapters/swift/mysql.rb +9 -0
  33. data/lib/sequel/adapters/tinytds.rb +4 -3
  34. data/lib/sequel/ast_transformer.rb +190 -0
  35. data/lib/sequel/core.rb +1 -1
  36. data/lib/sequel/database/connecting.rb +1 -1
  37. data/lib/sequel/database/misc.rb +6 -0
  38. data/lib/sequel/database/query.rb +33 -3
  39. data/lib/sequel/database/schema_methods.rb +13 -4
  40. data/lib/sequel/dataset/features.rb +6 -0
  41. data/lib/sequel/dataset/prepared_statements.rb +17 -2
  42. data/lib/sequel/dataset/query.rb +17 -0
  43. data/lib/sequel/dataset/sql.rb +2 -53
  44. data/lib/sequel/exceptions.rb +4 -0
  45. data/lib/sequel/extensions/columns_introspection.rb +61 -0
  46. data/lib/sequel/extensions/migration.rb +4 -3
  47. data/lib/sequel/extensions/to_dot.rb +95 -83
  48. data/lib/sequel/model/associations.rb +234 -32
  49. data/lib/sequel/model/base.rb +187 -60
  50. data/lib/sequel/model/exceptions.rb +3 -1
  51. data/lib/sequel/model.rb +5 -0
  52. data/lib/sequel/plugins/association_pks.rb +6 -4
  53. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  54. data/lib/sequel/plugins/identity_map.rb +2 -2
  55. data/lib/sequel/plugins/many_through_many.rb +33 -3
  56. data/lib/sequel/plugins/prepared_statements.rb +140 -0
  57. data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
  58. data/lib/sequel/plugins/prepared_statements_safe.rb +72 -0
  59. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  60. data/lib/sequel/plugins/serialization_modification_detection.rb +51 -0
  61. data/lib/sequel/plugins/single_table_inheritance.rb +2 -0
  62. data/lib/sequel/plugins/xml_serializer.rb +1 -1
  63. data/lib/sequel/sql.rb +8 -0
  64. data/lib/sequel/version.rb +1 -1
  65. data/spec/adapters/mssql_spec.rb +36 -0
  66. data/spec/adapters/mysql_spec.rb +6 -0
  67. data/spec/adapters/postgres_spec.rb +43 -18
  68. data/spec/adapters/spec_helper.rb +5 -0
  69. data/spec/core/connection_pool_spec.rb +56 -77
  70. data/spec/core/database_spec.rb +33 -0
  71. data/spec/core/dataset_spec.rb +127 -16
  72. data/spec/core/expression_filters_spec.rb +13 -0
  73. data/spec/core/schema_spec.rb +13 -1
  74. data/spec/core/spec_helper.rb +5 -0
  75. data/spec/extensions/association_pks_spec.rb +7 -0
  76. data/spec/extensions/columns_introspection_spec.rb +91 -0
  77. data/spec/extensions/defaults_setter_spec.rb +64 -0
  78. data/spec/extensions/many_through_many_spec.rb +77 -0
  79. data/spec/extensions/migration_spec.rb +17 -17
  80. data/spec/extensions/nested_attributes_spec.rb +1 -0
  81. data/spec/extensions/prepared_statements_associations_spec.rb +126 -0
  82. data/spec/extensions/prepared_statements_safe_spec.rb +69 -0
  83. data/spec/extensions/prepared_statements_spec.rb +72 -0
  84. data/spec/extensions/prepared_statements_with_pk_spec.rb +38 -0
  85. data/spec/extensions/serialization_modification_detection_spec.rb +36 -0
  86. data/spec/extensions/single_table_inheritance_spec.rb +11 -0
  87. data/spec/extensions/spec_helper.rb +3 -1
  88. data/spec/extensions/to_dot_spec.rb +3 -5
  89. data/spec/extensions/xml_serializer_spec.rb +12 -0
  90. data/spec/integration/associations_test.rb +212 -0
  91. data/spec/integration/dataset_test.rb +8 -1
  92. data/spec/integration/plugin_test.rb +134 -0
  93. data/spec/integration/prepared_statement_test.rb +72 -1
  94. data/spec/integration/schema_test.rb +66 -8
  95. data/spec/integration/spec_helper.rb +5 -0
  96. data/spec/integration/transaction_test.rb +40 -0
  97. data/spec/integration/type_test.rb +7 -0
  98. data/spec/model/associations_spec.rb +463 -5
  99. data/spec/model/base_spec.rb +59 -0
  100. data/spec/model/eager_loading_spec.rb +269 -1
  101. data/spec/model/hooks_spec.rb +161 -0
  102. data/spec/model/record_spec.rb +30 -0
  103. data/spec/model/spec_helper.rb +5 -0
  104. metadata +29 -4
@@ -4,109 +4,111 @@
4
4
  # of the dataset's abstract syntax tree.
5
5
 
6
6
  module Sequel
7
- class Dataset
7
+ class ToDot
8
8
  # The option keys that should be included in the dot output.
9
9
  TO_DOT_OPTIONS = [:with, :distinct, :select, :from, :join, :where, :group, :having, :compounds, :order, :limit, :offset, :lock].freeze
10
10
 
11
- # Return a string that can be processed by the +dot+ program (included
12
- # with graphviz) in order to see a visualization of the dataset's
13
- # abstract syntax tree.
14
- def to_dot
15
- i = 0
16
- dot = ["digraph G {", "#{i} [label=\"self\"];"]
17
- _to_dot(dot, "", i, self, i)
18
- dot << "}"
19
- dot.join("\n")
11
+ # Given a +Dataset+, return a string in +dot+ format that will
12
+ # generate a visualization of the dataset.
13
+ def self.output(ds)
14
+ new(ds).output
15
+ end
16
+
17
+ # Given a +Dataset+, parse the internal structure to generate
18
+ # a dataset visualization.
19
+ def initialize(ds)
20
+ @i = 0
21
+ @stack = [@i]
22
+ @dot = ["digraph G {", "0 [label=\"self\"];"]
23
+ v(ds, "")
24
+ @dot << "}"
25
+ end
26
+
27
+ # Output the dataset visualization as a string in +dot+ format.
28
+ def output
29
+ @dot.join("\n")
20
30
  end
21
31
 
22
32
  private
23
33
 
24
- # Internal recursive version that handles all object types understood
25
- # by Sequel. Arguments:
26
- # * dot :: An array of strings representing the lines in the returned
27
- # output. This function just pushes strings onto this array.
28
- # * l :: The transition label from the parent node of the AST to the
29
- # current node.
30
- # * c :: An integer representing the parent node of the AST.
31
- # * e :: The current node of the AST.
32
- # * i :: The integer representing the last created node of the AST.
33
- #
34
- # The basic algorithm is that the +i+ is incremented to get the current
35
- # node's integer. Then the transition from the parent node to the
36
- # current node is added to the +dot+ array. Finally, the current node
37
- # is added to the +dot+ array, and if it is a compound node with children,
38
- # its children are then added by recursively calling this method. The
39
- # return value is the integer representing the last created node.
40
- def _to_dot(dot, l, c, e, i)
41
- i += 1
42
- dot << "#{c} -> #{i} [label=\"#{l}\"];" if l
43
- c = i
34
+ # Add an entry to the +dot+ output with the given label. If +j+
35
+ # is given, it is used directly as the node or transition. Otherwise
36
+ # a node is created for the current object.
37
+ def dot(label, j=nil)
38
+ @dot << "#{j||@i} [label=#{label.to_s.inspect}];"
39
+ end
40
+
41
+ # Recursive method that parses all of Sequel's internal datastructures,
42
+ # adding the appropriate nodes and transitions to the internal +dot+
43
+ # structure.
44
+ def v(e, l)
45
+ @i += 1
46
+ dot(l, "#{@stack.last} -> #{@i}") if l
47
+ @stack.push(@i)
44
48
  case e
45
49
  when LiteralString
46
- dot << "#{i} [label=\"#{e.inspect.gsub('"', '\\"')}.lit\"];"
47
- i
50
+ dot "#{e.inspect}.lit"
48
51
  when Symbol, Numeric, String, Class, TrueClass, FalseClass, NilClass
49
- dot << "#{i} [label=\"#{e.inspect.gsub('"', '\\"')}\"];"
50
- i
52
+ dot e.inspect
51
53
  when Array
52
- dot << "#{i} [label=\"Array\"];"
53
- e.each_with_index do |v, j|
54
- i = _to_dot(dot, j, c, v, i)
54
+ dot "Array"
55
+ e.each_with_index do |val, j|
56
+ v(val, j)
55
57
  end
56
58
  when Hash
57
- dot << "#{i} [label=\"Hash\"];"
58
- e.each do |k, v|
59
- i = _to_dot(dot, k, c, v, i)
59
+ dot "Hash"
60
+ e.each do |k, val|
61
+ v(val, k)
60
62
  end
61
63
  when SQL::ComplexExpression
62
- dot << "#{i} [label=\"ComplexExpression: #{e.op}\"];"
63
- e.args.each_with_index do |v, j|
64
- i = _to_dot(dot, j, c, v, i)
64
+ dot "ComplexExpression: #{e.op}"
65
+ e.args.each_with_index do |val, j|
66
+ v(val, j)
65
67
  end
66
68
  when SQL::Identifier
67
- dot << "#{i} [label=\"Identifier\"];"
68
- i = _to_dot(dot, :value, c, e.value, i)
69
+ dot "Identifier"
70
+ v(e.value, :value)
69
71
  when SQL::QualifiedIdentifier
70
- dot << "#{i} [label=\"QualifiedIdentifier\"];"
71
- i = _to_dot(dot, :table, c, e.table, i)
72
- i = _to_dot(dot, :column, c, e.column, i)
72
+ dot "QualifiedIdentifier"
73
+ v(e.table, :table)
74
+ v(e.column, :column)
73
75
  when SQL::OrderedExpression
74
- dot << "#{i} [label=\"OrderedExpression: #{e.descending ? :DESC : :ASC}#{" NULLS #{e.nulls.to_s.upcase}" if e.nulls}\"];"
75
- i = _to_dot(dot, :expression, c, e.expression, i)
76
+ dot "OrderedExpression: #{e.descending ? :DESC : :ASC}#{" NULLS #{e.nulls.to_s.upcase}" if e.nulls}"
77
+ v(e.expression, :expression)
76
78
  when SQL::AliasedExpression
77
- dot << "#{i} [label=\"AliasedExpression\"];"
78
- i = _to_dot(dot, :expression, c, e.expression, i)
79
- i = _to_dot(dot, :alias, c, e.aliaz, i)
79
+ dot "AliasedExpression"
80
+ v(e.expression, :expression)
81
+ v(e.aliaz, :alias)
80
82
  when SQL::CaseExpression
81
- dot << "#{i} [label=\"CaseExpression\"];"
82
- i = _to_dot(dot, :expression, c, e.expression, i) if e.expression
83
- i = _to_dot(dot, :conditions, c, e.conditions, i)
84
- i = _to_dot(dot, :default, c, e.default, i)
83
+ dot "CaseExpression"
84
+ v(e.expression, :expression) if e.expression
85
+ v(e.conditions, :conditions)
86
+ v(e.default, :default)
85
87
  when SQL::Cast
86
- dot << "#{i} [label=\"Cast\"];"
87
- i = _to_dot(dot, :expr, c, e.expr, i)
88
- i = _to_dot(dot, :type, c, e.type, i)
88
+ dot "Cast"
89
+ v(e.expr, :expr)
90
+ v(e.type, :type)
89
91
  when SQL::Function
90
- dot << "#{i} [label=\"Function: #{e.f}\"];"
91
- e.args.each_with_index do |v, j|
92
- i = _to_dot(dot, j, c, v, i)
92
+ dot "Function: #{e.f}"
93
+ e.args.each_with_index do |val, j|
94
+ v(val, j)
93
95
  end
94
96
  when SQL::Subscript
95
- dot << "#{i} [label=\"Subscript: #{e.f}\"];"
96
- i = _to_dot(dot, :f, c, e.f, i)
97
- i = _to_dot(dot, :sub, c, e.sub, i)
97
+ dot "Subscript"
98
+ v(e.f, :f)
99
+ v(e.sub, :sub)
98
100
  when SQL::WindowFunction
99
- dot << "#{i} [label=\"WindowFunction\"];"
100
- i = _to_dot(dot, :function, c, e.function, i)
101
- i = _to_dot(dot, :window, c, e.window, i)
101
+ dot "WindowFunction"
102
+ v(e.function, :function)
103
+ v(e.window, :window)
102
104
  when SQL::Window
103
- dot << "#{i} [label=\"Window\"];"
104
- i = _to_dot(dot, :opts, c, e.opts, i)
105
+ dot "Window"
106
+ v(e.opts, :opts)
105
107
  when SQL::PlaceholderLiteralString
106
108
  str = e.str
107
109
  str = "(#{str})" if e.parens
108
- dot << "#{i} [label=\"PlaceholderLiteralString: #{str.inspect.gsub('"', '\\"')}\"];"
109
- i = _to_dot(dot, :args, c, e.args, i)
110
+ dot "PlaceholderLiteralString: #{str.inspect}"
111
+ v(e.args, :args)
110
112
  when SQL::JoinClause
111
113
  str = "#{e.join_type.to_s.upcase} JOIN"
112
114
  if e.is_a?(SQL::JoinOnClause)
@@ -114,24 +116,34 @@ module Sequel
114
116
  elsif e.is_a?(SQL::JoinUsingClause)
115
117
  str << " USING"
116
118
  end
117
- dot << "#{i} [label=\"#{str}\"];"
118
- i = _to_dot(dot, :table, c, e.table, i)
119
- i = _to_dot(dot, :alias, c, e.table_alias, i) if e.table_alias
119
+ dot str
120
+ v(e.table, :table)
121
+ v(e.table_alias, :alias) if e.table_alias
120
122
  if e.is_a?(SQL::JoinOnClause)
121
- i = _to_dot(dot, :on, c, e.on, i)
123
+ v(e.on, :on)
122
124
  elsif e.is_a?(SQL::JoinUsingClause)
123
- i = _to_dot(dot, :using, c, e.using, i)
125
+ v(e.using, :using)
124
126
  end
125
127
  when Dataset
126
- dot << "#{i} [label=\"Dataset\"];"
128
+ dot "Dataset"
127
129
  TO_DOT_OPTIONS.each do |k|
128
- next unless e.opts[k]
129
- i = _to_dot(dot, k, c, e.opts[k], i)
130
+ if val = e.opts[k]
131
+ v(val, k.to_s)
132
+ end
130
133
  end
131
134
  else
132
- dot << "#{i} [label=\"Unhandled: #{e.inspect.gsub('"', "''")}\"];"
135
+ dot "Unhandled: #{e.inspect}"
133
136
  end
134
- i
137
+ @stack.pop
138
+ end
139
+ end
140
+
141
+ class Dataset
142
+ # Return a string that can be processed by the +dot+ program (included
143
+ # with graphviz) in order to see a visualization of the dataset's
144
+ # abstract syntax tree.
145
+ def to_dot
146
+ ToDot.output(self)
135
147
  end
136
148
  end
137
149
  end
@@ -529,8 +529,12 @@ module Sequel
529
529
  # set to nil.
530
530
  # - :eager_graph - The associations to eagerly load via +eager_graph+ when loading the associated object(s).
531
531
  # - :eager_grapher - A proc to use to implement eager loading via +eager_graph+, overriding the default.
532
- # Takes three arguments, a dataset, an alias to use for the table to graph for this association,
533
- # and the alias that was used for the current table (since you can cascade associations),
532
+ # Takes one or three arguments. If three arguments, they are a dataset, an alias to use for
533
+ # the table to graph for this association, and the alias that was used for the current table
534
+ # (since you can cascade associations). If one argument, is passed a hash with keys :self,
535
+ # :table_alias, and :implicit_qualifier, corresponding to the three arguments, and an optional
536
+ # additional key :eager_block, a callback accepting one argument, the associated dataset. This
537
+ # is used to customize the association at query time.
534
538
  # Should return a copy of the dataset with the association graphed into it.
535
539
  # - :eager_loader - A proc to use to implement eager loading, overriding the default. Takes one or three arguments.
536
540
  # If three arguments, the first should be a key hash (used solely to enhance performance), the second an array of records,
@@ -628,7 +632,8 @@ module Sequel
628
632
  raise(Error, 'invalid association type') unless assoc_class = ASSOCIATION_TYPES[type]
629
633
  raise(Error, 'Model.associate name argument must be a symbol') unless Symbol === name
630
634
  raise(Error, ':eager_loader option must have an arity of 1 or 3') if opts[:eager_loader] && ![1, 3].include?(opts[:eager_loader].arity)
631
-
635
+ raise(Error, ':eager_grapher option must have an arity of 1 or 3') if opts[:eager_grapher] && ![1, 3].include?(opts[:eager_grapher].arity)
636
+
632
637
  # dup early so we don't modify opts
633
638
  orig_opts = opts.dup
634
639
  orig_opts = association_reflection(opts[:clone])[:orig_opts].merge(orig_opts) if opts[:clone]
@@ -678,16 +683,18 @@ module Sequel
678
683
  if opts[:eager_graph]
679
684
  ds = ds.eager_graph(opts[:eager_graph])
680
685
  ds = ds.add_graph_aliases(opts.associated_key_alias=>[opts.associated_class.table_name, opts.associated_key_alias, SQL::QualifiedIdentifier.new(opts.associated_key_table, opts.associated_key_column)]) if opts.eager_loading_use_associated_key?
681
- elsif opts.eager_loading_use_associated_key?
686
+ end
687
+ ds = ds.eager(associations) unless Array(associations).empty?
688
+ ds = opts[:eager_block].call(ds) if opts[:eager_block]
689
+ ds = eager_options[:eager_block].call(ds) if eager_options[:eager_block]
690
+ if !opts[:eager_graph] && opts.eager_loading_use_associated_key?
682
691
  ds = if opts[:uses_left_composite_keys]
683
692
  t = opts.associated_key_table
684
- ds.select_more(*opts.associated_key_alias.zip(opts.associated_key_column).map{|a, c| SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(t, c), a)})
693
+ ds.select_append(*opts.associated_key_alias.zip(opts.associated_key_column).map{|a, c| SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(t, c), a)})
685
694
  else
686
- ds.select_more(SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(opts.associated_key_table, opts.associated_key_column), opts.associated_key_alias))
695
+ ds.select_append(SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(opts.associated_key_table, opts.associated_key_column), opts.associated_key_alias))
687
696
  end
688
697
  end
689
- ds = ds.eager(associations) unless Array(associations).empty?
690
- ds = opts[:eager_block].call(ds) if opts[:eager_block]
691
698
  ds
692
699
  end
693
700
 
@@ -753,9 +760,20 @@ module Sequel
753
760
  def_association_method(opts)
754
761
  end
755
762
 
756
- # Adds the association method to the association methods module.
757
- def def_association_method(opts)
758
- association_module_def(opts.association_method, opts){|*reload| load_associated_objects(opts, reload[0])}
763
+ # Adds the association method to the association methods module. Be backwards
764
+ # compatible with ruby 1.8.6, which doesn't support blocks taking block arguments.
765
+ if RUBY_VERSION >= '1.8.7'
766
+ class_eval <<-END, __FILE__, __LINE__+1
767
+ def def_association_method(opts)
768
+ association_module_def(opts.association_method, opts){|*dynamic_opts, &block| load_associated_objects(opts, dynamic_opts[0], &block)}
769
+ end
770
+ END
771
+ else
772
+ class_eval <<-END, __FILE__, __LINE__+1
773
+ def def_association_method(opts)
774
+ association_module_def(opts.association_method, opts){|*dynamic_opts| load_associated_objects(opts, dynamic_opts[0])}
775
+ end
776
+ END
759
777
  end
760
778
 
761
779
  # Configures many_to_many association reflection and adds the related association methods
@@ -782,8 +800,7 @@ module Sequel
782
800
  opts[:graph_join_table_join_type] ||= opts[:graph_join_type]
783
801
  opts[:after_load].unshift(:array_uniq!) if opts[:uniq]
784
802
  opts[:dataset] ||= proc{opts.associated_class.inner_join(join_table, rcks.zip(opts.right_primary_keys) + lcks.zip(lcpks.map{|k| send(k)}))}
785
- database = db
786
-
803
+
787
804
  opts[:eager_loader] ||= proc do |eo|
788
805
  h = eo[:key_hash][left_pk]
789
806
  eo[:rows].each{|object| object.associations[name] = []}
@@ -810,9 +827,10 @@ module Sequel
810
827
  jt_only_conditions = opts[:graph_join_table_only_conditions]
811
828
  jt_join_type = opts[:graph_join_table_join_type]
812
829
  jt_graph_block = opts[:graph_join_table_block]
813
- opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
814
- ds = ds.graph(join_table, use_jt_only_conditions ? jt_only_conditions : lcks.zip(lcpks) + graph_jt_conds, :select=>false, :table_alias=>ds.unused_table_alias(join_table), :join_type=>jt_join_type, :implicit_qualifier=>table_alias, :from_self_alias=>ds.opts[:eager_graph][:master], &jt_graph_block)
815
- ds.graph(opts.associated_class, use_only_conditions ? only_conditions : opts.right_primary_keys.zip(rcks) + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, &graph_block)
830
+ opts[:eager_grapher] ||= proc do |eo|
831
+ ds = eo[:self]
832
+ ds = ds.graph(join_table, use_jt_only_conditions ? jt_only_conditions : lcks.zip(lcpks) + graph_jt_conds, :select=>false, :table_alias=>ds.unused_table_alias(join_table), :join_type=>jt_join_type, :implicit_qualifier=>eo[:implicit_qualifier], :from_self_alias=>ds.opts[:eager_graph][:master], &jt_graph_block)
833
+ 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], :join_type=>join_type, &graph_block)
816
834
  end
817
835
 
818
836
  def_association_dataset_methods(opts)
@@ -876,8 +894,9 @@ module Sequel
876
894
  only_conditions = opts[:graph_only_conditions]
877
895
  conditions = opts[:graph_conditions]
878
896
  graph_block = opts[:graph_block]
879
- opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
880
- ds.graph(opts.associated_class, use_only_conditions ? only_conditions : opts.primary_keys.zip(cks) + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, :implicit_qualifier=>table_alias, :from_self_alias=>ds.opts[:eager_graph][:master], &graph_block)
897
+ opts[:eager_grapher] ||= proc do |eo|
898
+ ds = eo[:self]
899
+ ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : opts.primary_keys.zip(cks) + conditions, eo.merge(:select=>select, :join_type=>join_type, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
881
900
  end
882
901
 
883
902
  def_association_dataset_methods(opts)
@@ -936,10 +955,11 @@ module Sequel
936
955
  conditions = opts[:graph_conditions]
937
956
  opts[:cartesian_product_number] ||= one_to_one ? 0 : 1
938
957
  graph_block = opts[:graph_block]
939
- opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
940
- ds = ds.graph(opts.associated_class, use_only_conditions ? only_conditions : cks.zip(cpks) + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, :implicit_qualifier=>table_alias, :from_self_alias=>ds.opts[:eager_graph][:master], &graph_block)
958
+ opts[:eager_grapher] ||= proc do |eo|
959
+ ds = eo[:self]
960
+ ds = ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : cks.zip(cpks) + conditions, eo.merge(:select=>select, :join_type=>join_type, :from_self_alias=>ds.opts[:eager_graph][:master]), &graph_block)
941
961
  # We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
942
- ds.opts[:eager_graph][:reciprocals][assoc_alias] = opts.reciprocal
962
+ ds.opts[:eager_graph][:reciprocals][eo[:table_alias]] = opts.reciprocal
943
963
  ds
944
964
  end
945
965
 
@@ -993,6 +1013,15 @@ module Sequel
993
1013
  association_module_def(opts.remove_method, opts){|o,*args| remove_associated_object(opts, o, *args)}
994
1014
  association_module_def(opts.remove_all_method, opts){|*args| remove_all_associated_objects(opts, *args)}
995
1015
  end
1016
+
1017
+ # Return dataset to graph into given the association reflection, applying the :callback option if set.
1018
+ def eager_graph_dataset(opts, eager_options)
1019
+ ds = opts.associated_class.dataset
1020
+ if cb = eager_options[:callback]
1021
+ ds = cb.call(ds)
1022
+ end
1023
+ ds
1024
+ end
996
1025
  end
997
1026
 
998
1027
  # Instance methods used to implement the associations support.
@@ -1033,6 +1062,15 @@ module Sequel
1033
1062
  ds
1034
1063
  end
1035
1064
 
1065
+ # Return a dataset for the association after applying any dynamic callback.
1066
+ def _associated_dataset(opts, dynamic_opts)
1067
+ ds = send(opts.dataset_method)
1068
+ if callback = dynamic_opts[:callback]
1069
+ ds = callback.call(ds)
1070
+ end
1071
+ ds
1072
+ end
1073
+
1036
1074
  # Return an association dataset for the given association reflection
1037
1075
  def _dataset(opts)
1038
1076
  raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk
@@ -1045,13 +1083,14 @@ module Sequel
1045
1083
  opts[:join_table_block] ? opts[:join_table_block].call(ds) : ds
1046
1084
  end
1047
1085
 
1048
- # Return the associated objects from the dataset, without callbacks, reciprocals, and caching.
1049
- def _load_associated_objects(opts)
1086
+ # Return the associated objects from the dataset, without association callbacks, reciprocals, and caching.
1087
+ # Still apply the dynamic callback if present.
1088
+ def _load_associated_objects(opts, dynamic_opts={})
1050
1089
  if opts.returns_array?
1051
- opts.can_have_associated_objects?(self) ? send(opts.dataset_method).all : []
1090
+ opts.can_have_associated_objects?(self) ? _associated_dataset(opts, dynamic_opts).all : []
1052
1091
  else
1053
1092
  if opts.can_have_associated_objects?(self)
1054
- send(opts.dataset_method).all.first
1093
+ _associated_dataset(opts, dynamic_opts).all.first
1055
1094
  end
1056
1095
  end
1057
1096
  end
@@ -1113,12 +1152,20 @@ module Sequel
1113
1152
  end
1114
1153
 
1115
1154
  # Load the associated objects using the dataset, handling callbacks, reciprocals, and caching.
1116
- def load_associated_objects(opts, reload=false)
1155
+ def load_associated_objects(opts, dynamic_opts=nil)
1156
+ if dynamic_opts == true or dynamic_opts == false or dynamic_opts == nil
1157
+ dynamic_opts = {:reload=>dynamic_opts}
1158
+ elsif dynamic_opts.respond_to?(:call)
1159
+ dynamic_opts = {:callback=>dynamic_opts}
1160
+ end
1161
+ if block_given?
1162
+ dynamic_opts = dynamic_opts.merge(:callback=>Proc.new)
1163
+ end
1117
1164
  name = opts[:name]
1118
- if associations.include?(name) and !reload
1165
+ if associations.include?(name) and !dynamic_opts[:callback] and !dynamic_opts[:reload]
1119
1166
  associations[name]
1120
1167
  else
1121
- objs = _load_associated_objects(opts)
1168
+ objs = _load_associated_objects(opts, dynamic_opts)
1122
1169
  run_association_callbacks(opts, :after_load, objs)
1123
1170
  if opts.set_reciprocal_to_self?
1124
1171
  if opts.returns_array?
@@ -1259,6 +1306,24 @@ module Sequel
1259
1306
  # Artist.eager_graph(:albums=>:tracks).all
1260
1307
  # Artist.eager(:albums=>{:tracks=>:genre}).all
1261
1308
  # Artist.eager_graph(:albums=>{:tracks=>:genre}).all
1309
+ #
1310
+ # You can also pass a callback as a hash value in order to customize the dataset being
1311
+ # eager loaded at query time, analogous to the way the :eager_block association option
1312
+ # allows you to customize it at association definition time. For example,
1313
+ # if you wanted artists with their albums since 1990:
1314
+ #
1315
+ # Artist.eager(:albums => proc{|ds| ds.filter{year > 1990}})
1316
+ #
1317
+ # Or if you needed albums and their artist's name only, using a single query:
1318
+ #
1319
+ # Albums.eager_graph(:artist => proc{|ds| ds.select(:name)})
1320
+ #
1321
+ # To cascade eager loading while using a callback, you substitute the cascaded
1322
+ # associations with a single entry hash that has the proc callback as the key and
1323
+ # the cascaded associations as the value. This will load artists with their albums
1324
+ # since 1990, and also the tracks on those albums and the genre for those tracks:
1325
+ #
1326
+ # Artist.eager(:albums => {proc{|ds| ds.filter{year > 1990}}=>{:tracks => :genre}})
1262
1327
  module DatasetMethods
1263
1328
  # Add the <tt>eager!</tt> and <tt>eager_graph!</tt> mutation methods to the dataset.
1264
1329
  def self.extended(obj)
@@ -1343,6 +1408,55 @@ module Sequel
1343
1408
  ds.eager_graph_associations(ds, model, ds.opts[:eager_graph][:master], [], *associations)
1344
1409
  end
1345
1410
 
1411
+ # If the expression is in the form <tt>x = y</tt> where +y+ is a <tt>Sequel::Model</tt>
1412
+ # instance, assume +x+ is an association symbol and look up the association reflection
1413
+ # via the dataset's model. From there, return the appropriate SQL based on the type of
1414
+ # association and the values of the foreign/primary keys of +y+. For most association
1415
+ # types, this is a simple transformation, but for +many_to_many+ associations this
1416
+ # creates a subquery to the join table.
1417
+ def complex_expression_sql(op, args)
1418
+ r = args.at(1)
1419
+ if (((op == :'=' || op == :'!=') and r.is_a?(Sequel::Model)) ||
1420
+ (multiple = ((op == :IN || op == :'NOT IN') and ((is_ds = r.is_a?(Sequel::Dataset)) or r.all?{|x| x.is_a?(Sequel::Model)}))))
1421
+ l = args.at(0)
1422
+ if ar = model.association_reflections[l]
1423
+ if multiple
1424
+ klass = ar.associated_class
1425
+ if is_ds
1426
+ if r.respond_to?(:model)
1427
+ unless r.model <= klass
1428
+ # A dataset for a different model class, could be a valid regular query
1429
+ return super
1430
+ end
1431
+ else
1432
+ # Not a model dataset, could be a valid regular query
1433
+ return super
1434
+ end
1435
+ else
1436
+ unless r.all?{|x| x.is_a?(klass)}
1437
+ raise Sequel::Error, "invalid association class for one object for association #{l.inspect} used in dataset filter for model #{model.inspect}, expected class #{klass.inspect}"
1438
+ end
1439
+ end
1440
+ elsif !r.is_a?(ar.associated_class)
1441
+ raise Sequel::Error, "invalid association class #{r.class.inspect} for association #{l.inspect} used in dataset filter for model #{model.inspect}, expected class #{ar.associated_class.inspect}"
1442
+ end
1443
+
1444
+ if exp = association_filter_expression(op, ar, r)
1445
+ literal(exp)
1446
+ else
1447
+ raise Sequel::Error, "invalid association type #{ar[:type].inspect} for association #{l.inspect} used in dataset filter for model #{model.inspect}"
1448
+ end
1449
+ elsif multiple && (is_ds || r.empty?)
1450
+ # Not a query designed for this support, could be a valid regular query
1451
+ super
1452
+ else
1453
+ raise Sequel::Error, "invalid association #{l.inspect} used in dataset filter for model #{model.inspect}"
1454
+ end
1455
+ else
1456
+ super
1457
+ end
1458
+ end
1459
+
1346
1460
  # Do not attempt to split the result set into associations,
1347
1461
  # just return results as simple objects. This is useful if you
1348
1462
  # want to use eager_graph as a shortcut to have all of the joins
@@ -1366,10 +1480,23 @@ module Sequel
1366
1480
  # r :: association reflection for the current association
1367
1481
  # *associations :: any associations dependent on this one
1368
1482
  def eager_graph_association(ds, model, ta, requirements, r, *associations)
1369
- klass = r.associated_class
1370
1483
  assoc_name = r[:name]
1371
1484
  assoc_table_alias = ds.unused_table_alias(assoc_name)
1372
- ds = r[:eager_grapher].call(ds, assoc_table_alias, ta)
1485
+ loader = r[:eager_grapher]
1486
+ if !associations.empty?
1487
+ if associations.first.respond_to?(:call)
1488
+ callback = associations.first
1489
+ associations = {}
1490
+ elsif associations.length == 1 && (assocs = associations.first).is_a?(Hash) && assocs.length == 1 && (pr_assoc = assocs.to_a.first) && pr_assoc.first.respond_to?(:call)
1491
+ callback, assoc = pr_assoc
1492
+ associations = assoc.is_a?(Array) ? assoc : [assoc]
1493
+ end
1494
+ end
1495
+ ds = if loader.arity == 1
1496
+ loader.call(:self=>ds, :table_alias=>assoc_table_alias, :implicit_qualifier=>ta, :callback=>callback)
1497
+ else
1498
+ loader.call(ds, assoc_table_alias, ta)
1499
+ end
1373
1500
  ds = ds.order_more(*qualified_expression(r[:order], assoc_table_alias)) if r[:order] and r[:order_eager_graph]
1374
1501
  eager_graph = ds.opts[:eager_graph]
1375
1502
  eager_graph[:requirements][assoc_table_alias] = requirements.dup
@@ -1471,6 +1598,49 @@ module Sequel
1471
1598
 
1472
1599
  private
1473
1600
 
1601
+ # Return an expression for filtering by the given association reflection and associated object.
1602
+ def association_filter_expression(op, ref, obj)
1603
+ meth = :"#{ref[:type]}_association_filter_expression"
1604
+ send(meth, op, ref, obj) if respond_to?(meth, true)
1605
+ end
1606
+
1607
+ # Handle inversion for association filters by returning an inverted expression,
1608
+ # plus also handling cases where the referenced columns are NULL.
1609
+ def association_filter_handle_inversion(op, exp, cols)
1610
+ if op == :'!=' || op == :'NOT IN'
1611
+ if exp == SQL::Constants::FALSE
1612
+ ~exp
1613
+ else
1614
+ ~exp | Sequel::SQL::BooleanExpression.from_value_pairs(cols.zip([]), :OR)
1615
+ end
1616
+ else
1617
+ exp
1618
+ end
1619
+ end
1620
+
1621
+ # Return an expression for making sure that the given keys match the value of
1622
+ # the given methods for either the single object given or for any of the objects
1623
+ # given if +obj+ is an array.
1624
+ def association_filter_key_expression(keys, meths, obj)
1625
+ vals = if obj.is_a?(Sequel::Dataset)
1626
+ {(keys.length == 1 ? keys.first : keys)=>obj.select(*meths).exclude(Sequel::SQL::BooleanExpression.from_value_pairs(meths.zip([]), :OR))}
1627
+ else
1628
+ vals = Array(obj).reject{|o| !meths.all?{|m| o.send(m)}}
1629
+ return SQL::Constants::FALSE if vals.empty?
1630
+ if obj.is_a?(Array)
1631
+ if keys.length == 1
1632
+ meth = meths.first
1633
+ {keys.first=>vals.map{|o| o.send(meth)}}
1634
+ else
1635
+ {keys=>vals.map{|o| meths.map{|m| o.send(m)}}}
1636
+ end
1637
+ else
1638
+ keys.zip(meths.map{|k| obj.send(k)})
1639
+ end
1640
+ end
1641
+ SQL::BooleanExpression.from_value_pairs(vals)
1642
+ end
1643
+
1474
1644
  # Make sure the association is valid for this model, and return the related AssociationReflection.
1475
1645
  def check_association(model, association)
1476
1646
  raise(Sequel::UndefinedAssociation, "Invalid association #{association} for #{model.name}") unless reflection = model.association_reflection(association)
@@ -1557,15 +1727,47 @@ module Sequel
1557
1727
 
1558
1728
  reflections.each do |r|
1559
1729
  loader = r[:eager_loader]
1730
+ associations = eager_assoc[r[:name]]
1731
+ if associations.respond_to?(:call)
1732
+ eager_block = associations
1733
+ associations = {}
1734
+ elsif associations.is_a?(Hash) && associations.length == 1 && (pr_assoc = associations.to_a.first) && pr_assoc.first.respond_to?(:call)
1735
+ eager_block, associations = pr_assoc
1736
+ end
1560
1737
  if loader.arity == 1
1561
- loader.call(:key_hash=>key_hash, :rows=>a, :associations=>eager_assoc[r[:name]], :self=>self)
1738
+ loader.call(:key_hash=>key_hash, :rows=>a, :associations=>associations, :self=>self, :eager_block=>eager_block)
1562
1739
  else
1563
- loader.call(key_hash, a, eager_assoc[r[:name]])
1740
+ loader.call(key_hash, a, associations)
1564
1741
  end
1565
1742
  a.each{|object| object.send(:run_association_callbacks, r, :after_load, object.associations[r[:name]])} unless r[:after_load].empty?
1566
1743
  end
1567
1744
  end
1568
1745
 
1746
+ # Return a subquery expression for filering by a many_to_many association
1747
+ def many_to_many_association_filter_expression(op, ref, obj)
1748
+ lpks, lks, rks = ref.values_at(:left_primary_keys, :left_keys, :right_keys)
1749
+ lpks = lpks.first if lpks.length == 1
1750
+ exp = association_filter_key_expression(rks, ref.right_primary_keys, obj)
1751
+ if exp == SQL::Constants::FALSE
1752
+ association_filter_handle_inversion(op, exp, Array(lpks))
1753
+ else
1754
+ association_filter_handle_inversion(op, SQL::BooleanExpression.from_value_pairs(lpks=>model.db[ref[:join_table]].select(*lks).where(exp).exclude(SQL::BooleanExpression.from_value_pairs(lks.zip([]), :OR))), Array(lpks))
1755
+ end
1756
+ end
1757
+
1758
+ # Return a simple equality expression for filering by a many_to_one association
1759
+ def many_to_one_association_filter_expression(op, ref, obj)
1760
+ keys = ref[:keys]
1761
+ association_filter_handle_inversion(op, association_filter_key_expression(keys, ref.primary_keys, obj), keys)
1762
+ end
1763
+
1764
+ # Return a simple equality expression for filering by a one_to_* association
1765
+ def one_to_many_association_filter_expression(op, ref, obj)
1766
+ keys = ref[:primary_keys]
1767
+ association_filter_handle_inversion(op, association_filter_key_expression(keys, ref[:keys], obj), keys)
1768
+ end
1769
+ alias one_to_one_association_filter_expression one_to_many_association_filter_expression
1770
+
1569
1771
  # Build associations from the graph if #eager_graph was used,
1570
1772
  # and/or load other associations if #eager was used.
1571
1773
  def post_load(all_records)