sequel 3.23.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 (76) hide show
  1. data/CHANGELOG +64 -0
  2. data/doc/association_basics.rdoc +43 -5
  3. data/doc/model_hooks.rdoc +64 -27
  4. data/doc/prepared_statements.rdoc +8 -4
  5. data/doc/reflection.rdoc +8 -2
  6. data/doc/release_notes/3.23.0.txt +1 -1
  7. data/doc/release_notes/3.24.0.txt +420 -0
  8. data/lib/sequel/adapters/db2.rb +8 -1
  9. data/lib/sequel/adapters/firebird.rb +25 -9
  10. data/lib/sequel/adapters/informix.rb +4 -19
  11. data/lib/sequel/adapters/jdbc.rb +34 -17
  12. data/lib/sequel/adapters/jdbc/h2.rb +5 -0
  13. data/lib/sequel/adapters/jdbc/informix.rb +31 -0
  14. data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
  15. data/lib/sequel/adapters/jdbc/mssql.rb +0 -32
  16. data/lib/sequel/adapters/jdbc/mysql.rb +9 -0
  17. data/lib/sequel/adapters/jdbc/sqlserver.rb +46 -0
  18. data/lib/sequel/adapters/postgres.rb +30 -1
  19. data/lib/sequel/adapters/shared/access.rb +10 -0
  20. data/lib/sequel/adapters/shared/informix.rb +45 -0
  21. data/lib/sequel/adapters/shared/mssql.rb +82 -8
  22. data/lib/sequel/adapters/shared/mysql.rb +25 -7
  23. data/lib/sequel/adapters/shared/postgres.rb +39 -6
  24. data/lib/sequel/adapters/shared/sqlite.rb +57 -5
  25. data/lib/sequel/adapters/sqlite.rb +8 -3
  26. data/lib/sequel/adapters/swift/mysql.rb +9 -0
  27. data/lib/sequel/ast_transformer.rb +190 -0
  28. data/lib/sequel/core.rb +1 -1
  29. data/lib/sequel/database/misc.rb +6 -0
  30. data/lib/sequel/database/query.rb +33 -3
  31. data/lib/sequel/database/schema_methods.rb +6 -2
  32. data/lib/sequel/dataset/features.rb +6 -0
  33. data/lib/sequel/dataset/prepared_statements.rb +17 -2
  34. data/lib/sequel/dataset/query.rb +17 -0
  35. data/lib/sequel/dataset/sql.rb +2 -53
  36. data/lib/sequel/exceptions.rb +4 -0
  37. data/lib/sequel/extensions/to_dot.rb +95 -83
  38. data/lib/sequel/model.rb +5 -0
  39. data/lib/sequel/model/associations.rb +80 -14
  40. data/lib/sequel/model/base.rb +182 -55
  41. data/lib/sequel/model/exceptions.rb +3 -1
  42. data/lib/sequel/plugins/association_pks.rb +6 -4
  43. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  44. data/lib/sequel/plugins/many_through_many.rb +8 -3
  45. data/lib/sequel/plugins/prepared_statements.rb +140 -0
  46. data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
  47. data/lib/sequel/plugins/prepared_statements_safe.rb +72 -0
  48. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  49. data/lib/sequel/sql.rb +8 -0
  50. data/lib/sequel/version.rb +1 -1
  51. data/spec/adapters/postgres_spec.rb +43 -18
  52. data/spec/core/connection_pool_spec.rb +56 -77
  53. data/spec/core/database_spec.rb +25 -0
  54. data/spec/core/dataset_spec.rb +127 -16
  55. data/spec/core/expression_filters_spec.rb +13 -0
  56. data/spec/core/schema_spec.rb +6 -1
  57. data/spec/extensions/association_pks_spec.rb +7 -0
  58. data/spec/extensions/defaults_setter_spec.rb +64 -0
  59. data/spec/extensions/many_through_many_spec.rb +60 -4
  60. data/spec/extensions/nested_attributes_spec.rb +1 -0
  61. data/spec/extensions/prepared_statements_associations_spec.rb +126 -0
  62. data/spec/extensions/prepared_statements_safe_spec.rb +69 -0
  63. data/spec/extensions/prepared_statements_spec.rb +72 -0
  64. data/spec/extensions/prepared_statements_with_pk_spec.rb +38 -0
  65. data/spec/extensions/to_dot_spec.rb +3 -5
  66. data/spec/integration/associations_test.rb +155 -1
  67. data/spec/integration/dataset_test.rb +8 -1
  68. data/spec/integration/plugin_test.rb +119 -0
  69. data/spec/integration/prepared_statement_test.rb +72 -1
  70. data/spec/integration/schema_test.rb +66 -8
  71. data/spec/integration/transaction_test.rb +40 -0
  72. data/spec/model/associations_spec.rb +349 -8
  73. data/spec/model/base_spec.rb +59 -0
  74. data/spec/model/hooks_spec.rb +161 -0
  75. data/spec/model/record_spec.rb +24 -0
  76. metadata +21 -4
