sequel 3.9.0 → 3.10.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 (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