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
@@ -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