sequel 3.0.0 → 3.1.0

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