sequel 3.0.0 → 3.1.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 +100 -0
- data/README.rdoc +3 -3
- data/bin/sequel +102 -19
- data/doc/reflection.rdoc +83 -0
- data/doc/release_notes/3.1.0.txt +406 -0
- data/lib/sequel/adapters/ado.rb +11 -0
- data/lib/sequel/adapters/amalgalite.rb +5 -20
- data/lib/sequel/adapters/do.rb +44 -36
- data/lib/sequel/adapters/firebird.rb +29 -43
- data/lib/sequel/adapters/jdbc.rb +17 -27
- data/lib/sequel/adapters/mysql.rb +35 -40
- data/lib/sequel/adapters/odbc.rb +4 -23
- data/lib/sequel/adapters/oracle.rb +22 -19
- data/lib/sequel/adapters/postgres.rb +6 -15
- data/lib/sequel/adapters/shared/mssql.rb +1 -1
- data/lib/sequel/adapters/shared/mysql.rb +29 -10
- data/lib/sequel/adapters/shared/oracle.rb +6 -8
- data/lib/sequel/adapters/shared/postgres.rb +28 -72
- data/lib/sequel/adapters/shared/sqlite.rb +5 -3
- data/lib/sequel/adapters/sqlite.rb +5 -20
- data/lib/sequel/adapters/utils/savepoint_transactions.rb +80 -0
- data/lib/sequel/adapters/utils/unsupported.rb +0 -12
- data/lib/sequel/core.rb +12 -3
- data/lib/sequel/core_sql.rb +1 -8
- data/lib/sequel/database.rb +107 -43
- data/lib/sequel/database/schema_generator.rb +1 -0
- data/lib/sequel/database/schema_methods.rb +38 -4
- data/lib/sequel/dataset.rb +6 -0
- data/lib/sequel/dataset/convenience.rb +2 -2
- data/lib/sequel/dataset/graph.rb +2 -2
- data/lib/sequel/dataset/prepared_statements.rb +3 -8
- data/lib/sequel/dataset/sql.rb +93 -19
- data/lib/sequel/extensions/blank.rb +2 -1
- data/lib/sequel/extensions/inflector.rb +4 -3
- data/lib/sequel/extensions/migration.rb +13 -2
- data/lib/sequel/extensions/pagination.rb +4 -0
- data/lib/sequel/extensions/pretty_table.rb +4 -0
- data/lib/sequel/extensions/query.rb +4 -0
- data/lib/sequel/extensions/schema_dumper.rb +100 -24
- data/lib/sequel/extensions/string_date_time.rb +3 -4
- data/lib/sequel/model.rb +2 -1
- data/lib/sequel/model/associations.rb +96 -38
- data/lib/sequel/model/base.rb +14 -14
- data/lib/sequel/model/plugins.rb +32 -21
- data/lib/sequel/plugins/caching.rb +13 -15
- data/lib/sequel/plugins/identity_map.rb +107 -0
- data/lib/sequel/plugins/lazy_attributes.rb +65 -0
- data/lib/sequel/plugins/many_through_many.rb +188 -0
- data/lib/sequel/plugins/schema.rb +13 -0
- data/lib/sequel/plugins/serialization.rb +53 -37
- data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
- data/lib/sequel/plugins/tactical_eager_loading.rb +61 -0
- data/lib/sequel/plugins/validation_class_methods.rb +28 -7
- data/lib/sequel/plugins/validation_helpers.rb +31 -24
- data/lib/sequel/sql.rb +16 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/ado_spec.rb +47 -1
- data/spec/adapters/firebird_spec.rb +39 -36
- data/spec/adapters/mysql_spec.rb +25 -9
- data/spec/adapters/postgres_spec.rb +11 -24
- data/spec/core/database_spec.rb +54 -13
- data/spec/core/dataset_spec.rb +147 -29
- data/spec/core/object_graph_spec.rb +6 -1
- data/spec/core/schema_spec.rb +34 -0
- data/spec/core/spec_helper.rb +0 -2
- data/spec/extensions/caching_spec.rb +7 -0
- data/spec/extensions/identity_map_spec.rb +158 -0
- data/spec/extensions/lazy_attributes_spec.rb +113 -0
- data/spec/extensions/many_through_many_spec.rb +813 -0
- data/spec/extensions/migration_spec.rb +4 -4
- data/spec/extensions/schema_dumper_spec.rb +114 -13
- data/spec/extensions/schema_spec.rb +19 -3
- data/spec/extensions/serialization_spec.rb +28 -0
- data/spec/extensions/single_table_inheritance_spec.rb +25 -1
- data/spec/extensions/spec_helper.rb +2 -7
- data/spec/extensions/tactical_eager_loading_spec.rb +65 -0
- data/spec/extensions/validation_class_methods_spec.rb +10 -5
- data/spec/integration/dataset_test.rb +39 -6
- data/spec/integration/eager_loader_test.rb +7 -7
- data/spec/integration/spec_helper.rb +0 -1
- data/spec/integration/transaction_test.rb +28 -1
- data/spec/model/association_reflection_spec.rb +29 -3
- data/spec/model/associations_spec.rb +1 -0
- data/spec/model/eager_loading_spec.rb +70 -1
- data/spec/model/plugins_spec.rb +236 -50
- data/spec/model/spec_helper.rb +0 -2
- metadata +18 -5
@@ -1,17 +1,29 @@
|
|
1
|
+
# The schema_dumper extension supports dumping tables and indexes
|
2
|
+
# in a Sequel::Migration format, so they can be restored on another
|
3
|
+
# database (which can be the same type or a different type than
|
4
|
+
# the current database). The main interface is through
|
5
|
+
# Sequel::Database#dump_schema_migration.
|
6
|
+
|
1
7
|
module Sequel
|
2
8
|
class Database
|
9
|
+
POSTGRES_DEFAULT_RE = /\A(?:B?('.*')::[^']+|\((-?\d+(?:\.\d+)?)\))\z/
|
10
|
+
MYSQL_TIMESTAMP_RE = /\ACURRENT_(?:DATE|TIMESTAMP)?\z/
|
11
|
+
STRING_DEFAULT_RE = /\A'(.*)'\z/
|
12
|
+
|
3
13
|
# Dump indexes for all tables as a migration. This complements
|
4
|
-
# the :indexes=>false option to dump_schema_migration.
|
5
|
-
|
14
|
+
# the :indexes=>false option to dump_schema_migration. Options:
|
15
|
+
# * :same_db - Create a dump for the same database type, so
|
16
|
+
# don't ignore errors if the index statements fail.
|
17
|
+
def dump_indexes_migration(options={})
|
6
18
|
ts = tables
|
7
19
|
<<END_MIG
|
8
20
|
Class.new(Sequel::Migration) do
|
9
21
|
def up
|
10
|
-
#{ts.map{|t| dump_table_indexes(t, :add_index)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, ' ')}
|
22
|
+
#{ts.sort_by{|t| t.to_s}.map{|t| dump_table_indexes(t, :add_index, options)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, ' ')}
|
11
23
|
end
|
12
24
|
|
13
25
|
def down
|
14
|
-
#{ts.map{|t| dump_table_indexes(t, :drop_index)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, ' ')}
|
26
|
+
#{ts.sort_by{|t| t.to_s}.map{|t| dump_table_indexes(t, :drop_index, options)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, ' ')}
|
15
27
|
end
|
16
28
|
end
|
17
29
|
END_MIG
|
@@ -31,11 +43,11 @@ END_MIG
|
|
31
43
|
<<END_MIG
|
32
44
|
Class.new(Sequel::Migration) do
|
33
45
|
def up
|
34
|
-
#{ts.map{|t| dump_table_schema(t, options)}.join("\n\n").gsub(/^/o, ' ')}
|
46
|
+
#{ts.sort_by{|t| t.to_s}.map{|t| dump_table_schema(t, options)}.join("\n\n").gsub(/^/o, ' ')}
|
35
47
|
end
|
36
48
|
|
37
49
|
def down
|
38
|
-
drop_table(#{ts.inspect[1...-1]})
|
50
|
+
drop_table(#{ts.sort_by{|t| t.to_s}.inspect[1...-1]})
|
39
51
|
end
|
40
52
|
end
|
41
53
|
END_MIG
|
@@ -56,23 +68,70 @@ END_MIG
|
|
56
68
|
indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts))} if indexes
|
57
69
|
end
|
58
70
|
commands = [gen.dump_columns, gen.dump_constraints, gen.dump_indexes].reject{|x| x == ''}.join("\n\n")
|
59
|
-
"create_table(#{table.inspect}) do\n#{commands.gsub(/^/o, ' ')}\nend"
|
71
|
+
"create_table(#{table.inspect}#{', :ignore_index_errors=>true' if !options[:same_db] && options[:indexes] != false && indexes && !indexes.empty?}) do\n#{commands.gsub(/^/o, ' ')}\nend"
|
60
72
|
end
|
61
73
|
|
62
74
|
private
|
63
|
-
|
75
|
+
|
64
76
|
# Convert the given default, which should be a database specific string, into
|
65
|
-
# a ruby object.
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
77
|
+
# a ruby object.
|
78
|
+
def column_schema_to_ruby_default(default, type, options)
|
79
|
+
return if default.nil?
|
80
|
+
orig_default = default
|
81
|
+
if database_type == :postgres and m = POSTGRES_DEFAULT_RE.match(default)
|
82
|
+
default = m[1] || m[2]
|
83
|
+
end
|
84
|
+
if [:string, :blob, :date, :datetime, :time].include?(type)
|
85
|
+
if database_type == :mysql
|
86
|
+
if [:date, :datetime, :time].include?(type) && MYSQL_TIMESTAMP_RE.match(default)
|
87
|
+
return column_schema_to_ruby_default_fallback(default, options)
|
88
|
+
end
|
89
|
+
orig_default = default = "'#{default.gsub("'", "''").gsub('\\', '\\\\')}'"
|
90
|
+
end
|
91
|
+
if m = STRING_DEFAULT_RE.match(default)
|
92
|
+
default = m[1].gsub("''", "'")
|
93
|
+
else
|
94
|
+
return column_schema_to_ruby_default_fallback(default, options)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
res = begin
|
98
|
+
case type
|
99
|
+
when :boolean
|
100
|
+
case default
|
101
|
+
when /[f0]/i
|
102
|
+
false
|
103
|
+
when /[t1]/i
|
104
|
+
true
|
105
|
+
end
|
106
|
+
when :string
|
107
|
+
default
|
108
|
+
when :blob
|
109
|
+
Sequel::SQL::Blob.new(default)
|
110
|
+
when :integer
|
111
|
+
Integer(default)
|
112
|
+
when :float
|
113
|
+
Float(default)
|
114
|
+
when :date
|
115
|
+
Sequel.string_to_date(default)
|
116
|
+
when :datetime
|
117
|
+
DateTime.parse(default)
|
118
|
+
when :time
|
119
|
+
Sequel.string_to_time(default)
|
120
|
+
when :decimal
|
121
|
+
BigDecimal.new(default)
|
122
|
+
end
|
123
|
+
rescue
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
res.nil? ? column_schema_to_ruby_default_fallback(orig_default, options) : res
|
127
|
+
end
|
128
|
+
|
129
|
+
# If the database default can't be converted, return the string with the inspect
|
130
|
+
# method modified so that .lit is always appended after it, only if the
|
131
|
+
# :same_db option is used.
|
132
|
+
def column_schema_to_ruby_default_fallback(default, options)
|
133
|
+
if options[:same_db]
|
134
|
+
default = default.to_s
|
76
135
|
def default.inspect
|
77
136
|
"#{super}.lit"
|
78
137
|
end
|
@@ -89,7 +148,8 @@ END_MIG
|
|
89
148
|
col_opts = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema)
|
90
149
|
type = col_opts.delete(:type)
|
91
150
|
col_opts.delete(:size) if col_opts[:size].nil?
|
92
|
-
|
151
|
+
default = column_schema_to_ruby_default(schema[:default], schema[:type], options) if schema[:default]
|
152
|
+
col_opts[:default] = default unless default.nil?
|
93
153
|
col_opts[:null] = false if schema[:allow_null] == false
|
94
154
|
[:column, name, type, col_opts]
|
95
155
|
end
|
@@ -141,14 +201,14 @@ END_MIG
|
|
141
201
|
|
142
202
|
# Return a string that containing add_index/drop_index method calls for
|
143
203
|
# creating the index migration.
|
144
|
-
def dump_table_indexes(table, meth)
|
204
|
+
def dump_table_indexes(table, meth, options={})
|
145
205
|
return '' unless respond_to?(:indexes)
|
146
206
|
im = method(:index_to_generator_opts)
|
147
207
|
indexes = indexes(table).sort_by{|k,v| k.to_s}
|
148
208
|
gen = Schema::Generator.new(self) do
|
149
209
|
indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts))}
|
150
210
|
end
|
151
|
-
gen.dump_indexes(meth=>table)
|
211
|
+
gen.dump_indexes(meth=>table, :ignore_errors=>!options[:same_db])
|
152
212
|
end
|
153
213
|
|
154
214
|
# Convert the parsed index information into options to the Generators index method.
|
@@ -216,12 +276,13 @@ END_MIG
|
|
216
276
|
# can be called outside of a generator but inside a migration.
|
217
277
|
# The value of this option should be the table name to use.
|
218
278
|
# * :drop_index - Same as add_index, but create drop_index statements.
|
279
|
+
# * :ignore_errors - Add the ignore_errors option to the outputted indexes
|
219
280
|
def dump_indexes(options={})
|
220
281
|
indexes.map do |c|
|
221
282
|
c = c.dup
|
222
283
|
cols = c.delete(:columns)
|
223
284
|
if table = options[:add_index] || options[:drop_index]
|
224
|
-
"#{options[:drop_index] ? 'drop' : 'add'}_index #{table.inspect}, #{cols.inspect}#{opts_inspect(c)}"
|
285
|
+
"#{options[:drop_index] ? 'drop' : 'add'}_index #{table.inspect}, #{cols.inspect}#{', :ignore_errors=>true' if options[:ignore_errors]}#{opts_inspect(c)}"
|
225
286
|
else
|
226
287
|
"index #{cols.inspect}#{opts_inspect(c)}"
|
227
288
|
end
|
@@ -231,7 +292,22 @@ END_MIG
|
|
231
292
|
private
|
232
293
|
|
233
294
|
def opts_inspect(opts)
|
234
|
-
|
295
|
+
if opts[:default]
|
296
|
+
opts = opts.dup
|
297
|
+
de = case d = opts.delete(:default)
|
298
|
+
when BigDecimal, Sequel::SQL::Blob
|
299
|
+
"#{d.class.name}.new(#{d.to_s.inspect})"
|
300
|
+
when DateTime, Date
|
301
|
+
"#{d.class.name}.parse(#{d.to_s.inspect})"
|
302
|
+
when Time
|
303
|
+
"#{d.class.name}.parse(#{d.strftime('%H:%M:%S').inspect})"
|
304
|
+
else
|
305
|
+
d.inspect
|
306
|
+
end
|
307
|
+
", :default=>#{de}#{", #{opts.inspect[1...-1]}" if opts.length > 0}"
|
308
|
+
else
|
309
|
+
", #{opts.inspect[1...-1]}" if opts.length > 0
|
310
|
+
end
|
235
311
|
end
|
236
312
|
end
|
237
313
|
end
|
@@ -1,7 +1,6 @@
|
|
1
|
-
#
|
2
|
-
#
|
3
|
-
#
|
4
|
-
# to handle the internal conversions.
|
1
|
+
# The string_date_time extension provides String instance methods
|
2
|
+
# for converting the strings to a date (e.g. String#to_date), allowing
|
3
|
+
# for backwards compatibility with legacy Sequel code.
|
5
4
|
|
6
5
|
class String
|
7
6
|
# Converts a string into a Date object.
|
data/lib/sequel/model.rb
CHANGED
@@ -67,7 +67,7 @@ module Sequel
|
|
67
67
|
:@raise_on_save_failure=>nil, :@restricted_columns=>:dup, :@restrict_primary_key=>nil,
|
68
68
|
:@simple_pk=>nil, :@simple_table=>nil, :@strict_param_setting=>nil,
|
69
69
|
:@typecast_empty_string_to_nil=>nil, :@typecast_on_assignment=>nil,
|
70
|
-
:@raise_on_typecast_failure=>nil}
|
70
|
+
:@raise_on_typecast_failure=>nil, :@plugins=>:dup}
|
71
71
|
|
72
72
|
# Regexp that determines if a method name is normal in the sense that
|
73
73
|
# it could be called directly in ruby code without using send. Used to
|
@@ -88,6 +88,7 @@ module Sequel
|
|
88
88
|
@dataset_method_modules = []
|
89
89
|
@dataset_methods = {}
|
90
90
|
@overridable_methods_module = nil
|
91
|
+
@plugins = []
|
91
92
|
@primary_key = :id
|
92
93
|
@raise_on_save_failure = true
|
93
94
|
@raise_on_typecast_failure = true
|
@@ -57,6 +57,7 @@ module Sequel
|
|
57
57
|
self[:class] ||= constantize(self[:class_name])
|
58
58
|
end
|
59
59
|
|
60
|
+
|
60
61
|
# Name symbol for the dataset association method
|
61
62
|
def dataset_method
|
62
63
|
:"#{self[:name]}_dataset"
|
@@ -72,6 +73,12 @@ module Sequel
|
|
72
73
|
true
|
73
74
|
end
|
74
75
|
|
76
|
+
# By default associations do not need to select a key in an associated table
|
77
|
+
# to eagerly load.
|
78
|
+
def eager_loading_use_associated_key?
|
79
|
+
false
|
80
|
+
end
|
81
|
+
|
75
82
|
# Whether to eagerly graph a lazy dataset, true by default. If this
|
76
83
|
# is false, the association won't respect the :eager_graph option
|
77
84
|
# when loading the association for a single record.
|
@@ -96,7 +103,7 @@ module Sequel
|
|
96
103
|
r_type = reciprocal_type
|
97
104
|
key = self[:key]
|
98
105
|
associated_class.all_association_reflections.each do |assoc_reflect|
|
99
|
-
if assoc_reflect[:type] == r_type && assoc_reflect[:key] == key
|
106
|
+
if assoc_reflect[:type] == r_type && assoc_reflect[:key] == key && assoc_reflect.associated_class == self[:model]
|
100
107
|
return self[:reciprocal] = assoc_reflect[:name]
|
101
108
|
end
|
102
109
|
end
|
@@ -223,11 +230,31 @@ module Sequel
|
|
223
230
|
class ManyToManyAssociationReflection < AssociationReflection
|
224
231
|
ASSOCIATION_TYPES[:many_to_many] = self
|
225
232
|
|
233
|
+
# The alias to use for the associated key when eagerly loading
|
234
|
+
def associated_key_alias
|
235
|
+
self[:left_key_alias]
|
236
|
+
end
|
237
|
+
|
238
|
+
# The column to use for the associated key when eagerly loading
|
239
|
+
def associated_key_column
|
240
|
+
self[:left_key]
|
241
|
+
end
|
242
|
+
|
243
|
+
# The table containing the column to use for the associated key when eagerly loading
|
244
|
+
def associated_key_table
|
245
|
+
self[:join_table]
|
246
|
+
end
|
247
|
+
|
248
|
+
# The default associated key alias
|
249
|
+
def default_associated_key_alias
|
250
|
+
:x_foreign_key_x
|
251
|
+
end
|
252
|
+
|
226
253
|
# Default name symbol for the join table.
|
227
254
|
def default_join_table
|
228
255
|
[self[:class_name], self[:model].name].map{|i| underscore(pluralize(demodulize(i)))}.sort.join('_').to_sym
|
229
256
|
end
|
230
|
-
|
257
|
+
|
231
258
|
# Default foreign key name symbol for key in join table that points to
|
232
259
|
# current table's primary key (or :left_primary_key column).
|
233
260
|
def default_left_key
|
@@ -245,6 +272,11 @@ module Sequel
|
|
245
272
|
self[:left_primary_key]
|
246
273
|
end
|
247
274
|
|
275
|
+
# many_to_many associations need to select a key in an associated table to eagerly load
|
276
|
+
def eager_loading_use_associated_key?
|
277
|
+
true
|
278
|
+
end
|
279
|
+
|
248
280
|
# Whether the associated object needs a primary key to be added/removed,
|
249
281
|
# true for many_to_many associations.
|
250
282
|
def need_associated_primary_key?
|
@@ -258,8 +290,9 @@ module Sequel
|
|
258
290
|
right_key = self[:right_key]
|
259
291
|
join_table = self[:join_table]
|
260
292
|
associated_class.all_association_reflections.each do |assoc_reflect|
|
261
|
-
if assoc_reflect[:type] == :many_to_many && assoc_reflect[:left_key] == right_key
|
262
|
-
|
293
|
+
if assoc_reflect[:type] == :many_to_many && assoc_reflect[:left_key] == right_key &&
|
294
|
+
assoc_reflect[:right_key] == left_key && assoc_reflect[:join_table] == join_table &&
|
295
|
+
assoc_reflect.associated_class == self[:model]
|
263
296
|
return self[:reciprocal] = assoc_reflect[:name]
|
264
297
|
end
|
265
298
|
end
|
@@ -376,6 +409,9 @@ module Sequel
|
|
376
409
|
# before a new item is added to the association.
|
377
410
|
# - :before_remove - Symbol, Proc, or array of both/either specifying a callback to call
|
378
411
|
# before an item is removed from the association.
|
412
|
+
# - :cartesian_product_number - he number of joins completed by this association that could cause more
|
413
|
+
# than one row for each row in the current table (default: 0 for many_to_one associations,
|
414
|
+
# 1 for *_to_many associations).
|
379
415
|
# - :class - The associated class or its name. If not
|
380
416
|
# given, uses the association's name, which is camelized (and
|
381
417
|
# singularized unless the type is :many_to_one)
|
@@ -532,13 +568,20 @@ module Sequel
|
|
532
568
|
association_reflections.keys
|
533
569
|
end
|
534
570
|
|
535
|
-
# Modify and return eager loading dataset based on association options
|
571
|
+
# Modify and return eager loading dataset based on association options. Options:
|
536
572
|
def eager_loading_dataset(opts, ds, select, associations)
|
537
573
|
ds = ds.select(*select) if select
|
538
|
-
|
574
|
+
if c = opts[:conditions]
|
575
|
+
ds = (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.filter(*c) : ds.filter(c)
|
576
|
+
end
|
539
577
|
ds = ds.order(*opts[:order]) if opts[:order]
|
540
578
|
ds = ds.eager(opts[:eager]) if opts[:eager]
|
541
|
-
|
579
|
+
if opts[:eager_graph]
|
580
|
+
ds = ds.eager_graph(opts[:eager_graph])
|
581
|
+
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?
|
582
|
+
else
|
583
|
+
ds.select_more(SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(opts.associated_key_table, opts.associated_key_column), opts.associated_key_alias)) if opts.eager_loading_use_associated_key?
|
584
|
+
end
|
542
585
|
ds = ds.eager(associations) unless Array(associations).empty?
|
543
586
|
ds = opts[:eager_block].call(ds) if opts[:eager_block]
|
544
587
|
ds
|
@@ -604,9 +647,9 @@ module Sequel
|
|
604
647
|
right = (opts[:right_key] ||= opts.default_right_key)
|
605
648
|
left_pk = (opts[:left_primary_key] ||= self.primary_key)
|
606
649
|
opts[:class_name] ||= camelize(singularize(name))
|
650
|
+
opts[:cartesian_product_number] ||= 1
|
607
651
|
join_table = (opts[:join_table] ||= opts.default_join_table)
|
608
|
-
left_key_alias = opts[:left_key_alias] ||=
|
609
|
-
left_key_select = opts[:left_key_select] ||= SQL::QualifiedIdentifier.new(join_table, left).as(opts[:left_key_alias])
|
652
|
+
left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
|
610
653
|
graph_jt_conds = opts[:graph_join_table_conditions] = opts[:graph_join_table_conditions] ? opts[:graph_join_table_conditions].to_a : []
|
611
654
|
opts[:graph_join_table_join_type] ||= opts[:graph_join_type]
|
612
655
|
opts[:after_load].unshift(:array_uniq!) if opts[:uniq]
|
@@ -616,7 +659,7 @@ module Sequel
|
|
616
659
|
opts[:eager_loader] ||= proc do |key_hash, records, associations|
|
617
660
|
h = key_hash[left_pk]
|
618
661
|
records.each{|object| object.associations[name] = []}
|
619
|
-
model.eager_loading_dataset(opts, opts.associated_class.inner_join(join_table, [[right, opts.right_primary_key], [left, h.keys]]), Array(opts.select)
|
662
|
+
model.eager_loading_dataset(opts, opts.associated_class.inner_join(join_table, [[right, opts.right_primary_key], [left, h.keys]]), Array(opts.select), associations).all do |assoc_record|
|
620
663
|
next unless objects = h[assoc_record.values.delete(left_key_alias)]
|
621
664
|
objects.each{|object| object.associations[name].push(assoc_record)}
|
622
665
|
end
|
@@ -661,6 +704,7 @@ module Sequel
|
|
661
704
|
model = self
|
662
705
|
opts[:key] = opts.default_key unless opts.include?(:key)
|
663
706
|
key = opts[:key]
|
707
|
+
opts[:cartesian_product_number] ||= 0
|
664
708
|
opts[:class_name] ||= camelize(name)
|
665
709
|
opts[:dataset] ||= proc do
|
666
710
|
klass = opts.associated_class
|
@@ -730,6 +774,7 @@ module Sequel
|
|
730
774
|
use_only_conditions = opts.include?(:graph_only_conditions)
|
731
775
|
only_conditions = opts[:graph_only_conditions]
|
732
776
|
conditions = opts[:graph_conditions]
|
777
|
+
opts[:cartesian_product_number] ||= 1
|
733
778
|
graph_block = opts[:graph_block]
|
734
779
|
opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
|
735
780
|
ds = ds.graph(opts.associated_class, use_only_conditions ? only_conditions : [[key, primary_key]] + conditions, :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, :implicit_qualifier=>table_alias, &graph_block)
|
@@ -790,6 +835,13 @@ module Sequel
|
|
790
835
|
|
791
836
|
# Private instance methods used to implement the associations support.
|
792
837
|
module InstanceMethods
|
838
|
+
# Used internally by the associations code, like pk but doesn't raise
|
839
|
+
# an Error if the model has no primary key.
|
840
|
+
def pk_or_nil
|
841
|
+
key = primary_key
|
842
|
+
key.is_a?(Array) ? key.map{|k| @values[k]} : @values[key]
|
843
|
+
end
|
844
|
+
|
793
845
|
private
|
794
846
|
|
795
847
|
# Backbone behind association dataset methods
|
@@ -801,7 +853,9 @@ module Sequel
|
|
801
853
|
ds.association_reflection = opts
|
802
854
|
opts[:extend].each{|m| ds.extend(m)}
|
803
855
|
ds = ds.select(*opts.select) if opts.select
|
804
|
-
|
856
|
+
if c = opts[:conditions]
|
857
|
+
ds = (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.filter(*c) : ds.filter(c)
|
858
|
+
end
|
805
859
|
ds = ds.order(*opts[:order]) if opts[:order]
|
806
860
|
ds = ds.limit(*opts[:limit]) if opts[:limit]
|
807
861
|
ds = ds.eager(*opts[:eager]) if opts[:eager]
|
@@ -810,6 +864,19 @@ module Sequel
|
|
810
864
|
ds
|
811
865
|
end
|
812
866
|
|
867
|
+
# Return the associated objects from the dataset, without callbacks, reciprocals, and caching.
|
868
|
+
def _load_associated_objects(opts)
|
869
|
+
if opts.returns_array?
|
870
|
+
send(opts.dataset_method).all
|
871
|
+
else
|
872
|
+
if !opts[:key]
|
873
|
+
send(opts.dataset_method).all.first
|
874
|
+
elsif send(opts[:key])
|
875
|
+
send(opts.dataset_method).first
|
876
|
+
end
|
877
|
+
end
|
878
|
+
end
|
879
|
+
|
813
880
|
# Add the given associated object to the given association
|
814
881
|
def add_associated_object(opts, o)
|
815
882
|
raise(Sequel::Error, "model object #{model} does not have a primary key") unless pk
|
@@ -840,21 +907,13 @@ module Sequel
|
|
840
907
|
a.uniq!
|
841
908
|
end
|
842
909
|
|
843
|
-
# Load the associated objects using the dataset
|
910
|
+
# Load the associated objects using the dataset, handling callbacks, reciprocals, and caching.
|
844
911
|
def load_associated_objects(opts, reload=false)
|
845
912
|
name = opts[:name]
|
846
913
|
if associations.include?(name) and !reload
|
847
914
|
associations[name]
|
848
915
|
else
|
849
|
-
objs =
|
850
|
-
send(opts.dataset_method).all
|
851
|
-
else
|
852
|
-
if !opts[:key]
|
853
|
-
send(opts.dataset_method).all.first
|
854
|
-
elsif send(opts[:key])
|
855
|
-
send(opts.dataset_method).first
|
856
|
-
end
|
857
|
-
end
|
916
|
+
objs = _load_associated_objects(opts)
|
858
917
|
run_association_callbacks(opts, :after_load, objs)
|
859
918
|
objs.each{|o| add_reciprocal_object(opts, o)} if opts.set_reciprocal_to_self?
|
860
919
|
associations[name] = objs
|
@@ -1040,7 +1099,7 @@ module Sequel
|
|
1040
1099
|
# :requirements - array of requirements for this association
|
1041
1100
|
# :alias_association_type_map - the type of association for this association
|
1042
1101
|
# :alias_association_name_map - the name of the association for this association
|
1043
|
-
clone(:eager_graph=>{:requirements=>{}, :master=>model.table_name, :alias_association_type_map=>{}, :alias_association_name_map=>{}, :reciprocals=>{}})
|
1102
|
+
clone(:eager_graph=>{:requirements=>{}, :master=>model.table_name, :alias_association_type_map=>{}, :alias_association_name_map=>{}, :reciprocals=>{}, :cartesian_product_number=>0})
|
1044
1103
|
end
|
1045
1104
|
ds.eager_graph_associations(ds, model, table_name, [], *associations)
|
1046
1105
|
end
|
@@ -1069,6 +1128,7 @@ module Sequel
|
|
1069
1128
|
eager_graph[:requirements][assoc_table_alias] = requirements.dup
|
1070
1129
|
eager_graph[:alias_association_name_map][assoc_table_alias] = assoc_name
|
1071
1130
|
eager_graph[:alias_association_type_map][assoc_table_alias] = r.returns_array?
|
1131
|
+
eager_graph[:cartesian_product_number] += r[:cartesian_product_number] || 2
|
1072
1132
|
ds = ds.eager_graph_associations(ds, r.associated_class, assoc_table_alias, requirements + [assoc_table_alias], *associations) unless associations.empty?
|
1073
1133
|
ds
|
1074
1134
|
end
|
@@ -1143,7 +1203,7 @@ module Sequel
|
|
1143
1203
|
records = []
|
1144
1204
|
record_graphs.each do |record_graph|
|
1145
1205
|
primary_record = record_graph[master]
|
1146
|
-
key = primary_record.
|
1206
|
+
key = primary_record.pk_or_nil || primary_record.values.sort_by{|x| x[0].to_s}
|
1147
1207
|
if cached_pr = records_map[master][key]
|
1148
1208
|
primary_record = cached_pr
|
1149
1209
|
else
|
@@ -1156,7 +1216,7 @@ module Sequel
|
|
1156
1216
|
end
|
1157
1217
|
|
1158
1218
|
# Remove duplicate records from all associations if this graph could possibly be a cartesian product
|
1159
|
-
eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) if
|
1219
|
+
eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) if eager_graph[:cartesian_product_number] > 1
|
1160
1220
|
|
1161
1221
|
# Replace the array of object graphs with an array of model objects
|
1162
1222
|
record_graphs.replace(records)
|
@@ -1167,6 +1227,7 @@ module Sequel
|
|
1167
1227
|
# N (starting at 0 and increasing until an unused one is found).
|
1168
1228
|
def eager_unique_table_alias(ds, table_alias)
|
1169
1229
|
used_aliases = ds.opts[:from]
|
1230
|
+
used_aliases += ds.opts[:join].map{|j| j.table_alias || j.table} if ds.opts[:join]
|
1170
1231
|
graph = ds.opts[:graph]
|
1171
1232
|
used_aliases += graph[:table_aliases].keys if graph
|
1172
1233
|
if used_aliases.include?(table_alias)
|
@@ -1200,21 +1261,20 @@ module Sequel
|
|
1200
1261
|
end
|
1201
1262
|
dependency_map.each do |ta, deps|
|
1202
1263
|
next unless rec = record_graph[ta]
|
1203
|
-
key = rec.
|
1264
|
+
key = rec.pk_or_nil || rec.values.sort_by{|x| x[0].to_s}
|
1204
1265
|
if cached_rec = records_map[ta][key]
|
1205
1266
|
rec = cached_rec
|
1206
1267
|
else
|
1207
|
-
records_map[ta][
|
1268
|
+
records_map[ta][key] = rec
|
1208
1269
|
end
|
1209
1270
|
assoc_name = alias_map[ta]
|
1210
|
-
|
1211
|
-
when false
|
1212
|
-
current.associations[assoc_name] = rec
|
1213
|
-
else
|
1271
|
+
if type_map[ta]
|
1214
1272
|
current.associations[assoc_name].push(rec)
|
1215
1273
|
if reciprocal = reciprocal_map[ta]
|
1216
1274
|
rec.associations[reciprocal] = current
|
1217
1275
|
end
|
1276
|
+
else
|
1277
|
+
current.associations[assoc_name] = rec
|
1218
1278
|
end
|
1219
1279
|
# Recurse into dependencies of the current object
|
1220
1280
|
eager_graph_build_associations_graph(deps, alias_map, type_map, reciprocal_map, records_map, rec, record_graph)
|
@@ -1229,12 +1289,11 @@ module Sequel
|
|
1229
1289
|
def eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map)
|
1230
1290
|
records.each do |record|
|
1231
1291
|
dependency_map.each do |ta, deps|
|
1232
|
-
list =
|
1233
|
-
|
1234
|
-
[item] if item
|
1235
|
-
else
|
1236
|
-
list = record.send(alias_map[ta])
|
1292
|
+
list = record.send(alias_map[ta])
|
1293
|
+
list = if type_map[ta]
|
1237
1294
|
list.uniq!
|
1295
|
+
else
|
1296
|
+
[list] if list
|
1238
1297
|
end
|
1239
1298
|
# Recurse into dependencies
|
1240
1299
|
eager_graph_make_associations_unique(list, deps, alias_map, type_map) if list
|
@@ -1260,10 +1319,8 @@ module Sequel
|
|
1260
1319
|
end
|
1261
1320
|
|
1262
1321
|
# Eagerly load all specified associations
|
1263
|
-
def eager_load(a)
|
1322
|
+
def eager_load(a, eager_assoc=@opts[:eager])
|
1264
1323
|
return if a.empty?
|
1265
|
-
# All associations to eager load
|
1266
|
-
eager_assoc = @opts[:eager]
|
1267
1324
|
# Key is foreign/primary key name symbol
|
1268
1325
|
# Value is hash with keys being foreign/primary key values (generally integers)
|
1269
1326
|
# and values being an array of current model objects with that
|
@@ -1293,6 +1350,7 @@ module Sequel
|
|
1293
1350
|
def post_load(all_records)
|
1294
1351
|
eager_graph_build_associations(all_records) if @opts[:eager_graph]
|
1295
1352
|
eager_load(all_records) if @opts[:eager]
|
1353
|
+
super
|
1296
1354
|
end
|
1297
1355
|
end
|
1298
1356
|
end
|