sequel 3.27.0 → 3.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|