sequel 3.9.0 → 3.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/CHANGELOG +56 -0
  2. data/README.rdoc +1 -1
  3. data/Rakefile +1 -1
  4. data/doc/advanced_associations.rdoc +7 -10
  5. data/doc/release_notes/3.10.0.txt +286 -0
  6. data/lib/sequel/adapters/do/mysql.rb +4 -0
  7. data/lib/sequel/adapters/jdbc.rb +5 -0
  8. data/lib/sequel/adapters/jdbc/as400.rb +58 -0
  9. data/lib/sequel/adapters/jdbc/oracle.rb +30 -0
  10. data/lib/sequel/adapters/shared/mssql.rb +23 -9
  11. data/lib/sequel/adapters/shared/mysql.rb +12 -1
  12. data/lib/sequel/adapters/shared/postgres.rb +7 -18
  13. data/lib/sequel/adapters/shared/sqlite.rb +5 -0
  14. data/lib/sequel/adapters/sqlite.rb +5 -0
  15. data/lib/sequel/connection_pool/single.rb +3 -3
  16. data/lib/sequel/database.rb +3 -2
  17. data/lib/sequel/dataset.rb +6 -5
  18. data/lib/sequel/dataset/convenience.rb +3 -3
  19. data/lib/sequel/dataset/query.rb +13 -0
  20. data/lib/sequel/dataset/sql.rb +31 -1
  21. data/lib/sequel/extensions/schema_dumper.rb +3 -3
  22. data/lib/sequel/model.rb +8 -6
  23. data/lib/sequel/model/associations.rb +144 -102
  24. data/lib/sequel/model/base.rb +21 -1
  25. data/lib/sequel/model/plugins.rb +3 -1
  26. data/lib/sequel/plugins/association_dependencies.rb +14 -7
  27. data/lib/sequel/plugins/caching.rb +4 -0
  28. data/lib/sequel/plugins/composition.rb +138 -0
  29. data/lib/sequel/plugins/identity_map.rb +2 -2
  30. data/lib/sequel/plugins/lazy_attributes.rb +1 -1
  31. data/lib/sequel/plugins/nested_attributes.rb +3 -2
  32. data/lib/sequel/plugins/rcte_tree.rb +281 -0
  33. data/lib/sequel/plugins/typecast_on_load.rb +16 -5
  34. data/lib/sequel/sql.rb +18 -1
  35. data/lib/sequel/version.rb +1 -1
  36. data/spec/adapters/mssql_spec.rb +4 -0
  37. data/spec/adapters/mysql_spec.rb +4 -0
  38. data/spec/adapters/postgres_spec.rb +55 -5
  39. data/spec/core/database_spec.rb +5 -3
  40. data/spec/core/dataset_spec.rb +86 -15
  41. data/spec/core/expression_filters_spec.rb +23 -6
  42. data/spec/extensions/association_dependencies_spec.rb +24 -5
  43. data/spec/extensions/association_proxies_spec.rb +3 -0
  44. data/spec/extensions/composition_spec.rb +194 -0
  45. data/spec/extensions/identity_map_spec.rb +16 -0
  46. data/spec/extensions/nested_attributes_spec.rb +44 -1
  47. data/spec/extensions/rcte_tree_spec.rb +205 -0
  48. data/spec/extensions/schema_dumper_spec.rb +6 -0
  49. data/spec/extensions/spec_helper.rb +6 -0
  50. data/spec/extensions/typecast_on_load_spec.rb +9 -0
  51. data/spec/extensions/validation_helpers_spec.rb +5 -5
  52. data/spec/integration/dataset_test.rb +13 -9
  53. data/spec/integration/eager_loader_test.rb +56 -1
  54. data/spec/integration/model_test.rb +8 -0
  55. data/spec/integration/plugin_test.rb +270 -0
  56. data/spec/integration/schema_test.rb +1 -1
  57. data/spec/model/associations_spec.rb +541 -118
  58. data/spec/model/eager_loading_spec.rb +24 -3
  59. data/spec/model/record_spec.rb +34 -0
  60. metadata +9 -2
@@ -301,7 +301,7 @@ module Sequel
301
301
  # Example:
302
302
  # class Tagging < Sequel::Model
303
303
  # # composite key
304
- # set_primary_key :taggable_id, :tag_id
304
+ # set_primary_key [:taggable_id, :tag_id]
305
305
  # end
306
306
  #
