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.
- data/CHANGELOG +112 -0
- data/README.rdoc +15 -1
- data/doc/association_basics.rdoc +159 -40
- data/doc/model_hooks.rdoc +64 -27
- data/doc/prepared_statements.rdoc +8 -4
- data/doc/reflection.rdoc +8 -2
- data/doc/release_notes/3.22.0.txt +39 -0
- data/doc/release_notes/3.23.0.txt +172 -0
- data/doc/release_notes/3.24.0.txt +420 -0
- data/doc/virtual_rows.rdoc +2 -2
- data/lib/sequel/adapters/ado.rb +2 -1
- data/lib/sequel/adapters/db2.rb +8 -1
- data/lib/sequel/adapters/firebird.rb +25 -9
- data/lib/sequel/adapters/informix.rb +4 -19
- data/lib/sequel/adapters/jdbc/h2.rb +5 -0
- data/lib/sequel/adapters/jdbc/informix.rb +31 -0
- data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
- data/lib/sequel/adapters/jdbc/mssql.rb +0 -32
- data/lib/sequel/adapters/jdbc/mysql.rb +9 -0
- data/lib/sequel/adapters/jdbc/sqlserver.rb +46 -0
- data/lib/sequel/adapters/jdbc.rb +39 -20
- data/lib/sequel/adapters/odbc.rb +2 -0
- data/lib/sequel/adapters/oracle.rb +12 -0
- data/lib/sequel/adapters/postgres.rb +30 -1
- data/lib/sequel/adapters/shared/access.rb +10 -0
- data/lib/sequel/adapters/shared/informix.rb +45 -0
- data/lib/sequel/adapters/shared/mssql.rb +106 -11
- data/lib/sequel/adapters/shared/mysql.rb +25 -7
- data/lib/sequel/adapters/shared/postgres.rb +39 -6
- data/lib/sequel/adapters/shared/sqlite.rb +57 -5
- data/lib/sequel/adapters/sqlite.rb +8 -3
- data/lib/sequel/adapters/swift/mysql.rb +9 -0
- data/lib/sequel/adapters/tinytds.rb +4 -3
- data/lib/sequel/ast_transformer.rb +190 -0
- data/lib/sequel/core.rb +1 -1
- data/lib/sequel/database/connecting.rb +1 -1
- data/lib/sequel/database/misc.rb +6 -0
- data/lib/sequel/database/query.rb +33 -3
- data/lib/sequel/database/schema_methods.rb +13 -4
- data/lib/sequel/dataset/features.rb +6 -0
- data/lib/sequel/dataset/prepared_statements.rb +17 -2
- data/lib/sequel/dataset/query.rb +17 -0
- data/lib/sequel/dataset/sql.rb +2 -53
- data/lib/sequel/exceptions.rb +4 -0
- data/lib/sequel/extensions/columns_introspection.rb +61 -0
- data/lib/sequel/extensions/migration.rb +4 -3
- data/lib/sequel/extensions/to_dot.rb +95 -83
- data/lib/sequel/model/associations.rb +234 -32
- data/lib/sequel/model/base.rb +187 -60
- data/lib/sequel/model/exceptions.rb +3 -1
- data/lib/sequel/model.rb +5 -0
- data/lib/sequel/plugins/association_pks.rb +6 -4
- data/lib/sequel/plugins/defaults_setter.rb +58 -0
- data/lib/sequel/plugins/identity_map.rb +2 -2
- data/lib/sequel/plugins/many_through_many.rb +33 -3
- data/lib/sequel/plugins/prepared_statements.rb +140 -0
- data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
- data/lib/sequel/plugins/prepared_statements_safe.rb +72 -0
- data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
- data/lib/sequel/plugins/serialization_modification_detection.rb +51 -0
- data/lib/sequel/plugins/single_table_inheritance.rb +2 -0
- data/lib/sequel/plugins/xml_serializer.rb +1 -1
- data/lib/sequel/sql.rb +8 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +36 -0
- data/spec/adapters/mysql_spec.rb +6 -0
- data/spec/adapters/postgres_spec.rb +43 -18
- data/spec/adapters/spec_helper.rb +5 -0
- data/spec/core/connection_pool_spec.rb +56 -77
- data/spec/core/database_spec.rb +33 -0
- data/spec/core/dataset_spec.rb +127 -16
- data/spec/core/expression_filters_spec.rb +13 -0
- data/spec/core/schema_spec.rb +13 -1
- data/spec/core/spec_helper.rb +5 -0
- data/spec/extensions/association_pks_spec.rb +7 -0
- data/spec/extensions/columns_introspection_spec.rb +91 -0
- data/spec/extensions/defaults_setter_spec.rb +64 -0
- data/spec/extensions/many_through_many_spec.rb +77 -0
- data/spec/extensions/migration_spec.rb +17 -17
- data/spec/extensions/nested_attributes_spec.rb +1 -0
- data/spec/extensions/prepared_statements_associations_spec.rb +126 -0
- data/spec/extensions/prepared_statements_safe_spec.rb +69 -0
- data/spec/extensions/prepared_statements_spec.rb +72 -0
- data/spec/extensions/prepared_statements_with_pk_spec.rb +38 -0
- data/spec/extensions/serialization_modification_detection_spec.rb +36 -0
- data/spec/extensions/single_table_inheritance_spec.rb +11 -0
- data/spec/extensions/spec_helper.rb +3 -1
- data/spec/extensions/to_dot_spec.rb +3 -5
- data/spec/extensions/xml_serializer_spec.rb +12 -0
- data/spec/integration/associations_test.rb +212 -0
- data/spec/integration/dataset_test.rb +8 -1
- data/spec/integration/plugin_test.rb +134 -0
- data/spec/integration/prepared_statement_test.rb +72 -1
- data/spec/integration/schema_test.rb +66 -8
- data/spec/integration/spec_helper.rb +5 -0
- data/spec/integration/transaction_test.rb +40 -0
- data/spec/integration/type_test.rb +7 -0
- data/spec/model/associations_spec.rb +463 -5
- data/spec/model/base_spec.rb +59 -0
- data/spec/model/eager_loading_spec.rb +269 -1
- data/spec/model/hooks_spec.rb +161 -0
- data/spec/model/record_spec.rb +30 -0
- data/spec/model/spec_helper.rb +5 -0
- metadata +29 -4
|
@@ -4,109 +4,111 @@
|
|
|
4
4
|
# of the dataset's abstract syntax tree.
|
|
5
5
|
|
|
6
6
|
module Sequel
|
|
7
|
-
class
|
|
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
|
-
#
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
47
|
-
i
|
|
50
|
+
dot "#{e.inspect}.lit"
|
|
48
51
|
when Symbol, Numeric, String, Class, TrueClass, FalseClass, NilClass
|
|
49
|
-
dot
|
|
50
|
-
i
|
|
52
|
+
dot e.inspect
|
|
51
53
|
when Array
|
|
52
|
-
dot
|
|
53
|
-
e.each_with_index do |
|
|
54
|
-
|
|
54
|
+
dot "Array"
|
|
55
|
+
e.each_with_index do |val, j|
|
|
56
|
+
v(val, j)
|
|
55
57
|
end
|
|
56
58
|
when Hash
|
|
57
|
-
dot
|
|
58
|
-
e.each do |k,
|
|
59
|
-
|
|
59
|
+
dot "Hash"
|
|
60
|
+
e.each do |k, val|
|
|
61
|
+
v(val, k)
|
|
60
62
|
end
|
|
61
63
|
when SQL::ComplexExpression
|
|
62
|
-
dot
|
|
63
|
-
e.args.each_with_index do |
|
|
64
|
-
|
|
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
|
|
68
|
-
|
|
69
|
+
dot "Identifier"
|
|
70
|
+
v(e.value, :value)
|
|
69
71
|
when SQL::QualifiedIdentifier
|
|
70
|
-
dot
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
dot "QualifiedIdentifier"
|
|
73
|
+
v(e.table, :table)
|
|
74
|
+
v(e.column, :column)
|
|
73
75
|
when SQL::OrderedExpression
|
|
74
|
-
dot
|
|
75
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
dot "AliasedExpression"
|
|
80
|
+
v(e.expression, :expression)
|
|
81
|
+
v(e.aliaz, :alias)
|
|
80
82
|
when SQL::CaseExpression
|
|
81
|
-
dot
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
dot "Cast"
|
|
89
|
+
v(e.expr, :expr)
|
|
90
|
+
v(e.type, :type)
|
|
89
91
|
when SQL::Function
|
|
90
|
-
dot
|
|
91
|
-
e.args.each_with_index do |
|
|
92
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
dot "Subscript"
|
|
98
|
+
v(e.f, :f)
|
|
99
|
+
v(e.sub, :sub)
|
|
98
100
|
when SQL::WindowFunction
|
|
99
|
-
dot
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
dot "WindowFunction"
|
|
102
|
+
v(e.function, :function)
|
|
103
|
+
v(e.window, :window)
|
|
102
104
|
when SQL::Window
|
|
103
|
-
dot
|
|
104
|
-
|
|
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
|
|
109
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
123
|
+
v(e.on, :on)
|
|
122
124
|
elsif e.is_a?(SQL::JoinUsingClause)
|
|
123
|
-
|
|
125
|
+
v(e.using, :using)
|
|
124
126
|
end
|
|
125
127
|
when Dataset
|
|
126
|
-
dot
|
|
128
|
+
dot "Dataset"
|
|
127
129
|
TO_DOT_OPTIONS.each do |k|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
if val = e.opts[k]
|
|
131
|
+
v(val, k.to_s)
|
|
132
|
+
end
|
|
130
133
|
end
|
|
131
134
|
else
|
|
132
|
-
dot
|
|
135
|
+
dot "Unhandled: #{e.inspect}"
|
|
133
136
|
end
|
|
134
|
-
|
|
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
|
|
533
|
-
# and the alias that was used for the current table
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
758
|
-
|
|
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
|
-
|
|
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 |
|
|
814
|
-
ds =
|
|
815
|
-
ds.graph(
|
|
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 |
|
|
880
|
-
ds
|
|
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 |
|
|
940
|
-
ds =
|
|
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][
|
|
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
|
-
|
|
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) ?
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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=>
|
|
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,
|
|
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)
|