sequel 3.0.0 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|