307
307
  # class Person < Sequel::Model
@@ -437,6 +437,21 @@ module Sequel
437
437
  end
438
438
  schema_hash
439
439
  end
440
+
441
+ # For the given opts hash and default name or :class option, add a
442
+ # :class_name option unless already present which contains the name
443
+ # of the class to use as a string. The purpose is to allow late
444
+ # binding to the class later using constantize.
445
+ def late_binding_class_option(opts, default)
446
+ case opts[:class]
447
+ when String, Symbol
448
+ # Delete :class to allow late binding
449
+ opts[:class_name] ||= opts.delete(:class).to_s
450
+ when Class
451
+ opts[:class_name] ||= opts[:class].name
452
+ end
453
+ opts[:class_name] ||= ((name || '').split("::")[0..-2] + [camelize(default)]).join('::')
454
+ end
440
455
 
441
456
  # Module that the class includes that holds methods the class adds for column accessors and
442
457
  # associations so that the methods can be overridden with super
@@ -653,6 +668,11 @@ module Sequel
653
668
  @values.keys
654
669
  end
655
670
 
671
+ # Refresh this record using for_update unless this is a new record. Returns self.
672
+ def lock!
673
+ new? ? self : _refresh(this.for_update)
674
+ end
675
+
656
676
  # Remove elements of the model object that make marshalling fail. Returns self.
657
677
  def marshallable!
658
678
  @this = nil
@@ -64,7 +64,9 @@ module Sequel
64
64
  # defined, the corresponding plugin gem is automatically loaded.
65
65
  def plugin_module(plugin)
66
66
  module_name = plugin.to_s.gsub(/(^|_)(.)/){|x| x[-1..-1].upcase}
67
- if not Sequel::Plugins.const_defined?(module_name)
67
+ if !Sequel::Plugins.const_defined?(module_name) ||
68
+ (Sequel.const_defined?(module_name) &&
69
+ Sequel::Plugins.const_get(module_name) == Sequel.const_get(module_name))
68
70
  begin
69
71
  Sequel.tsk_require "sequel/plugins/#{plugin}"
70
72
  rescue LoadError
@@ -6,7 +6,7 @@ module Sequel
6
6
  #
7
7
  # * :many_to_many - :nullify (removes all related entries in join table)
8
8
  # * :many_to_one - :delete, :destroy
9
- # * :one_to_many - :delete, :destroy, :nullify (sets foreign key to NULL for all associated objects)
9
+ # * :one_to_many, one_to_one - :delete, :destroy, :nullify (sets foreign key to NULL for all associated objects)
10
10
  #
11
11
  # This plugin works directly with the association datasets and does not use any cached association values.
12
12
  # The :delete action will delete all associated objects from the database in a single SQL call.
@@ -23,7 +23,7 @@ module Sequel
23
23
  module AssociationDependencies
24
24
  # Mapping of association types to when the dependency calls should be made (either
25
25
  # :before for in before_destroy or :after for in after_destroy)
26
- ASSOCIATION_MAPPING = {:one_to_many=>:before, :many_to_one=>:after, :many_to_many=>:before}
26
+ ASSOCIATION_MAPPING = {:one_to_many=>:before, :many_to_one=>:after, :many_to_many=>:before, :one_to_one=>:before}
27
27
 
28
28
  # The valid dependence actions
29
29
  DEPENDENCE_ACTIONS = [:delete, :destroy, :nullify]
@@ -52,13 +52,20 @@ module Sequel
52
52
  def add_association_dependencies(hash)
53
53
  hash.each do |association, action|
54
54
  raise(Error, "Nonexistent association: #{association}") unless r = association_reflection(association)
55
+ type = r[:type]
55
56
  raise(Error, "Invalid dependence action type: association: #{association}, dependence action: #{action}") unless DEPENDENCE_ACTIONS.include?(action)
56
- raise(Error, "Invalid association type: association: #{association}, type: #{r[:type]}") unless time = ASSOCIATION_MAPPING[r[:type]]
57
+ raise(Error, "Invalid association type: association: #{association}, type: #{type}") unless time = ASSOCIATION_MAPPING[type]
57
58
  association_dependencies[:"#{time}_#{action}"] << if action == :nullify
