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