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
@@ -86,12 +86,7 @@ module Sequel
86
86
  # the given argument(s).
87
87
  def [](*args)
88
88
  args = args.first if (args.size == 1)
89
- return dataset[args] if args.is_a?(Hash)
90
- if t = simple_table and p = simple_pk
91
- with_sql("SELECT * FROM #{t} WHERE #{p} = #{dataset.literal(args)}").first
92
- else
93
- dataset[primary_key_hash(args)]
94
- end
89
+ args.is_a?(Hash) ? dataset[args] : primary_key_lookup(args)
95
90
  end
96
91
 
97
92
  # Returns the columns in the result set in their original order.
@@ -380,8 +375,8 @@ module Sequel
380
375
  im = instance_methods.collect{|x| x.to_s}
381
376
  columns.each do |column|
382
377
  meth = "#{column}="
383
- overridable_methods_module.module_eval("def #{column}; self[:#{column}] end") unless im.include?(column.to_s)
384
- overridable_methods_module.module_eval("def #{meth}(v); self[:#{column}] = v end") unless im.include?(meth)
378
+ overridable_methods_module.module_eval("def #{column}; self[:#{column}] end", __FILE__, __LINE__) unless im.include?(column.to_s)
379
+ overridable_methods_module.module_eval("def #{meth}(v); self[:#{column}] = v end", __FILE__, __LINE__) unless im.include?(meth)
385
380
  end
386
381
  end
387
382
 
@@ -432,6 +427,16 @@ module Sequel
432
427
  include(@overridable_methods_module = Module.new) unless @overridable_methods_module
433
428
  @overridable_methods_module
434
429
  end
430
+
431
+ # Find the row in the dataset that matches the primary key. Uses
432
+ # an static SQL optimization if the table and primary key are simple.
433
+ def primary_key_lookup(pk)
434
+ if t = simple_table and p = simple_pk
435
+ with_sql("SELECT * FROM #{t} WHERE #{p} = #{dataset.literal(pk)}").first
436
+ else
437
+ dataset[primary_key_hash(pk)]
438
+ end
439
+ end
435
440
 
436
441
  # Set the columns for this model and create accessor methods for each column.
437
442
  def set_columns(new_columns)
@@ -634,12 +639,7 @@ module Sequel
634
639
  # If the model has a composite primary key, returns an array of values.
635
640
  def pk
636
641
  raise(Error, "No primary key is associated with this model") unless key = primary_key
637
- case key
638
- when Array
639
- key.collect{|k| @values[k]}
640
- else
641
- @values[key]
642
- end
642
+ key.is_a?(Array) ? key.map{|k| @values[k]} : @values[key]
643
643
  end
644
644
 
645
645
  # Returns a hash identifying the model instance. It should be true that:
@@ -3,14 +3,19 @@ module Sequel
3
3
  # so they can be loaded via Model.plugin.
4
4
  #
5
5
  # Plugins should be modules with one of the following conditions:
6
- # * A singleton method named apply, which takes a model and
7
- # additional arguments.
6
+ # * A singleton method named apply, which takes a model,
7
+ # additional arguments, and an optional block. This is called
8
+ # once, the first time the plugin is loaded, with the arguments
9
+ # and block provide to the call to Model.plugin.
8
10
  # * A module inside the plugin module named InstanceMethods,
9
11
  # which will be included in the model class.
10
12
  # * A module inside the plugin module named ClassMethods,
11
13
  # which will extend the model class.
12
14
  # * A module inside the plugin module named DatasetMethods,
13
15
  # which will extend the model's dataset.
16
+ # * A singleton method named configure, which takes a model,
17
+ # additional arguments, and an optional block. This is called
18
+ # every time the Model.plugin method is called.
14
19
  module Plugins
15
20
  end
16
21
 
@@ -20,32 +25,38 @@ module Sequel
20
25
  # require the plugin from either sequel/plugins/#{plugin} or
21
26
  # sequel_#{plugin}, and then attempt to load the module using a
22
27
  # the camelized plugin name under Sequel::Plugins.
23
- def self.plugin(plugin, *args)
28
+ def self.plugin(plugin, *args, &blk)
24
29
  arg = args.first