58
- raise(Error, "Can't nullify many_to_one associated objects: association: #{association}") if r[:type] == :many_to_one
59
- r.remove_all_method
59
+ case type
60
+ when :one_to_many , :many_to_many
61
+ proc{send(r.remove_all_method)}
62
+ when :one_to_one
63
+ proc{send(r.setter_method, nil)}
64
+ else
65
+ raise(Error, "Can't nullify many_to_one associated objects: association: #{association}")
66
+ end
60
67
  else
61
- raise(Error, "Can only nullify many_to_many associations: association: #{association}") if r[:type] == :many_to_many
68
+ raise(Error, "Can only nullify many_to_many associations: association: #{association}") if type == :many_to_many
62
69
  r.dataset_method
63
70
  end
64
71
  end
@@ -87,7 +94,7 @@ module Sequel
87
94
  def before_destroy
88
95
  model.association_dependencies[:before_delete].each{|m| send(m).delete}
89
96
  model.association_dependencies[:before_destroy].each{|m| send(m).destroy}
90
- model.association_dependencies[:before_nullify].each{|m| send(m)}
97
+ model.association_dependencies[:before_nullify].each{|p| instance_eval(&p)}
91
98
  super
92
99
  end
93
100
  end
@@ -15,11 +15,15 @@ module Sequel
15
15
  # cache_store.get(key) => obj # Returns object set with same key.
16
16
  # cache_store.get(key2) => nil # nil returned if there isn't an object
17
17
  # # currently in the cache with that key.
18
+ # cache_store.delete(key) # Remove key from cache
18
19
  #
19
20
  # If the :ignore_exceptions option is true, exceptions raised by cache_store.get
20
21
  # are ignored and nil is returned instead. The memcached API is to
21
22
  # raise an exception for a missing record, so if you use memcached, you will
22
23
  # want to use this option.
24
+ #
25
+ # Note that only Model.[] method calls with a primary key argument are cached
26
+ # using this plugin.
23
27
  module Caching
24
28
  # Set the cache_store and cache_ttl attributes for the given model.
25
29
  # If the :ttl option is not given, 3600 seconds is the default.
