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.
Files changed (87) hide show
  1. data/CHANGELOG +100 -0
  2. data/README.rdoc +3 -3
  3. data/bin/sequel +102 -19
  4. data/doc/reflection.rdoc +83 -0
  5. data/doc/release_notes/3.1.0.txt +406 -0
  6. data/lib/sequel/adapters/ado.rb +11 -0
  7. data/lib/sequel/adapters/amalgalite.rb +5 -20
  8. data/lib/sequel/adapters/do.rb +44 -36
  9. data/lib/sequel/adapters/firebird.rb +29 -43
  10. data/lib/sequel/adapters/jdbc.rb +17 -27
  11. data/lib/sequel/adapters/mysql.rb +35 -40
  12. data/lib/sequel/adapters/odbc.rb +4 -23
  13. data/lib/sequel/adapters/oracle.rb +22 -19
  14. data/lib/sequel/adapters/postgres.rb +6 -15
  15. data/lib/sequel/adapters/shared/mssql.rb +1 -1
  16. data/lib/sequel/adapters/shared/mysql.rb +29 -10
  17. data/lib/sequel/adapters/shared/oracle.rb +6 -8
  18. data/lib/sequel/adapters/shared/postgres.rb +28 -72
  19. data/lib/sequel/adapters/shared/sqlite.rb +5 -3
  20. data/lib/sequel/adapters/sqlite.rb +5 -20
  21. data/lib/sequel/adapters/utils/savepoint_transactions.rb +80 -0
  22. data/lib/sequel/adapters/utils/unsupported.rb +0 -12
  23. data/lib/sequel/core.rb +12 -3
  24. data/lib/sequel/core_sql.rb +1 -8
  25. data/lib/sequel/database.rb +107 -43
  26. data/lib/sequel/database/schema_generator.rb +1 -0
  27. data/lib/sequel/database/schema_methods.rb +38 -4
  28. data/lib/sequel/dataset.rb +6 -0
  29. data/lib/sequel/dataset/convenience.rb +2 -2
  30. data/lib/sequel/dataset/graph.rb +2 -2
  31. data/lib/sequel/dataset/prepared_statements.rb +3 -8
  32. data/lib/sequel/dataset/sql.rb +93 -19
  33. data/lib/sequel/extensions/blank.rb +2 -1
  34. data/lib/sequel/extensions/inflector.rb +4 -3
  35. data/lib/sequel/extensions/migration.rb +13 -2
  36. data/lib/sequel/extensions/pagination.rb +4 -0
  37. data/lib/sequel/extensions/pretty_table.rb +4 -0
  38. data/lib/sequel/extensions/query.rb +4 -0
  39. data/lib/sequel/extensions/schema_dumper.rb +100 -24
  40. data/lib/sequel/extensions/string_date_time.rb +3 -4
  41. data/lib/sequel/model.rb +2 -1
  42. data/lib/sequel/model/associations.rb +96 -38
  43. data/lib/sequel/model/base.rb +14 -14
  44. data/lib/sequel/model/plugins.rb +32 -21
  45. data/lib/sequel/plugins/caching.rb +13 -15
  46. data/lib/sequel/plugins/identity_map.rb +107 -0
  47. data/lib/sequel/plugins/lazy_attributes.rb +65 -0
  48. data/lib/sequel/plugins/many_through_many.rb +188 -0
  49. data/lib/sequel/plugins/schema.rb +13 -0
  50. data/lib/sequel/plugins/serialization.rb +53 -37
  51. data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
  52. data/lib/sequel/plugins/tactical_eager_loading.rb +61 -0
  53. data/lib/sequel/plugins/validation_class_methods.rb +28 -7
  54. data/lib/sequel/plugins/validation_helpers.rb +31 -24
  55. data/lib/sequel/sql.rb +16 -0
  56. data/lib/sequel/version.rb +1 -1
  57. data/spec/adapters/ado_spec.rb +47 -1
  58. data/spec/adapters/firebird_spec.rb +39 -36
  59. data/spec/adapters/mysql_spec.rb +25 -9
  60. data/spec/adapters/postgres_spec.rb +11 -24
  61. data/spec/core/database_spec.rb +54 -13
  62. data/spec/core/dataset_spec.rb +147 -29
  63. data/spec/core/object_graph_spec.rb +6 -1
  64. data/spec/core/schema_spec.rb +34 -0
  65. data/spec/core/spec_helper.rb +0 -2
  66. data/spec/extensions/caching_spec.rb +7 -0
  67. data/spec/extensions/identity_map_spec.rb +158 -0
  68. data/spec/extensions/lazy_attributes_spec.rb +113 -0
  69. data/spec/extensions/many_through_many_spec.rb +813 -0
  70. data/spec/extensions/migration_spec.rb +4 -4
  71. data/spec/extensions/schema_dumper_spec.rb +114 -13
  72. data/spec/extensions/schema_spec.rb +19 -3
  73. data/spec/extensions/serialization_spec.rb +28 -0
  74. data/spec/extensions/single_table_inheritance_spec.rb +25 -1
  75. data/spec/extensions/spec_helper.rb +2 -7
  76. data/spec/extensions/tactical_eager_loading_spec.rb +65 -0
  77. data/spec/extensions/validation_class_methods_spec.rb +10 -5
  78. data/spec/integration/dataset_test.rb +39 -6
  79. data/spec/integration/eager_loader_test.rb +7 -7
  80. data/spec/integration/spec_helper.rb +0 -1
  81. data/spec/integration/transaction_test.rb +28 -1
  82. data/spec/model/association_reflection_spec.rb +29 -3
  83. data/spec/model/associations_spec.rb +1 -0
  84. data/spec/model/eager_loading_spec.rb +70 -1
  85. data/spec/model/plugins_spec.rb +236 -50
  86. data/spec/model/spec_helper.rb +0 -2
  87. 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
- def dump_indexes_migration
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. If it can't be converted, return the string with the inspect
66
- # method modified so that .lit is always appended after it.
67
- def column_schema_to_ruby_default(default, type)
68
- case default
69
- when /false/
70
- false
71
- when 'true'
72
- true
73
- when /\A\d+\z/
74
- default.to_i
75
- else
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
- col_opts[:default] = column_schema_to_ruby_default(schema[:default], type) if schema[:default]
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
- ", #{opts.inspect[1...-1]}" if opts.length > 0
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
- # This file contains the previous extensions to String for date/time
2
- # conversions. These are provided mainly for backward compatibility,
3
- # Sequel now uses a module level method instead of extending string
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
- && assoc_reflect[:right_key] == left_key && assoc_reflect[:join_table] == join_table
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
- ds = ds.filter(opts[:conditions]) if opts[:conditions]
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
- ds = ds.eager_graph(opts[:eager_graph]) if opts[:eager_graph]
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] ||= :x_foreign_key_x
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) + Array(left_key_select), associations).all do |assoc_record|
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
- ds = ds.filter(opts[:conditions]) if opts[:conditions]
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 = if opts.returns_array?
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.pk || primary_record.values.sort_by{|x| x[0].to_s}
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 type_map.values.select{|v| v}.length > 1
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.pk || rec.values.sort_by{|x| x[0].to_s}
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][rec.pk] = rec
1268
+ records_map[ta][key] = rec
1208
1269
  end
1209
1270
  assoc_name = alias_map[ta]
1210
- case type_map[ta]
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 = if !type_map[ta]
1233
- item = record.send(alias_map[ta])
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