25
- block = lambda{arg}
30
+ block = args.length > 1 ? lambda{args} : lambda{arg}
26
31
  m = plugin.is_a?(Module) ? plugin : plugin_module(plugin)
27
- if m.respond_to?(:apply)
28
- m.apply(self, *args)
29
- end
30
- if m.const_defined?("InstanceMethods")
31
- define_method(:"#{plugin}_opts", &block)
32
- include(m::InstanceMethods)
33
- end
34
- if m.const_defined?("ClassMethods")
35
- meta_def(:"#{plugin}_opts", &block)
36
- extend(m::ClassMethods)
37
- end
38
- if m.const_defined?("DatasetMethods")
39
- if @dataset
40
- dataset.meta_def(:"#{plugin}_opts", &block)
41
- dataset.extend(m::DatasetMethods)
32
+ unless @plugins.include?(m)
33
+ @plugins << m
34
+ m.apply(self, *args, &blk) if m.respond_to?(:apply)
35
+ if m.const_defined?("InstanceMethods")
36
+ define_method(:"#{plugin}_opts", &block)
37
+ include(m::InstanceMethods)
38
+ end
39
+ if m.const_defined?("ClassMethods")
40
+ meta_def(:"#{plugin}_opts", &block)
41
+ extend(m::ClassMethods)
42
+ end
43
+ if m.const_defined?("DatasetMethods")
44
+ if @dataset
45
+ dataset.meta_def(:"#{plugin}_opts", &block)
46
+ dataset.extend(m::DatasetMethods)
47
+ end
48
+ dataset_method_modules << m::DatasetMethods
49
+ meths = m::DatasetMethods.public_instance_methods.reject{|x| NORMAL_METHOD_NAME_REGEXP !~ x.to_s}
50
+ def_dataset_method(*meths) unless meths.empty?
42
51
  end
43
- dataset_method_modules << m::DatasetMethods
44
- def_dataset_method(*m::DatasetMethods.public_instance_methods.reject{|x| NORMAL_METHOD_NAME_REGEXP !~ x.to_s})
45
52
  end
53
+ m.configure(self, *args, &blk) if m.respond_to?(:configure)
46
54
  end
47
55
 
48
56
  module ClassMethods
57
+ # Array of plugins loaded by this class
58
+ attr_reader :plugins
59
+
49
60
  private
50
61
 
51
62
  # Returns the new style location for the plugin name.
@@ -18,7 +18,7 @@ module Sequel
18
18
  module Caching
19
19
  # Set the cache_store and cache_ttl attributes for the given model.
20
20
  # If the :ttl option is not given, 3600 seconds is the default.
21
- def self.apply(model, store, opts={})
21
+ def self.configure(model, store, opts={})
22
22
  model.instance_eval do
23
23
  @cache_store = store
24
24
  @cache_ttl = opts[:ttl] || 3600
@@ -32,20 +32,6 @@ module Sequel
32
32
 
33
33
  # The time to live for the cache store, in seconds.
34
34
  attr_reader :cache_ttl
35
-
36
- # Check the cache before a database lookup unless a hash is supplied.
37
- def [](*args)
38
- args = args.first if (args.size == 1)
39
- return super(args) if args.is_a?(Hash)
40
- ck = cache_key(args)
41
- if obj = @cache_store.get(ck)
42
- return obj
43
- end
44
- if obj = super(args)
45
- @cache_store.set(ck, obj, @cache_ttl)
46
- end
47
- obj
48
- end
49
35
 
50
36
  # Set the time to live for the cache store, in seconds (default is 3600, # so 1 hour).
51
37
  def set_cache_ttl(ttl)
@@ -75,6 +61,18 @@ module Sequel
75
61
  def cache_key(pk)
76
62
  "#{self}:#{Array(pk).join(',')}"
77
63
  end
64
+
65
+ # Check the cache before a database lookup unless a hash is supplied.
66
+ def primary_key_lookup(pk)
67
+ ck = cache_key(pk)
68
+ if obj = @cache_store.get(ck)
69
+ return obj
70
+ end
71
+ if obj = super(pk)
72
+ @cache_store.set(ck, obj, @cache_ttl)
73
+ end
74
+ obj
75
+ end
78
76
  end