@@ -0,0 +1,138 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The composition plugin allows you to easily define getter and
4
+ # setter instance methods for a class where the backing data
5
+ # is composed of other getters and decomposed to other setters.
6
+ #
7
+ # A simple example of this is when you have a database table with
8
+ # separate columns for year, month, and day, but where you want
9
+ # to deal with Date objects in your ruby code. This can be handled
10
+ # with:
11
+ #
12
+ # Model.composition :date, :mapping=>[:year, :month, :day]
13
+ #
14
+ # The :mapping option is optional, but you can define custom
15
+ # composition and decomposition procs via the :composer and
16
+ # :decomposer options.
17
+ #
18
+ # Note that when using the composition object, you should not
19
+ # modify the underlying columns if you are also instantiating
20
+ # the composition, as otherwise the composition object values
21
+ # will override any underlying columns when the object is saved.
22
+ module Composition
23
+ # Define the necessary class instance variables.
24
+ def self.apply(model)
25
+ model.instance_eval{@compositions = {}}
26
+ end
27
+
28
+ module ClassMethods
29
+ # A hash with composition name keys and composition reflection
30
+ # hash values.
31
+ attr_reader :compositions
32
+
33
+ # A module included in the class holding the composition
34
+ # getter and setter methods.
35
+ attr_reader :composition_module
36
+
37
+ # Define a composition for this model, with name being the name of the composition.
38
+ # You must provide either a :mapping option or both the :composer and :decomposer options.
39
+ #
40
+ # Options:
41
+ # * :class - if using the :mapping option, the class to use, as a Class, String or Symbol.
42
+ # * :composer - A proc that is instance evaled when the composition getter method is called
43
+ # to create the composition.
44
+ # * :decomposer - A proc that is instance evaled before saving the model object,
45
+ # if the composition object exists, which sets the columns in the model object
46
+ # based on the value of the composition object.
47
+ # * :mapping - An array where each element is either a symbol or an array of two symbols.
48
+ # A symbol is treated like an array of two symbols where both symbols are the same.
49
+ # The first symbol represents the getter method in the model, and the second symbol
50
+ # represents the getter method in the composition object. Example:
51
+ # # Uses columns year, month, and day in the current model
52
+ # # Uses year, month, and day methods in the composition object
53
+ # :mapping=>[:year, :month, :day]
54
+ # # Uses columns year, month, and day in the current model
55
+ # # Uses y, m, and d methods in the composition object where
56
+ # # for example y in the composition object represents year
57
+ # # in the model object.
58
+ # :mapping=>[[:year, :y], [:month, :m], [:day, :d]]
59
+ def composition(name, opts={})
60
+ opts = opts.dup
61
+ compositions[name] = opts
62
+ if mapping = opts[:mapping]
63
+ keys = mapping.map{|k| k.is_a?(Array) ? k.first : k}
64
+ if !opts[:composer]
65
+ late_binding_class_option(opts, name)
66
+ klass = opts[:class]
67
+ class_proc = proc{klass || constantize(opts[:class_name])}
68
+ opts[:composer] = proc do
69
+ if values = keys.map{|k| send(k)} and values.any?{|v| !v.nil?}
70
+ class_proc.call.new(*values)
71
+ else
72
+ nil
73
+ end
74
+ end
75
+ end
76
+ if !opts[:decomposer]
77
+ setter_meths = keys.map{|k| :"#{k}="}
78
+ cov_methods = mapping.map{|k| k.is_a?(Array) ? k.last : k}
79
+ setters = setter_meths.zip(cov_methods)
80
+ opts[:decomposer] = proc do
81
+ if (o = compositions[name]).nil?
82
+ setter_meths.each{|sm| send(sm, nil)}
83
+ else
84
+ setters.each{|sm, cm| send(sm, o.send(cm))}
85
+ end
86
+ end
87
+ end
88
+ end
89
+ raise(Error, "Must provide :composer and :decomposer options, or :mapping option") unless opts[:composer] && opts[:decomposer]
90
+ define_composition_accessor(name, opts)
91
+ end
92
+
93
+ # Copy the necessary class instance variables to the subclass.
94
+ def inherited(subclass)
95
+ super
96
+ c = compositions.dup
97
+ subclass.instance_eval{@compositions = c}
98
+ end
99
+
100
+ # Define getter and setter methods for the composition object.
101
+ def define_composition_accessor(name, opts={})
102
+ include(@composition_module ||= Module.new) unless composition_module
103
+ composer = opts[:composer]
104
+ composition_module.class_eval do
105
+ define_method(name) do
106
+ compositions.include?(name) ? compositions[name] : (compositions[name] = instance_eval(&composer))
107
+ end
108
+ define_method("#{name}=") do |v|
109
+ modified!
110
+ compositions[name] = v
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ module InstanceMethods
117
+ # Clear the cached compositions when refreshing.
118
+ def _refresh(ds)
119
+ v = super
120
+ compositions.clear
121
+ v
122
+ end
123
+
124
+ # For each composition, set the columns in the model class based
125
+ # on the composition object.
126
+ def before_save
127
+ @compositions.keys.each{|n| instance_eval(&model.compositions[n][:decomposer])} if @compositions
128
+ super
129
+ end
130
+
131
+ # Cache of composition objects for this class.
132
+ def compositions
133
+ @compositions ||= {}
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -103,8 +103,8 @@ module Sequel
103
103
  # map for the associated object and return it if present.
104
104
  def _load_associated_objects(opts)
105
105
  klass = opts.associated_class
106
- if idm = model.identity_map and !opts.returns_array? and opts[:key] and pk = send(opts[:key]) and
107
- opts[:primary_key] == klass.primary_key and o = idm[klass.identity_map_key(pk)]
106
+ if idm = model.identity_map and opts[:type] == :many_to_one and opts[:primary_key] == klass.primary_key and
107
+ opts[:key] and pk = send(opts[:key]) and o = idm[klass.identity_map_key(pk)]
108
108
  o
109
109
  else
110
110
  super
@@ -17,7 +17,7 @@ module Sequel
17
17
  # end
18
18
  # end
19
19
  module LazyAttributes
20
- # Tactical eager loading requires the tactical_eager_loading plugin
20
+ # Lazy attributes requires the identity map and tactical eager loading plugins
21
21
  def self.apply(model, *attrs)
22
22
  model.plugin :identity_map
23
23
  model.plugin :tactical_eager_loading
@@ -91,9 +91,10 @@ module Sequel
91
91
  send(reflection[:name]) << obj
92
92
  after_save_hook{send(reflection.add_method, obj)}
93
93
  else
94
+ associations[reflection[:name]] = obj
94
95
  # Don't need to validate the object twice if :validate association option is not false
95
96
  # and don't want to validate it at all if it is false.
