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.
- data/CHANGELOG +56 -0
- data/README.rdoc +1 -1
- data/Rakefile +1 -1
- data/doc/advanced_associations.rdoc +7 -10
- data/doc/release_notes/3.10.0.txt +286 -0
- data/lib/sequel/adapters/do/mysql.rb +4 -0
- data/lib/sequel/adapters/jdbc.rb +5 -0
- data/lib/sequel/adapters/jdbc/as400.rb +58 -0
- data/lib/sequel/adapters/jdbc/oracle.rb +30 -0
- data/lib/sequel/adapters/shared/mssql.rb +23 -9
- data/lib/sequel/adapters/shared/mysql.rb +12 -1
- data/lib/sequel/adapters/shared/postgres.rb +7 -18
- data/lib/sequel/adapters/shared/sqlite.rb +5 -0
- data/lib/sequel/adapters/sqlite.rb +5 -0
- data/lib/sequel/connection_pool/single.rb +3 -3
- data/lib/sequel/database.rb +3 -2
- data/lib/sequel/dataset.rb +6 -5
- data/lib/sequel/dataset/convenience.rb +3 -3
- data/lib/sequel/dataset/query.rb +13 -0
- data/lib/sequel/dataset/sql.rb +31 -1
- data/lib/sequel/extensions/schema_dumper.rb +3 -3
- data/lib/sequel/model.rb +8 -6
- data/lib/sequel/model/associations.rb +144 -102
- data/lib/sequel/model/base.rb +21 -1
- data/lib/sequel/model/plugins.rb +3 -1
- data/lib/sequel/plugins/association_dependencies.rb +14 -7
- data/lib/sequel/plugins/caching.rb +4 -0
- data/lib/sequel/plugins/composition.rb +138 -0
- data/lib/sequel/plugins/identity_map.rb +2 -2
- data/lib/sequel/plugins/lazy_attributes.rb +1 -1
- data/lib/sequel/plugins/nested_attributes.rb +3 -2
- data/lib/sequel/plugins/rcte_tree.rb +281 -0
- data/lib/sequel/plugins/typecast_on_load.rb +16 -5
- data/lib/sequel/sql.rb +18 -1
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +4 -0
- data/spec/adapters/mysql_spec.rb +4 -0
- data/spec/adapters/postgres_spec.rb +55 -5
- data/spec/core/database_spec.rb +5 -3
- data/spec/core/dataset_spec.rb +86 -15
- data/spec/core/expression_filters_spec.rb +23 -6
- data/spec/extensions/association_dependencies_spec.rb +24 -5
- data/spec/extensions/association_proxies_spec.rb +3 -0
- data/spec/extensions/composition_spec.rb +194 -0
- data/spec/extensions/identity_map_spec.rb +16 -0
- data/spec/extensions/nested_attributes_spec.rb +44 -1
- data/spec/extensions/rcte_tree_spec.rb +205 -0
- data/spec/extensions/schema_dumper_spec.rb +6 -0
- data/spec/extensions/spec_helper.rb +6 -0
- data/spec/extensions/typecast_on_load_spec.rb +9 -0
- data/spec/extensions/validation_helpers_spec.rb +5 -5
- data/spec/integration/dataset_test.rb +13 -9
- data/spec/integration/eager_loader_test.rb +56 -1
- data/spec/integration/model_test.rb +8 -0
- data/spec/integration/plugin_test.rb +270 -0
- data/spec/integration/schema_test.rb +1 -1
- data/spec/model/associations_spec.rb +541 -118
- data/spec/model/eager_loading_spec.rb +24 -3
- data/spec/model/record_spec.rb +34 -0
- metadata +9 -2
data/lib/sequel/model/base.rb
CHANGED
@@ -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
|
data/lib/sequel/model/plugins.rb
CHANGED
@@ -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
|
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: #{
|
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
|
-
|
59
|
-
|
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
|
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{|
|
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
|
107
|
-
opts[:
|
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
|
-
#
|
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,
|
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
|