79
77
 
80
78
  module InstanceMethods
@@ -0,0 +1,107 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The identity_map plugin allows the user to create temporary identity maps
4
+ # via the with_identity_map method, which takes a block. Inside the block,
5
+ # objects have a 1-1 correspondence with rows in the database.
6
+ #
7
+ # For example, the following is true, and wouldn't be true if you weren't
8
+ # using the identity map:
9
+ # Sequel::Model.with_identity_map do
10
+ # Album.filter{(id > 0) & (id < 2)}.first.object_id == Album.first(:id=>1).object_id
11
+ # end
12
+ #
13
+ # In additional to providing a 1-1 correspondence, the identity_map plugin
14
+ # also provides a cached looked up of records in two cases:
15
+ # * Model.[] (e.g. Album[1])
16
+ # * Model.many_to_one accessor methods (e.g. album.artist)
17
+ #
18
+ # If the object you are looking up using one of those two methods is already
19
+ # in the identity map, the record is returned without a database query being
20
+ # issued.
21
+ #
22
+ # Identity maps are thread-local and only presist for the duration of the block,
23
+ # so they should be should only be considered as a possible performance enhancer.
24
+ module IdentityMap
25
+ module ClassMethods
26
+ # Returns the current thread-local identity map. Should be a hash if
27
+ # there is an active identity map, and nil otherwise.
28
+ def identity_map
29
+ Thread.current[:sequel_identity_map]
30
+ end
31
+
32
+ # The identity map key for an object of the current class with the given pk.
33
+ # May not always be correct for a class which uses STI.
34
+ def identity_map_key(pk)
35
+ "#{self}:#{pk ? Array(pk).join(',') : "nil:#{rand}"}"
36
+ end
37
+
38
+ # If the identity map is in use, check it for a current copy of the object.
39
+ # If a copy does not exist, create a new object and add it to the identity map.
40
+ # If a copy exists, add any values in the given row that aren't currently
41
+ # in the object to the object's values. This allows you to only request
42
+ # certain fields in an initial query, make modifications to some of those
43
+ # fields and request other, potentially overlapping fields in a new query,
44
+ # and not have the second query override fields you modified.
45
+ def load(row)
46
+ return super unless idm = identity_map
47
+ if o = idm[identity_map_key(Array(primary_key).map{|x| row[x]})]
48
+ o.merge_db_update(row)
49
+ else
50
+ o = super
51
+ idm[identity_map_key(o.pk)] = o
52
+ end
53
+ o
54
+ end
55
+
56
+ # Take a block and inside that block use an identity map to ensure a 1-1
57
+ # correspondence of objects to the database row they represent.
58
+ def with_identity_map
59
+ return yield if identity_map
60
+ begin
61
+ self.identity_map = {}
62
+ yield
63
+ ensure
64
+ self.identity_map = nil
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # Set the thread local identity map to the given value.
71
+ def identity_map=(v)
72
+ Thread.current[:sequel_identity_map] = v
73
+ end
74
+
75
+ # Check the current identity map if it exists for the object with
76
+ # the matching pk. If one is found, return it, otherwise call super.
77
+ def primary_key_lookup(pk)
78
+ (idm = identity_map and o = idm[identity_map_key(pk)]) ? o : super
79
+ end
80
+ end
81
+
82
+ module InstanceMethods
83
+ # Merge the current values into the values provided in the row, ensuring
84
+ # that current values are not overridden by new values.
85
+ def merge_db_update(row)
86
+ @values = row.merge(@values)
87
+ end
88
+
89
+ private
90
+
91
+ # If the association is a many_to_one and it has a :key option and the
92
+ # key option has a value and the association uses the primary key of
93
+ # the associated class as the :primary_key option, check the identity
94
+ # map for the associated object and return it if present.
95
+ def _load_associated_objects(opts)
96
+ klass = opts.associated_class
97
+ if idm = model.identity_map and !opts.returns_array? and opts[:key] and pk = send(opts[:key]) and
98
+ opts[:primary_key] == klass.primary_key and o = idm[klass.identity_map_key(pk)]
99
+ o
100
+ else
101
+ super
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,65 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The lazy_attributes plugin allows users to easily set that some attributes
4
+ # should not be loaded by default when loading model objects. If the attribute
5
+ # is needed after the instance has been retrieved, a database query is made to
6
+ # retreive the value of the attribute.
7
+ #
8
+ # This plugin depends on the identity_map and tactical_eager_loading plugin, and allows you to
9
+ # eagerly load lazy attributes for all objects retrieved with the current object.
10
+ # So the following code should issue one query to get the albums and one query to
11
+ # get the reviews for all of those albums:
12
+ #
13
+ # Album.plugin :lazy_attributes, :review
14
+ # Sequel::Model.with_identity_map do
15
+ # Album.filter{id<100}.all do |a|
16
+ # a.review
17
+ # end
18
+ # end
19
+ module LazyAttributes
20
+ # Tactical eager loading requires the tactical_eager_loading plugin
21
+ def self.apply(model, *attrs)
22
+ model.plugin :identity_map
23
+ model.plugin :tactical_eager_loading
24
+ end
25
+
26
+ # Set the attributes given as lazy attributes
27
+ def self.configure(model, *attrs)
28
+ model.lazy_attributes(*attrs) unless attrs.empty?
29
+ end
30
+
31
+ module ClassMethods
32
+ # Remove the given attributes from the list of columns selected by default.
33
+ # For each attribute given, create an accessor method that allows a lazy
34
+ # lookup of the attribute. Each attribute should be given as a symbol.
35
+ def lazy_attributes(*attrs)
36
+ set_dataset(dataset.select(*(columns - attrs)))
37
+ attrs.each do |a|
38
+ define_method(a) do
39
+ if !values.include?(a) && !new?
40
+ lazy_attribute_lookup(a)
41
+ else
42
+ super()
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ module InstanceMethods
50
+ private
51
+
52
+ # If the model was selected with other model objects, eagerly load the
53
+ # attribute for all of those objects. If not, query the database for
54
+ # the attribute for just the current object. Return the value of
55
+ # the attribute for the current object.
56
+ def lazy_attribute_lookup(a)
57
+ primary_key = model.primary_key
58
+ model.select(*(Array(primary_key) + [a])).filter(primary_key=>retrieved_with.map{|o| o.pk}.sql_array).all if model.identity_map && retrieved_with
59
+ values[a] = this.select(a).first[a] unless values.include?(a)
60
+ values[a]
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,188 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The many_through_many plugin allow you to create a association to multiple objects using multiple join tables.
4
+ # For example, assume the following associations:
5
+ #
6
+ # Artist.many_to_many :albums
7
+ # Album.many_to_many :tags
8
+ #
9
+ # The many_through_many plugin would allow this:
10
+ #
11
+ # Artist.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
12
+ #
13
+ # Which will give you the tags for all of the artist's albums.
14
+ #
15
+ # Here are some more examples:
16
+ #
17
+ # # Same as Artist.many_to_many :albums
18
+ # Artist.many_through_many :albums, [[:albums_artists, :artist_id, :album_id]]
19
+ #
20
+ # # All artists that are associated to any album that this artist is associated to
21
+ # Artist.many_through_many :artists, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_artists, :album_id, :artist_id]]
22
+ #
23
+ # # All albums by artists that are associated to any album that this artist is associated to
24
+ # Artist.many_through_many :artist_albums, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], \
25
+ # [:albums_artists, :album_id, :artist_id], [:artists, :id, :id], [:albums_artists, :artist_id, :album_id]], \
26
+ # :class=>:Album
27
+ #
28
+ # # All tracks on albums by this artist
29
+ # Artist.many_through_many :tracks, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id]], \
30
+ # :right_primary_key=>:album_id
31
+ module ManyThroughMany
32
+ # The AssociationReflection subclass for many_through_many associations.
33
+ class ManyThroughManyAssociationReflection < Sequel::Model::Associations::ManyToManyAssociationReflection
34
+ Sequel::Model::Associations::ASSOCIATION_TYPES[:many_through_many] = self
35
+
36
+ # The table containing the column to use for the associated key when eagerly loading
37
+ def associated_key_table
38
+ self[:associated_key_table] = self[:final_reverse_edge][:alias]
39
+ end
40
+
41
+ # The list of joins to use when eager graphing
42
+ def edges
43
+ self[:edges] || calculate_edges || self[:edges]
44
+ end
45
+
46
+ # Many through many associations don't have a reciprocal
47
+ def reciprocal
48
+ nil
49
+ end
50
+
51
+ # The list of joins to use when lazy loading or eager loading
52
+ def reverse_edges
53
+ self[:reverse_edges] || calculate_edges || self[:reverse_edges]
54
+ end
55
+
56
+ private
57
+
58
+ # Make sure to use unique table aliases when lazy loading or eager loading
59
+ def calculate_reverse_edge_aliases(reverse_edges)
60
+ aliases = [associated_class.table_name]
61
+ reverse_edges.each do |e|
62
+ table_alias = e[:table]
63
+ if aliases.include?(table_alias)
64
+ i = 0
65
+ table_alias = loop do
66
+ ta = :"#{table_alias}_#{i}"
67
+ break ta unless aliases.include?(ta)
68
+ i += 1
69
+ end
70
+ end
71
+ aliases.push(e[:alias] = table_alias)
72
+ end
73
+ end
74
+
75
+ # Transform the :through option into a list of edges and reverse edges to use to join tables when loading the association.
76
+ def calculate_edges
77
+ es = [{:left_table=>self[:model].table_name, :left_key=>self[:left_primary_key]}]
78
+ self[:through].each do |t|
79
+ es.last.merge!(:right_key=>t[:left], :right_table=>t[:table], :join_type=>t[:join_type]||self[:graph_join_type], :conditions=>(t[:conditions]||[]).to_a, :block=>t[:block])
80
+ es.last[:only_conditions] = t[:only_conditions] if t.include?(:only_conditions)
81
+ es << {:left_table=>t[:table], :left_key=>t[:right]}
82
+ end
83
+ es.last.merge!(:right_key=>right_primary_key, :right_table=>associated_class.table_name)
84
+ edges = es.map do |e|
85
+ h = {:table=>e[:right_table], :left=>e[:left_key], :right=>e[:right_key], :conditions=>e[:conditions], :join_type=>e[:join_type], :block=>e[:block]}
86
+ h[:only_conditions] = e[:only_conditions] if e.include?(:only_conditions)
87
+ h
88
+ end
89
+ reverse_edges = es.reverse.map{|e| {:table=>e[:left_table], :left=>e[:left_key], :right=>e[:right_key]}}
90
+ reverse_edges.pop
91
+ calculate_reverse_edge_aliases(reverse_edges)
92
+ self[:final_edge] = edges.pop
93
+ self[:final_reverse_edge] = reverse_edges.pop
94
+ self[:edges] = edges
95
+ self[:reverse_edges] = reverse_edges
96
+ nil
97
+ end
98
+ end
99
+ module ClassMethods
100
+ # Create a many_through_many association. Arguments:
101
+ # * name - Same as associate, the name of the association.
102
+ # * through - The tables and keys to join between the current table and the associated table.
103
+ # Must be an array, with elements that are either 3 element arrays, or hashes with keys :table, :left, and :right.
104
+ # The required entries in the array/hash are:
105
+ # * :table (first array element) - The name of the table to join.
106
+ # * :left (middle array element) - The key joining the table to the previous table
107
+ # * :right (last array element) - The key joining the table to the next table
108
+ # If a hash is provided, the following keys are respected when using eager_graph:
109
+ # * :block - A proc to use as the block argument to join.
110
+ # * :conditions - Extra conditions to add to the JOIN ON clause. Must be a hash or array of two pairs.
111
+ # * :join_type - The join type to use for the join, defaults to :left_outer.
112
+ # * :only_conditions - Conditions to use for the join instead of the ones specified by the keys.
113
+ # * opts - The options for the associaion. Takes the same options as associate, and supports these additional options:
114
+ # * :left_primary_key - column in current table that the first :left option in through points to, as a symbol. Defaults to primary key of current table.
115
+ # * :right_primary_key - column in associated table that the final :right option in through points to, as a symbol. Defaults to primary key of the associated table.
116
+ # * :uniq - Adds a after_load callback that makes the array of objects unique.
117
+ def many_through_many(name, through, opts={}, &block)
118
+ associate(:many_through_many, name, opts.merge(:through=>through), &block)
119
+ end
120
+
121
+ private
122
+
123
+ # Create the association methods and :eager_loader and :eager_grapher procs.
124
+ def def_many_through_many(opts)
125
+ name = opts[:name]
126
+ model = self
127
+ opts[:read_only] = true
128
+ opts[:class_name] ||= camelize(singularize(name))
129
+ opts[:after_load].unshift(:array_uniq!) if opts[:uniq]
130
+ opts[:cartesian_product_number] ||= 2
131
+ opts[:through] = opts[:through].map do |e|
132
+ case e
133
+ when Array
134
+ raise(Error, "array elements of the through option/argument for many_through_many associations must have at least three elements") unless e.length == 3
135
+ {:table=>e[0], :left=>e[1], :right=>e[2]}
136
+ when Hash
137
+ raise(Error, "hash elements of the through option/argument for many_through_many associations must contain :table, :left, and :right keys") unless e[:table] && e[:left] && e[:right]
138
+ e
139
+ else
140
+ raise(Error, "the through option/argument for many_through_many associations must be an enumerable of arrays or hashes")
141
+ end
142
+ end
143
+
144
+ left_key = opts[:left_key] = opts[:through].first[:left]
145
+ left_pk = (opts[:left_primary_key] ||= self.primary_key)
146
+ opts[:dataset] ||= lambda do
147
+ ds = opts.associated_class
148
+ opts.reverse_edges.each{|t| ds = ds.join(t[:table], [[t[:left], t[:right]]], :table_alias=>t[:alias])}
149
+ ft = opts[:final_reverse_edge]
150
+ ds.join(ft[:table], [[ft[:left], ft[:right]], [left_key, send(left_pk)]], :table_alias=>ft[:alias])
151
+ end
152
+
153
+ left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
154
+ opts[:eager_loader] ||= lambda do |key_hash, records, associations|
155
+ h = key_hash[left_pk]
156
+ records.each{|object| object.associations[name] = []}
157
+ ds = opts.associated_class
158
+ opts.reverse_edges.each{|t| ds = ds.join(t[:table], [[t[:left], t[:right]]], :table_alias=>t[:alias])}
159
+ ft = opts[:final_reverse_edge]
160
+ ds = ds.join(ft[:table], [[ft[:left], ft[:right]], [left_key, h.keys]], :table_alias=>ft[:alias])
161
+ model.eager_loading_dataset(opts, ds, Array(opts.select), associations).all do |assoc_record|
162
+ next unless objects = h[assoc_record.values.delete(left_key_alias)]
163
+ objects.each{|object| object.associations[name].push(assoc_record)}
164
+ end
165
+ end
166
+
167
+ join_type = opts[:graph_join_type]
168
+ select = opts[:graph_select]
169
+ graph_block = opts[:graph_block]
170
+ only_conditions = opts[:graph_only_conditions]
171
+ use_only_conditions = opts.include?(:graph_only_conditions)
172
+ conditions = opts[:graph_conditions]
173
+ opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
174
+ iq = table_alias
175
+ opts.edges.each do |t|
176
+ ds = ds.graph(t[:table], t.include?(:only_conditions) ? t[:only_conditions] : ([[t[:right], t[:left]]] + t[:conditions]), :select=>false, :table_alias=>ds.send(:eager_unique_table_alias, ds, t[:table]), :join_type=>t[:join_type], :implicit_qualifier=>iq, &t[:block])
177
+ iq = nil
178
+ end
179
+ fe = opts[:final_edge]
180
+ ds.graph(opts.associated_class, use_only_conditions ? only_conditions : ([[opts.right_primary_key, fe[:left]]] + conditions), :select=>select, :table_alias=>assoc_alias, :join_type=>join_type, &graph_block)
181
+ end
182
+
183
+ def_association_dataset_methods(opts)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end