96
- before_save_hook{send(reflection.setter_method, obj.save(:validate=>false))}
97
+ send(reflection[:type] == :many_to_one ? :before_save_hook : :after_save_hook){send(reflection.setter_method, obj.save(:validate=>false))}
97
98
  end
98
99
  obj
99
100
  end
@@ -103,7 +104,7 @@ module Sequel
103
104
  def nested_attributes_find(reflection, pk)
104
105
  pk = pk.to_s
105
106
  unless obj = Array(associated_objects = send(reflection[:name])).find{|x| x.pk.to_s == pk}
106
- raise(Error, 'no associated object with that primary key does not exist') unless reflection[:nested_attributes][:strict] == false
107
+ raise(Error, "no matching associated object with given primary key (association: #{reflection[:name]}, pk: #{pk})") unless reflection[:nested_attributes][:strict] == false
107
108
  end
108
109
  obj
109
110
  end
@@ -0,0 +1,281 @@
1
+ module Sequel
2
+ module Plugins
3
+ # = Overview
4
+ #
5
+ # The rcte_tree plugin deals with tree structured data stored
6
+ # in the database using the adjacency list model (where child rows
7
+ # have a foreign key pointing to the parent rows), using recursive
8
+ # common table expressions to load all ancestors in a single query,
9
+ # all descendants in a single query, and all descendants to a given
10
+ # level (where level 1 is children, level 2 is children and grandchildren
11
+ # etc.) in a single query.
12
+ #
13
+ # = Background
14
+ #
15
+ # There are two types of common models for storing tree structured data
16
+ # in an SQL database, the adjacency list model and the nested set model.
17
+ # Before recursive common table expressions (or similar capabilities such
18
+ # as CONNECT BY for Oracle), the nested set model was the only easy way
19
+ # to retrieve all ancestors and descendants in a single query. However,
20
+ # it has significant performance corner cases.
21
+ #
22
+ # On PostgreSQL 8.4, with a significant number of rows, the nested set
23
+ # model is almost 500 times slower than using a recursive common table
24
+ # expression with the adjacency list model to get all descendants, and
25
+ # almost 24,000 times slower to get all descendants to a given level.
26
+ #
27
+ # Considering that the nested set model requires more difficult management
28
+ # than the adjacency list model, it's almost always better to use the
29
+ # adjacency list model if your database supports common table expressions.
30
+ # See http://explainextended.com/2009/09/24/adjacency-list-vs-nested-sets-postgresql/
31
+ # for detailed analysis.
32
+ #
33
+ # = Usage
34
+ #
35
+ # The rcte_tree plugin is unlike most plugins in that it doesn't add any class,
36
+ # instance, or dataset modules. It only has a single apply method, which
37
+ # adds four associations to the model: parent, children, ancestors, and
38
+ # descendants. Both the parent and children are fairly standard many_to_one
39
+ # and one_to_many associations, respectively. However, the ancestors and
40
+ # descendants associations are special. Both the ancestors and descendants
41
+ # associations will automatically set the parent and children associations,
42
+ # respectively, for current object and all of the ancestor or descendant
43
+ # objects, whenever they are loaded (either eagerly or lazily). Additionally,
44
+ # the descendants association can take a level argument when called eagerly,
45
+ # which limits the returned objects to only that many levels in the tree (see
46
+ # the Overview).
47
+ #
48
+ # Model.plugin :rcte_tree
49
+ #
50
+ # # Lazy loading
51
+ # model = Model.first
52
+ # model.parent
53
+ # model.children
54
+ # model.ancestors # Populates :parent association for all ancestors
55
+ # model.descendants # Populates :children association for all descendants
56
+ #
57
+ # # Eager loading - also populates the :parent and children associations
58
+ # # for all ancestors and descendants
59
+ # Model.filter(:id=>[1, 2]).eager(:ancestors, :descendants).all
60
+ #
61
+ # # Eager loading children and grand children
62
+ # Model.filter(:id=>[1, 2]).eager(:descendants=>2).all
63
+ # # Eager loading children, grand children, and great grand children
64
+ # Model.filter(:id=>[1, 2]).eager(:descendants=>3).all
65
+ #
66
+ # = Options
67
+ #
68
+ # You can override the options for any specific association by making
69
+ # sure the plugin options contain one of the following keys:
70
+ #
71
+ # * :parent - hash of options for the parent association
72
+ # * :children - hash of options for the children association
73
+ # * :ancestors - hash of options for the ancestors association
74
+ # * :descendants - hash of options for the descendants association
75
+ #
76
+ # Note that you can change the name of the above associations by specifying
77
+ # a :name key in the appropriate hash of options above. For example:
78
+ #
79
+ # Model.plugin :rcte_tree, :parent=>{:name=>:mother},
80
+ # :children=>{:name=>:daughters}, :descendants=>{:name=>:offspring}
81
+ #
82
+ # Any other keys in the main options hash are treated as options shared by
83
+ # all of the associations. Here's a few options that affect the plugin:
84
+ #
85
+ # * :key - The foreign key in the table that points to the primary key
86
+ # of the parent (default: :parent_id)
87
+ # * :primary_key - The primary key to use (default: the model's primary key)
88
+ # * :key_alias - The symbol identifier to use for aliasing when eager
89
+ # loading (default: :x_root_x)
90
+ # * :cte_name - The symbol identifier to use for the common table expression
91
+ # (default: :t)
92
+ # * :level_alias - The symbol identifier to use when eagerly loading descendants
93
+ # up to a given level (default: :x_level_x)
94
+ module RcteTree
95
+ # Create the appropriate parent, children, ancestors, and descendants
96
+ # associations for the model.
97
+ def self.apply(model, opts={})
98
+ opts = opts.dup
99
+ opts[:class] = model
100
+
101
+ key = opts[:key] ||= :parent_id
102
+ prkey = opts[:primary_key] ||= model.primary_key
103
+
104
+ par = opts.merge(opts.fetch(:parent, {}))
105
+ parent = par.fetch(:name, :parent)
106
+ model.many_to_one parent, par
107
+
108
+ chi = opts.merge(opts.fetch(:children, {}))
109
+ childrena = chi.fetch(:name, :children)
110
+ model.one_to_many childrena, chi
111
+
112
+ ka = opts[:key_alias] ||= :x_root_x
113
+ t = opts[:cte_name] ||= :t
114
+ opts[:reciprocal] = nil
115
+ c_all = SQL::ColumnAll.new(model.table_name)
116
+
117
+ a = opts.merge(opts.fetch(:ancestors, {}))
118
+ ancestors = a.fetch(:name, :ancestors)
119
+ a[:read_only] = true unless a.has_key?(:read_only)
120
+ a[:eager_loader_key] = key
121
+ a[:dataset] ||= proc do
122
+ model.from(t).
123
+ with_recursive(t, model.filter(prkey=>send(key)),
124
+ model.join(t, key=>prkey).
125
+ select(c_all))
126
+ end
127
+ aal = Array(a[:after_load])
128
+ aal << proc do |m, ancs|
129
+ unless m.associations.has_key?(parent)
130
+ parent_map = {m[prkey]=>m}
131
+ child_map = {}
132
+ child_map[m[key]] = m if m[key]
133
+ m.associations[parent] = nil
134
+ ancs.each do |obj|
135
+ obj.associations[parent] = nil
136
+ parent_map[obj[prkey]] = obj
137
+ if ok = obj[key]
138
+ child_map[ok] = obj
139
+ end
140
+ end
141
+ parent_map.each do |parent_id, obj|
142
+ if child = child_map[parent_id]
143
+ child.associations[parent] = obj
144
+ end
145
+ end
146
+ end
147
+ end
148
+ a[:after_load] ||= aal
149
+ a[:eager_loader] ||= proc do |key_hash, objects, associations|
150
+ id_map = key_hash[key]
151
+ parent_map = {}
152
+ children_map = {}
153
+ objects.each do |obj|
154
+ parent_map[obj[prkey]] = obj
155
+ (children_map[obj[key]] ||= []) << obj
156
+ obj.associations[ancestors] = []
157
+ obj.associations[parent] = nil
158
+ end
159
+ r = model.association_reflection(ancestors)
160
+ model.eager_loading_dataset(r,
161
+ model.from(t).
162
+ with_recursive(t, model.filter(prkey=>id_map.keys).
163
+ select(SQL::AliasedExpression.new(prkey, ka), c_all),
164
+ model.join(t, key=>prkey).
165
+ select(SQL::QualifiedIdentifier.new(t, ka), c_all)),
166
+ r.select,
167
+ associations).all do |obj|
168
+ opk = obj[prkey]
169
+ if in_pm = parent_map.has_key?(opk)
170
+ if idm_obj = parent_map[opk]
171
+ idm_obj.values[ka] = obj.values[ka]
172
+ obj = idm_obj
173
+ end
174
+ else
175
+ obj.associations[parent] = nil
176
+ parent_map[opk] = obj
177
+ (children_map[obj[key]] ||= []) << obj
178
+ end
179
+
180
+ if roots = id_map[obj.values.delete(ka)]
181
+ roots.each do |root|
182
+ root.associations[ancestors] << obj
183
+ end
184
+ end
185
+ end
186
+ parent_map.each do |parent_id, obj|
187
+ if children = children_map[parent_id]
188
+ children.each do |child|
189
+ child.associations[parent] = obj
190
+ end
191
+ end
192
+ end
193
+ end
194
+ model.one_to_many ancestors, a
195
+
196
+ d = opts.merge(opts.fetch(:descendants, {}))
197
+ descendants = d.fetch(:name, :descendants)
198
+ d[:read_only] = true unless d.has_key?(:read_only)
199
+ la = d[:level_alias] ||= :x_level_x
200
+ d[:dataset] ||= proc do
201
+ model.from(t).
202
+ with_recursive(t, model.filter(key=>send(prkey)),
203
+ model.join(t, prkey=>key).
204
+ select(SQL::ColumnAll.new(model.table_name)))
205
+ end
206
+ dal = Array(d[:after_load])
207
+ dal << proc do |m, descs|
208
+ unless m.associations.has_key?(childrena)
209
+ parent_map = {m[prkey]=>m}
210
+ children_map = {}
211
+ m.associations[childrena] = []
212
+ descs.each do |obj|
213
+ obj.associations[childrena] = []
214
+ if opk = obj[prkey]
215
+ parent_map[opk] = obj
216
+ end
217
+ if ok = obj[key]
218
+ (children_map[ok] ||= []) << obj
219
+ end
220
+ end
221
+ children_map.each do |parent_id, objs|
222
+ parent_map[parent_id].associations[childrena] = objs
223
+ end
224
+ end
225
+ end
226
+ d[:after_load] = dal
227
+ d[:eager_loader] ||= proc do |key_hash, objects, associations|
228
+ id_map = key_hash[prkey]
229
+ parent_map = {}
230
+ children_map = {}
231
+ objects.each do |obj|
232
+ parent_map[obj[prkey]] = obj
233
+ obj.associations[descendants] = []
234
+ obj.associations[childrena] = []
235
+ end
236
+ r = model.association_reflection(descendants)
237
+ base_case = model.filter(key=>id_map.keys).
238
+ select(SQL::AliasedExpression.new(key, ka), c_all)
239
+ recursive_case = model.join(t, prkey=>key).
240
+ select(SQL::QualifiedIdentifier.new(t, ka), c_all)
241
+ if associations.is_a?(Integer)
242
+ level = associations
243
+ no_cache_level = level - 1
244
+ associations = {}
245
+ base_case = base_case.select_more(SQL::AliasedExpression.new(0, la))
246
+ recursive_case = recursive_case.select_more(SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(t, la) + 1, la)).filter(SQL::QualifiedIdentifier.new(t, la) < level - 1)
247
+ end
248
+ model.eager_loading_dataset(r,
249
+ model.from(t).with_recursive(t, base_case, recursive_case),
250
+ r.select,
251
+ associations).all do |obj|
252
+ if level
253
+ no_cache = no_cache_level == obj.values.delete(la)
254
+ end
255
+
256
+ opk = obj[prkey]
257
+ if in_pm = parent_map.has_key?(opk)
258
+ if idm_obj = parent_map[opk]
259
+ idm_obj.values[ka] = obj.values[ka]
260
+ obj = idm_obj
261
+ end
262
+ else
263
+ obj.associations[childrena] = [] unless no_cache
264
+ parent_map[opk] = obj
265
+ end
266
+
267
+ if root = id_map[obj.values.delete(ka)].first
268
+ root.associations[descendants] << obj
269
+ end
270
+
271
+ (children_map[obj[key]] ||= []) << obj
272
+ end
273
+ children_map.each do |parent_id, objs|
274
+ parent_map[parent_id].associations[childrena] = objs.uniq
275
+ end
276
+ end
277
+ model.one_to_many descendants, d
278
+ end
279
+ end
280
+ end
281
+ end