@@ -269,6 +269,7 @@ module Sequel
269
269
  literal(op == :IN ? expr : ~expr)
270
270
  else
271
271
  old_vals = vals
272
+ vals = vals.naked if vals.is_a?(Sequel::Dataset)
272
273
  vals = vals.to_a
273
274
  val_cols = old_vals.columns
274
275
  complex_expression_sql(op, [cols, vals.map!{|x| x.values_at(*val_cols)}])
@@ -774,59 +775,7 @@ module Sequel
774
775
 
775
776
  # Qualify the given expression e to the given table.
776
777
  def qualified_expression(e, table)
777
- case e
778
- when Symbol
779
- t, column, aliaz = split_symbol(e)
780
- if t
781
- e
782
- elsif aliaz
783
- SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(table, SQL::Identifier.new(column)), aliaz)
784
- else
785
- SQL::QualifiedIdentifier.new(table, e)
786
- end
787
- when Array
788
- e.map{|a| qualified_expression(a, table)}
789
- when Hash
790
- h = {}
791
- e.each{|k,v| h[qualified_expression(k, table)] = qualified_expression(v, table)}
792
- h
793
- when SQL::Identifier
794
- SQL::QualifiedIdentifier.new(table, e)
795
- when SQL::OrderedExpression
796
- SQL::OrderedExpression.new(qualified_expression(e.expression, table), e.descending, :nulls=>e.nulls)
797
- when SQL::AliasedExpression
798
- SQL::AliasedExpression.new(qualified_expression(e.expression, table), e.aliaz)
799
- when SQL::CaseExpression
800
- args = [qualified_expression(e.conditions, table), qualified_expression(e.default, table)]
801
- args << qualified_expression(e.expression, table) if e.expression?
802
- SQL::CaseExpression.new(*args)
803
- when SQL::Cast
804
- SQL::Cast.new(qualified_expression(e.expr, table), e.type)
805
- when SQL::Function
806
- SQL::Function.new(e.f, *qualified_expression(e.args, table))
807
- when SQL::ComplexExpression
808
- SQL::ComplexExpression.new(e.op, *qualified_expression(e.args, table))
809
- when SQL::Subscript
810
- SQL::Subscript.new(qualified_expression(e.f, table), qualified_expression(e.sub, table))
811
- when SQL::WindowFunction
812
- SQL::WindowFunction.new(qualified_expression(e.function, table), qualified_expression(e.window, table))
813
- when SQL::Window
814
- o = e.opts.dup
815
- o[:partition] = qualified_expression(o[:partition], table) if o[:partition]
816
- o[:order] = qualified_expression(o[:order], table) if o[:order]
817
- SQL::Window.new(o)
818
- when SQL::PlaceholderLiteralString
819
- args = if e.args.is_a?(Hash)
820
- h = {}
821
- e.args.each{|k,v| h[k] = qualified_expression(v, table)}
822
- h
823
- else
824
- qualified_expression(e.args, table)
825
- end
826
- SQL::PlaceholderLiteralString.new(e.str, args, e.parens)
827
- else
828
- e
829
- end
778
+ Qualifier.new(self, table).transform(e)
830
779
  end
831
780
 
832
781
  # The order of methods to call to build the SELECT SQL statement
@@ -44,6 +44,10 @@ module Sequel
44
44
  # and won't reraise it.
45
45
  class Rollback < Error ; end
46
46
 
47
+ # Exception that occurs when unbinding a dataset that has multiple different values
48
+ # for a given variable.
49
+ class UnbindDuplicate < Error; end
50
+
47
51
  class Error
48
52
  AdapterNotFound = Sequel::AdapterNotFound
49
53
  InvalidOperation = Sequel::InvalidOperation
@@ -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
data/lib/sequel/model.rb CHANGED
@@ -80,6 +80,11 @@ module Sequel
80
80
  # +super+ on the first line of your method, so later hooks are called after earlier hooks.
81
81
  AFTER_HOOKS = [:after_initialize, :after_create, :after_update, :after_save, :after_destroy, :after_validation]
82
82
 
83
+ # Hooks that are called around an action. If overridden, these methods must call super
84
+ # exactly once if the behavior they wrap is desired. The can be used to rescue exceptions
85
+ # raised by the code they wrap or ensure that some behavior is executed no matter what.
86
+ AROUND_HOOKS = [:around_create, :around_update, :around_save, :around_destroy, :around_validation]
87
+
83
88
  # Empty instance methods to create that the user can override to get hook/callback behavior.
84
89
  # Just like any other method defined by Sequel, if you override one of these, you should
85
90
  # call +super+ to get the default behavior (while empty by default, they can also be defined
@@ -1415,18 +1415,40 @@ module Sequel
1415
1415
  # types, this is a simple transformation, but for +many_to_many+ associations this
1416
1416
  # creates a subquery to the join table.
1417
1417
  def complex_expression_sql(op, args)
1418
- if op == :'=' and args.at(1).is_a?(Sequel::Model)
1419
- l, r = 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)
1420
1422
  if ar = model.association_reflections[l]
1421
- unless r.is_a?(ar.associated_class)
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)
1422
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}"
1423
1442
  end
1424
1443
 
1425
- if exp = association_filter_expression(ar, r)
1444
+ if exp = association_filter_expression(op, ar, r)
1426
1445
  literal(exp)
1427
1446
  else
1428
1447
  raise Sequel::Error, "invalid association type #{ar[:type].inspect} for association #{l.inspect} used in dataset filter for model #{model.inspect}"
1429
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
1430
1452
  else
1431
1453
  raise Sequel::Error, "invalid association #{l.inspect} used in dataset filter for model #{model.inspect}"
1432
1454
  end
@@ -1577,9 +1599,46 @@ module Sequel
1577
1599
  private
1578
1600
 
1579
1601
  # Return an expression for filtering by the given association reflection and associated object.
1580
- def association_filter_expression(ref, obj)
1602
+ def association_filter_expression(op, ref, obj)
1581
1603
  meth = :"#{ref[:type]}_association_filter_expression"
1582
- send(meth, ref, obj) if respond_to?(meth, true)
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)
1583
1642
  end
1584
1643
 
1585
1644
  # Make sure the association is valid for this model, and return the related AssociationReflection.
@@ -1685,20 +1744,27 @@ module Sequel
1685
1744
  end
1686
1745
 
1687
1746
  # Return a subquery expression for filering by a many_to_many association
1688
- def many_to_many_association_filter_expression(ref, obj)
1689
- lpks, lks, rks = ref.values_at(:left_primary_keys, :left_keys, :right_keys)
1690
- lpks = lpks.first if lpks.length == 1
1691
- SQL::BooleanExpression.from_value_pairs(lpks=>model.db[ref[:join_table]].select(*lks).where(rks.zip(ref.right_primary_keys.map{|k| obj.send(k)})))
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
1692
1756
  end
1693
1757
 
1694
1758
  # Return a simple equality expression for filering by a many_to_one association
1695
- def many_to_one_association_filter_expression(ref, obj)
1696
- SQL::BooleanExpression.from_value_pairs(ref[:keys].zip(ref.primary_keys.map{|k| obj.send(k)}))
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)
1697
1762
  end
1698
1763
 
1699
1764
  # Return a simple equality expression for filering by a one_to_* association
1700
- def one_to_many_association_filter_expression(ref, obj)
1701
- SQL::BooleanExpression.from_value_pairs(ref[:primary_keys].zip(ref[:keys].map{|k| obj.send(k)}))
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)
1702
1768
  end
1703
1769
  alias one_to_one_association_filter_expression one_to_many_association_filter_expression
1704
1770