sequel 3.21.0 → 3.28.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 +294 -0
- data/README.rdoc +20 -6
- data/Rakefile +20 -15
- data/doc/association_basics.rdoc +210 -43
- data/doc/dataset_basics.rdoc +4 -4
- data/doc/mass_assignment.rdoc +54 -0
- data/doc/migration.rdoc +15 -538
- data/doc/model_hooks.rdoc +64 -27
- data/doc/opening_databases.rdoc +37 -10
- data/doc/prepared_statements.rdoc +16 -10
- data/doc/reflection.rdoc +8 -2
- data/doc/release_notes/3.22.0.txt +39 -0
- data/doc/release_notes/3.23.0.txt +172 -0
- data/doc/release_notes/3.24.0.txt +420 -0
- data/doc/release_notes/3.25.0.txt +88 -0
- data/doc/release_notes/3.26.0.txt +88 -0
- data/doc/release_notes/3.27.0.txt +82 -0
- data/doc/release_notes/3.28.0.txt +304 -0
- data/doc/schema_modification.rdoc +547 -0
- data/doc/testing.rdoc +106 -0
- data/doc/transactions.rdoc +97 -0
- data/doc/virtual_rows.rdoc +2 -2
- data/lib/sequel/adapters/ado.rb +12 -1
- data/lib/sequel/adapters/amalgalite.rb +4 -0
- data/lib/sequel/adapters/db2.rb +95 -58
- data/lib/sequel/adapters/do.rb +12 -0
- data/lib/sequel/adapters/firebird.rb +25 -203
- data/lib/sequel/adapters/ibmdb.rb +440 -0
- data/lib/sequel/adapters/informix.rb +4 -19
- data/lib/sequel/adapters/jdbc/as400.rb +0 -7
- data/lib/sequel/adapters/jdbc/db2.rb +49 -0
- data/lib/sequel/adapters/jdbc/firebird.rb +34 -0
- data/lib/sequel/adapters/jdbc/h2.rb +16 -5
- data/lib/sequel/adapters/jdbc/informix.rb +31 -0
- data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
- data/lib/sequel/adapters/jdbc/mssql.rb +0 -32
- data/lib/sequel/adapters/jdbc/mysql.rb +9 -0
- data/lib/sequel/adapters/jdbc/oracle.rb +2 -27
- data/lib/sequel/adapters/jdbc/postgresql.rb +6 -0
- data/lib/sequel/adapters/jdbc/sqlserver.rb +46 -0
- data/lib/sequel/adapters/jdbc/transactions.rb +34 -0
- data/lib/sequel/adapters/jdbc.rb +62 -29
- data/lib/sequel/adapters/mysql.rb +22 -139
- data/lib/sequel/adapters/mysql2.rb +9 -14
- data/lib/sequel/adapters/odbc/db2.rb +21 -0
- data/lib/sequel/adapters/odbc.rb +15 -3
- data/lib/sequel/adapters/oracle.rb +17 -1
- data/lib/sequel/adapters/postgres.rb +111 -16
- data/lib/sequel/adapters/shared/access.rb +21 -0
- data/lib/sequel/adapters/shared/db2.rb +290 -0
- data/lib/sequel/adapters/shared/firebird.rb +214 -0
- data/lib/sequel/adapters/shared/informix.rb +45 -0
- data/lib/sequel/adapters/shared/mssql.rb +85 -47
- data/lib/sequel/adapters/shared/mysql.rb +50 -7
- data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +143 -0
- data/lib/sequel/adapters/shared/oracle.rb +0 -4
- data/lib/sequel/adapters/shared/postgres.rb +75 -43
- data/lib/sequel/adapters/shared/sqlite.rb +56 -8
- data/lib/sequel/adapters/sqlite.rb +12 -11
- data/lib/sequel/adapters/swift/mysql.rb +9 -0
- data/lib/sequel/adapters/tinytds.rb +139 -7
- data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +55 -0
- data/lib/sequel/ast_transformer.rb +190 -0
- data/lib/sequel/connection_pool/threaded.rb +3 -2
- data/lib/sequel/connection_pool.rb +1 -1
- data/lib/sequel/core.rb +6 -5
- data/lib/sequel/database/connecting.rb +5 -5
- data/lib/sequel/database/dataset.rb +1 -1
- data/lib/sequel/database/dataset_defaults.rb +1 -1
- data/lib/sequel/database/logging.rb +1 -1
- data/lib/sequel/database/misc.rb +38 -17
- data/lib/sequel/database/query.rb +50 -19
- data/lib/sequel/database/schema_generator.rb +8 -5
- data/lib/sequel/database/schema_methods.rb +52 -27
- data/lib/sequel/dataset/actions.rb +167 -48
- data/lib/sequel/dataset/features.rb +57 -8
- data/lib/sequel/dataset/graph.rb +1 -1
- data/lib/sequel/dataset/misc.rb +39 -20
- data/lib/sequel/dataset/mutation.rb +3 -3
- data/lib/sequel/dataset/prepared_statements.rb +29 -14
- data/lib/sequel/dataset/query.rb +182 -32
- data/lib/sequel/dataset/sql.rb +31 -58
- data/lib/sequel/dataset.rb +8 -0
- data/lib/sequel/exceptions.rb +4 -0
- data/lib/sequel/extensions/columns_introspection.rb +61 -0
- data/lib/sequel/extensions/migration.rb +6 -4
- data/lib/sequel/extensions/to_dot.rb +95 -83
- data/lib/sequel/model/associations.rb +893 -309
- data/lib/sequel/model/base.rb +302 -105
- data/lib/sequel/model/errors.rb +1 -1
- data/lib/sequel/model/exceptions.rb +5 -1
- data/lib/sequel/model.rb +13 -7
- data/lib/sequel/plugins/association_pks.rb +22 -4
- data/lib/sequel/plugins/defaults_setter.rb +58 -0
- data/lib/sequel/plugins/identity_map.rb +113 -6
- data/lib/sequel/plugins/many_through_many.rb +67 -5
- data/lib/sequel/plugins/prepared_statements.rb +140 -0
- data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
- data/lib/sequel/plugins/prepared_statements_safe.rb +82 -0
- data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
- data/lib/sequel/plugins/serialization_modification_detection.rb +51 -0
- data/lib/sequel/plugins/sharding.rb +12 -20
- data/lib/sequel/plugins/single_table_inheritance.rb +2 -0
- data/lib/sequel/plugins/update_primary_key.rb +1 -1
- data/lib/sequel/plugins/xml_serializer.rb +3 -3
- data/lib/sequel/sql.rb +107 -51
- data/lib/sequel/timezones.rb +12 -3
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/db2_spec.rb +146 -0
- data/spec/adapters/mssql_spec.rb +36 -0
- data/spec/adapters/mysql_spec.rb +36 -19
- data/spec/adapters/postgres_spec.rb +115 -28
- data/spec/adapters/spec_helper.rb +6 -0
- data/spec/adapters/sqlite_spec.rb +11 -0
- data/spec/core/connection_pool_spec.rb +62 -77
- data/spec/core/database_spec.rb +244 -287
- data/spec/core/dataset_spec.rb +383 -34
- data/spec/core/expression_filters_spec.rb +159 -41
- data/spec/core/schema_spec.rb +326 -3
- data/spec/core/spec_helper.rb +45 -0
- data/spec/extensions/association_pks_spec.rb +38 -0
- data/spec/extensions/columns_introspection_spec.rb +91 -0
- data/spec/extensions/defaults_setter_spec.rb +64 -0
- data/spec/extensions/identity_map_spec.rb +162 -0
- data/spec/extensions/many_through_many_spec.rb +195 -20
- data/spec/extensions/migration_spec.rb +17 -17
- data/spec/extensions/nested_attributes_spec.rb +1 -0
- data/spec/extensions/prepared_statements_associations_spec.rb +126 -0
- data/spec/extensions/prepared_statements_safe_spec.rb +79 -0
- data/spec/extensions/prepared_statements_spec.rb +72 -0
- data/spec/extensions/prepared_statements_with_pk_spec.rb +38 -0
- data/spec/extensions/schema_dumper_spec.rb +2 -2
- data/spec/extensions/schema_spec.rb +12 -20
- data/spec/extensions/serialization_modification_detection_spec.rb +36 -0
- data/spec/extensions/single_table_inheritance_spec.rb +11 -0
- data/spec/extensions/spec_helper.rb +3 -1
- data/spec/extensions/to_dot_spec.rb +3 -5
- data/spec/extensions/xml_serializer_spec.rb +16 -4
- data/spec/integration/associations_test.rb +405 -15
- data/spec/integration/database_test.rb +4 -2
- data/spec/integration/dataset_test.rb +240 -20
- data/spec/integration/plugin_test.rb +142 -5
- data/spec/integration/prepared_statement_test.rb +174 -95
- data/spec/integration/schema_test.rb +128 -16
- data/spec/integration/spec_helper.rb +15 -0
- data/spec/integration/transaction_test.rb +40 -0
- data/spec/integration/type_test.rb +16 -2
- data/spec/model/association_reflection_spec.rb +91 -0
- data/spec/model/associations_spec.rb +476 -5
- data/spec/model/base_spec.rb +91 -1
- data/spec/model/eager_loading_spec.rb +519 -31
- data/spec/model/hooks_spec.rb +161 -0
- data/spec/model/model_spec.rb +89 -2
- data/spec/model/plugins_spec.rb +17 -0
- data/spec/model/record_spec.rb +184 -12
- data/spec/model/spec_helper.rb +5 -0
- data/spec/model/validations_spec.rb +11 -0
- metadata +85 -34
data/lib/sequel/model/errors.rb
CHANGED
|
@@ -40,7 +40,7 @@ module Sequel
|
|
|
40
40
|
def full_messages
|
|
41
41
|
inject([]) do |m, kv|
|
|
42
42
|
att, errors = *kv
|
|
43
|
-
errors.each {|e| m << "#{Array(att).join(ATTRIBUTE_JOINER)} #{e}"}
|
|
43
|
+
errors.each {|e| m << (e.is_a?(LiteralString) ? e : "#{Array(att).join(ATTRIBUTE_JOINER)} #{e}")}
|
|
44
44
|
m
|
|
45
45
|
end
|
|
46
46
|
end
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
module Sequel
|
|
2
2
|
# Exception class raised when +raise_on_save_failure+ is set and a before hook returns false
|
|
3
|
-
|
|
3
|
+
# or an around hook doesn't call super or yield.
|
|
4
|
+
class HookFailed < Error; end
|
|
5
|
+
|
|
6
|
+
# Deprecated alias for HookFailed, kept for backwards compatibility
|
|
7
|
+
BeforeHookFailed = HookFailed
|
|
4
8
|
|
|
5
9
|
# Exception class raised when +require_modification+ is set and an UPDATE or DELETE statement to modify the dataset doesn't
|
|
6
10
|
# modify a single row.
|
data/lib/sequel/model.rb
CHANGED
|
@@ -11,9 +11,9 @@ module Sequel
|
|
|
11
11
|
# with the +Database+ in +source+ to create the
|
|
12
12
|
# dataset to use)
|
|
13
13
|
# Dataset :: Sets the dataset for this model to +source+.
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
14
|
+
# other :: Sets the table name for this model to +source+. The
|
|
15
|
+
# class will use the default database for model
|
|
16
|
+
# classes in order to create the dataset.
|
|
17
17
|
#
|
|
18
18
|
# The purpose of this method is to set the dataset/database automatically
|
|
19
19
|
# for a model class, if the table name doesn't match the implicit
|
|
@@ -49,10 +49,11 @@ module Sequel
|
|
|
49
49
|
# called directly on the class. Model datasets return rows as model instances,
|
|
50
50
|
# which have fairly standard ORM instance behavior.
|
|
51
51
|
#
|
|
52
|
-
# <tt>Sequel::Model</tt> is built completely out of plugins
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
52
|
+
# <tt>Sequel::Model</tt> is built completely out of plugins. Plugins can override any class,
|
|
53
|
+
# instance, or dataset method defined by a previous plugin and call super to get the default
|
|
54
|
+
# behavior. By default, <tt>Sequel::Model</tt> loads two plugins, <tt>Sequel::Model</tt>
|
|
55
|
+
# (which is itself a plugin) for the base support, and <tt>Sequel::Model::Associations</tt>
|
|
56
|
+
# for the associations support.
|
|
56
57
|
#
|
|
57
58
|
# You can set the +SEQUEL_NO_ASSOCIATIONS+ constant or environment variable to
|
|
58
59
|
# make Sequel not load the associations plugin by default.
|
|
@@ -80,6 +81,11 @@ module Sequel
|
|
|
80
81
|
# +super+ on the first line of your method, so later hooks are called after earlier hooks.
|
|
81
82
|
AFTER_HOOKS = [:after_initialize, :after_create, :after_update, :after_save, :after_destroy, :after_validation]
|
|
82
83
|
|
|
84
|
+
# Hooks that are called around an action. If overridden, these methods must call super
|
|
85
|
+
# exactly once if the behavior they wrap is desired. The can be used to rescue exceptions
|
|
86
|
+
# raised by the code they wrap or ensure that some behavior is executed no matter what.
|
|
87
|
+
AROUND_HOOKS = [:around_create, :around_update, :around_save, :around_destroy, :around_validation]
|
|
88
|
+
|
|
83
89
|
# Empty instance methods to create that the user can override to get hook/callback behavior.
|
|
84
90
|
# Just like any other method defined by Sequel, if you override one of these, you should
|
|
85
91
|
# call +super+ to get the default behavior (while empty by default, they can also be defined
|
|
@@ -14,7 +14,7 @@ module Sequel
|
|
|
14
14
|
#
|
|
15
15
|
# Note that it uses the singular form of the association name. Also note
|
|
16
16
|
# that the setter both associates to new primary keys not in the assocation
|
|
17
|
-
# and
|
|
17
|
+
# and disassociates from primary keys not provided to the method.
|
|
18
18
|
#
|
|
19
19
|
# This plugin makes modifications directly to the underlying tables,
|
|
20
20
|
# it does not create or return any model objects, and therefore does
|
|
@@ -56,6 +56,7 @@ module Sequel
|
|
|
56
56
|
_join_table_dataset(opts).filter(opts[:left_key]=>send(opts[:left_primary_key])).select_map(opts[:right_key])
|
|
57
57
|
end
|
|
58
58
|
def_association_pks_setter(opts) do |pks|
|
|
59
|
+
pks = convert_pk_array(opts, pks)
|
|
59
60
|
checked_transaction do
|
|
60
61
|
ds = _join_table_dataset(opts).filter(opts[:left_key]=>send(opts[:left_primary_key]))
|
|
61
62
|
ds.exclude(opts[:right_key]=>pks).delete
|
|
@@ -71,17 +72,34 @@ module Sequel
|
|
|
71
72
|
super
|
|
72
73
|
return if opts[:type] == :one_to_one
|
|
73
74
|
def_association_pks_getter(opts) do
|
|
74
|
-
send(opts.dataset_method).select_map(opts
|
|
75
|
+
send(opts.dataset_method).select_map(opts.associated_class.primary_key)
|
|
75
76
|
end
|
|
76
77
|
def_association_pks_setter(opts) do |pks|
|
|
78
|
+
pks = convert_pk_array(opts, pks)
|
|
77
79
|
checked_transaction do
|
|
78
80
|
ds = send(opts.dataset_method)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
primary_key = opts.associated_class.primary_key
|
|
82
|
+
key = opts[:key]
|
|
83
|
+
ds.unfiltered.filter(primary_key=>pks).update(key=>pk)
|
|
84
|
+
ds.exclude(primary_key=>pks).update(key=>nil)
|
|
81
85
|
end
|
|
82
86
|
end
|
|
83
87
|
end
|
|
84
88
|
end
|
|
89
|
+
|
|
90
|
+
module InstanceMethods
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# If the associated class's primary key column type is integer,
|
|
94
|
+
# typecast all provided values to integer before using them.
|
|
95
|
+
def convert_pk_array(opts, pks)
|
|
96
|
+
if klass = opts.associated_class and sch = klass.db_schema and col = sch[klass.primary_key] and col[:type] == :integer
|
|
97
|
+
pks.map{|pk| model.db.typecast_value(:integer, pk)}
|
|
98
|
+
else
|
|
99
|
+
pks
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
85
103
|
end
|
|
86
104
|
end
|
|
87
105
|
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Sequel
|
|
2
|
+
module Plugins
|
|
3
|
+
# DefaultsSetter is a simple plugin that sets non-nil/NULL default values upon
|
|
4
|
+
# initialize:
|
|
5
|
+
#
|
|
6
|
+
# # column a default NULL
|
|
7
|
+
# # column b default 2
|
|
8
|
+
# album = Album.new.values # {:b => 2}
|
|
9
|
+
# album = Album.new(:a=>1, :b=>3).values # {:a => 1, :b => 3}
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
#
|
|
13
|
+
# # Make all model subclass instances set defaults (called before loading subclasses)
|
|
14
|
+
# Sequel::Model.plugin :defaults_setter
|
|
15
|
+
#
|
|
16
|
+
# # Make the Album class set defaults
|
|
17
|
+
# Album.plugin :defaults_setter
|
|
18
|
+
module DefaultsSetter
|
|
19
|
+
# Set the default values based on the model schema
|
|
20
|
+
def self.configure(model)
|
|
21
|
+
model.send(:set_default_values)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module ClassMethods
|
|
25
|
+
# The default values to set in initialize for this model. A hash with column symbol
|
|
26
|
+
# keys and default values. If the default values respond to +call+, it will be called
|
|
27
|
+
# to get the value, otherwise the value will be used directly. You can manually modify
|
|
28
|
+
# this hash to set specific default values, by default the ones will be parsed from the database.
|
|
29
|
+
attr_reader :default_values
|
|
30
|
+
|
|
31
|
+
# Set the default values when loading the dataset.
|
|
32
|
+
def set_dataset(*)
|
|
33
|
+
x = super
|
|
34
|
+
set_default_values
|
|
35
|
+
x
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def set_default_values
|
|
41
|
+
h = {}
|
|
42
|
+
@db_schema.each{|k, v| h[k] = v[:ruby_default] if v[:ruby_default]} if @db_schema
|
|
43
|
+
@default_values = h
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module InstanceMethods
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Set default values if they are not already set by the hash provided to initialize.
|
|
51
|
+
def initialize_set(h)
|
|
52
|
+
super
|
|
53
|
+
model.default_values.each{|k,v| self[k] = (v.respond_to?(:call) ? v.call : v) unless values.has_key?(k)}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -22,10 +22,7 @@ module Sequel
|
|
|
22
22
|
# Identity maps are thread-local and only persist for the duration of the block,
|
|
23
23
|
# so they should only be considered as a possible performance enhancer.
|
|
24
24
|
#
|
|
25
|
-
# The identity_map plugin is not compatible with the
|
|
26
|
-
# many_to_many and many_through_many associations. If you want to use the identity_map plugin,
|
|
27
|
-
# you should use +eager_graph+ instead of +eager+ for those associations. It is also
|
|
28
|
-
# not compatible with the eager loading in the +rcte_tree+ plugin.
|
|
25
|
+
# The identity_map plugin is not compatible with the eager loading in the +rcte_tree+ plugin.
|
|
29
26
|
#
|
|
30
27
|
# Usage:
|
|
31
28
|
#
|
|
@@ -37,6 +34,116 @@ module Sequel
|
|
|
37
34
|
# # would need to do Album.with_identity_map{} to use the identity map
|
|
38
35
|
module IdentityMap
|
|
39
36
|
module ClassMethods
|
|
37
|
+
# Override the default :eager_loader option for many_*_many associations to work
|
|
38
|
+
# with an identity_map. If the :eager_graph association option is used, you'll probably have to use
|
|
39
|
+
# :uniq=>true on the current association amd :cartesian_product_number=>2 on the association
|
|
40
|
+
# mentioned by :eager_graph, otherwise you'll end up with duplicates because the row proc will be
|
|
41
|
+
# getting called multiple times for the same object. If you do have duplicates and you use :eager_graph,
|
|
42
|
+
# they'll probably be lost. Making that work correctly would require changing a lot of the core
|
|
43
|
+
# architecture, such as how graphing and eager graphing work.
|
|
44
|
+
def associate(type, name, opts = {}, &block)
|
|
45
|
+
if opts[:eager_loader]
|
|
46
|
+
super
|
|
47
|
+
elsif type == :many_to_many
|
|
48
|
+
opts = super
|
|
49
|
+
el = opts[:eager_loader]
|
|
50
|
+
model = self
|
|
51
|
+
left_pk = opts[:left_primary_key]
|
|
52
|
+
uses_lcks = opts[:uses_left_composite_keys]
|
|
53
|
+
uses_rcks = opts[:uses_right_composite_keys]
|
|
54
|
+
right = opts[:right_key]
|
|
55
|
+
join_table = opts[:join_table]
|
|
56
|
+
left = opts[:left_key]
|
|
57
|
+
lcks = opts[:left_keys]
|
|
58
|
+
left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
|
|
59
|
+
opts[:eager_loader] = lambda do |eo|
|
|
60
|
+
return el.call(eo) unless model.identity_map
|
|
61
|
+
h = eo[:key_hash][left_pk]
|
|
62
|
+
eo[:rows].each{|object| object.associations[name] = []}
|
|
63
|
+
r = uses_rcks ? rcks.zip(opts.right_primary_keys) : [[right, opts.right_primary_key]]
|
|
64
|
+
l = uses_lcks ? [[lcks.map{|k| SQL::QualifiedIdentifier.new(join_table, k)}, h.keys]] : [[left, h.keys]]
|
|
65
|
+
|
|
66
|
+
# Replace the row proc to remove the left key alias before calling the previous row proc.
|
|
67
|
+
# Associate the value of the left key alias with the associated object (through its object_id).
|
|
68
|
+
# When loading the associated objects, lookup the left key alias value and associate the
|
|
69
|
+
# associated objects to the main objects if the left key alias value matches the left primary key
|
|
70
|
+
# value of the main object.
|
|
71
|
+
#
|
|
72
|
+
# The deleting of the left key alias from the hash before calling the previous row proc
|
|
73
|
+
# is necessary when an identity map is used, otherwise if the same associated object is returned more than
|
|
74
|
+
# once for the association, it won't know which of current objects to associate it to.
|
|
75
|
+
ds = opts.associated_class.inner_join(join_table, r + l)
|
|
76
|
+
pr = ds.row_proc
|
|
77
|
+
h2 = {}
|
|
78
|
+
ds.row_proc = proc do |hash|
|
|
79
|
+
hash_key = if uses_lcks
|
|
80
|
+
left_key_alias.map{|k| hash.delete(k)}
|
|
81
|
+
else
|
|
82
|
+
hash.delete(left_key_alias)
|
|
83
|
+
end
|
|
84
|
+
obj = pr.call(hash)
|
|
85
|
+
(h2[obj.object_id] ||= []) << hash_key
|
|
86
|
+
obj
|
|
87
|
+
end
|
|
88
|
+
model.eager_loading_dataset(opts, ds, Array(opts.select), eo[:associations], eo) .all do |assoc_record|
|
|
89
|
+
if hash_keys = h2.delete(assoc_record.object_id)
|
|
90
|
+
hash_keys.each do |hash_key|
|
|
91
|
+
if objects = h[hash_key]
|
|
92
|
+
objects.each{|object| object.associations[name].push(assoc_record)}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
opts
|
|
99
|
+
elsif type == :many_through_many
|
|
100
|
+
opts = super
|
|
101
|
+
el = opts[:eager_loader]
|
|
102
|
+
model = self
|
|
103
|
+
left_pk = opts[:left_primary_key]
|
|
104
|
+
left_key = opts[:left_key]
|
|
105
|
+
uses_lcks = opts[:uses_left_composite_keys]
|
|
106
|
+
left_keys = Array(left_key)
|
|
107
|
+
left_key_alias = opts[:left_key_alias]
|
|
108
|
+
opts[:eager_loader] = lambda do |eo|
|
|
109
|
+
return el.call(eo) unless model.identity_map
|
|
110
|
+
h = eo[:key_hash][left_pk]
|
|
111
|
+
eo[:rows].each{|object| object.associations[name] = []}
|
|
112
|
+
ds = opts.associated_class
|
|
113
|
+
opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
|
|
114
|
+
ft = opts[:final_reverse_edge]
|
|
115
|
+
conds = uses_lcks ? [[left_keys.map{|k| SQL::QualifiedIdentifier.new(ft[:table], k)}, h.keys]] : [[left_key, h.keys]]
|
|
116
|
+
|
|
117
|
+
# See above comment in many_to_many eager_loader
|
|
118
|
+
ds = ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + conds, :table_alias=>ft[:alias])
|
|
119
|
+
pr = ds.row_proc
|
|
120
|
+
h2 = {}
|
|
121
|
+
ds.row_proc = proc do |hash|
|
|
122
|
+
hash_key = if uses_lcks
|
|
123
|
+
left_key_alias.map{|k| hash.delete(k)}
|
|
124
|
+
else
|
|
125
|
+
hash.delete(left_key_alias)
|
|
126
|
+
end
|
|
127
|
+
obj = pr.call(hash)
|
|
128
|
+
(h2[obj.object_id] ||= []) << hash_key
|
|
129
|
+
obj
|
|
130
|
+
end
|
|
131
|
+
model.eager_loading_dataset(opts, ds, Array(opts.select), eo[:associations], eo).all do |assoc_record|
|
|
132
|
+
if hash_keys = h2.delete(assoc_record.object_id)
|
|
133
|
+
hash_keys.each do |hash_key|
|
|
134
|
+
if objects = h[hash_key]
|
|
135
|
+
objects.each{|object| object.associations[name].push(assoc_record)}
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
opts
|
|
142
|
+
else
|
|
143
|
+
super
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
40
147
|
# Returns the current thread-local identity map. Should be a hash if
|
|
41
148
|
# there is an active identity map, and nil otherwise.
|
|
42
149
|
def identity_map
|
|
@@ -121,9 +228,9 @@ module Sequel
|
|
|
121
228
|
# key option has a value and the association uses the primary key of
|
|
122
229
|
# the associated class as the :primary_key option, check the identity
|
|
123
230
|
# map for the associated object and return it if present.
|
|
124
|
-
def _load_associated_objects(opts)
|
|
231
|
+
def _load_associated_objects(opts, dynamic_opts={})
|
|
125
232
|
klass = opts.associated_class
|
|
126
|
-
if klass.respond_to?(:identity_map) && idm = klass.identity_map and opts[:type] == :many_to_one and opts
|
|
233
|
+
if !dynamic_opts[:callback] && klass.respond_to?(:identity_map) && idm = klass.identity_map and opts[:type] == :many_to_one and opts.primary_key == klass.primary_key and
|
|
127
234
|
opts[:key] and pk = _associated_object_pk(opts[:key]) and o = idm[klass.identity_map_key(pk)]
|
|
128
235
|
o
|
|
129
236
|
else
|
|
@@ -58,6 +58,19 @@ module Sequel
|
|
|
58
58
|
self[:uses_left_composite_keys] ? (0...self[:through].first[:left].length).map{|i| :"x_foreign_key_#{i}_x"} : :x_foreign_key_x
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# The hash key to use for the eager loading predicate (left side of IN (1, 2, 3))
|
|
62
|
+
def eager_loading_predicate_key
|
|
63
|
+
self[:eager_loading_predicate_key] ||= begin
|
|
64
|
+
calculate_edges
|
|
65
|
+
e = self[:edges].first
|
|
66
|
+
if self[:uses_left_composite_keys]
|
|
67
|
+
e[:right].map{|k| SQL::QualifiedIdentifier.new(e[:table], k)}
|
|
68
|
+
else
|
|
69
|
+
SQL::QualifiedIdentifier.new(e[:table], e[:right])
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
61
74
|
# The list of joins to use when eager graphing
|
|
62
75
|
def edges
|
|
63
76
|
self[:edges] || calculate_edges || self[:edges]
|
|
@@ -116,6 +129,7 @@ module Sequel
|
|
|
116
129
|
nil
|
|
117
130
|
end
|
|
118
131
|
end
|
|
132
|
+
|
|
119
133
|
module ClassMethods
|
|
120
134
|
# Create a many_through_many association. Arguments:
|
|
121
135
|
# * name - Same as associate, the name of the association.
|
|
@@ -181,13 +195,28 @@ module Sequel
|
|
|
181
195
|
left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
|
|
182
196
|
opts[:eager_loader] ||= lambda do |eo|
|
|
183
197
|
h = eo[:key_hash][left_pk]
|
|
184
|
-
|
|
198
|
+
rows = eo[:rows]
|
|
199
|
+
rows.each{|object| object.associations[name] = []}
|
|
185
200
|
ds = opts.associated_class
|
|
186
201
|
opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
|
|
187
202
|
ft = opts[:final_reverse_edge]
|
|
188
203
|
conds = uses_lcks ? [[left_keys.map{|k| SQL::QualifiedIdentifier.new(ft[:table], k)}, h.keys]] : [[left_key, h.keys]]
|
|
189
204
|
ds = ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + conds, :table_alias=>ft[:alias])
|
|
190
|
-
model.eager_loading_dataset(opts, ds, Array(opts.select), eo[:associations], eo)
|
|
205
|
+
ds = model.eager_loading_dataset(opts, ds, Array(opts.select), eo[:associations], eo)
|
|
206
|
+
case opts.eager_limit_strategy
|
|
207
|
+
when :window_function
|
|
208
|
+
delete_rn = true
|
|
209
|
+
rn = ds.row_number_column
|
|
210
|
+
ds = apply_window_function_eager_limit_strategy(ds, opts)
|
|
211
|
+
when :correlated_subquery
|
|
212
|
+
ds = apply_correlated_subquery_eager_limit_strategy(ds, opts) do |xds|
|
|
213
|
+
dsa = ds.send(:dataset_alias, 2)
|
|
214
|
+
opts.reverse_edges.each{|t| xds = xds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
|
|
215
|
+
xds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + left_keys.map{|k| [k, SQL::QualifiedIdentifier.new(ft[:table], k)]}, :table_alias=>dsa)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
ds.all do |assoc_record|
|
|
219
|
+
assoc_record.values.delete(rn) if delete_rn
|
|
191
220
|
hash_key = if uses_lcks
|
|
192
221
|
left_key_alias.map{|k| assoc_record.values.delete(k)}
|
|
193
222
|
else
|
|
@@ -196,6 +225,10 @@ module Sequel
|
|
|
196
225
|
next unless objects = h[hash_key]
|
|
197
226
|
objects.each{|object| object.associations[name].push(assoc_record)}
|
|
198
227
|
end
|
|
228
|
+
if opts.eager_limit_strategy == :ruby
|
|
229
|
+
limit, offset = opts.limit_and_offset
|
|
230
|
+
rows.each{|o| o.associations[name] = o.associations[name].slice(offset||0, limit) || []}
|
|
231
|
+
end
|
|
199
232
|
end
|
|
200
233
|
|
|
201
234
|
join_type = opts[:graph_join_type]
|
|
@@ -204,19 +237,48 @@ module Sequel
|
|
|
204
237
|
only_conditions = opts[:graph_only_conditions]
|
|
205
238
|
use_only_conditions = opts.include?(:graph_only_conditions)
|
|
206
239
|
conditions = opts[:graph_conditions]
|
|
207
|
-
opts[:eager_grapher] ||= proc do |
|
|
208
|
-
|
|
240
|
+
opts[:eager_grapher] ||= proc do |eo|
|
|
241
|
+
ds = eo[:self]
|
|
242
|
+
iq = eo[:implicit_qualifier]
|
|
209
243
|
opts.edges.each do |t|
|
|
210
244
|
ds = ds.graph(t[:table], t.fetch(:only_conditions, (Array(t[:right]).zip(Array(t[:left])) + t[:conditions])), :select=>false, :table_alias=>ds.unused_table_alias(t[:table]), :join_type=>t[:join_type], :implicit_qualifier=>iq, &t[:block])
|
|
211
245
|
iq = nil
|
|
212
246
|
end
|
|
213
247
|
fe = opts[:final_edge]
|
|
214
|
-
ds.graph(opts.associated_class, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>
|
|
248
|
+
ds.graph(opts.associated_class, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>eo[:table_alias], :join_type=>join_type, &graph_block)
|
|
215
249
|
end
|
|
216
250
|
|
|
217
251
|
def_association_dataset_methods(opts)
|
|
218
252
|
end
|
|
219
253
|
end
|
|
254
|
+
|
|
255
|
+
module DatasetMethods
|
|
256
|
+
private
|
|
257
|
+
|
|
258
|
+
# Use a subquery to filter rows to those related to the given associated object
|
|
259
|
+
def many_through_many_association_filter_expression(op, ref, obj)
|
|
260
|
+
lpks = ref[:left_primary_keys]
|
|
261
|
+
lpks = lpks.first if lpks.length == 1
|
|
262
|
+
edges = ref.edges
|
|
263
|
+
first, rest = edges.first, edges[1..-1]
|
|
264
|
+
last = edges.last
|
|
265
|
+
ds = model.db[first[:table]].select(*Array(first[:right]).map{|x| ::Sequel::SQL::QualifiedIdentifier.new(first[:table], x)})
|
|
266
|
+
rest.each{|e| ds = ds.join(e[:table], e.fetch(:only_conditions, (Array(e[:right]).zip(Array(e[:left])) + e[:conditions])), :table_alias=>ds.unused_table_alias(e[:table]), &e[:block])}
|
|
267
|
+
last_alias = if rest.empty?
|
|
268
|
+
first[:table]
|
|
269
|
+
else
|
|
270
|
+
last_join = ds.opts[:join].last
|
|
271
|
+
last_join.table_alias || last_join.table
|
|
272
|
+
end
|
|
273
|
+
exp = association_filter_key_expression(Array(ref[:final_edge][:left]).map{|x| ::Sequel::SQL::QualifiedIdentifier.new(last_alias, x)}, ref.right_primary_keys, obj)
|
|
274
|
+
if exp == SQL::Constants::FALSE
|
|
275
|
+
association_filter_handle_inversion(op, exp, Array(lpks))
|
|
276
|
+
else
|
|
277
|
+
ds = ds.where(exp).exclude(SQL::BooleanExpression.from_value_pairs(ds.opts[:select].zip([]), :OR))
|
|
278
|
+
association_filter_handle_inversion(op, SQL::BooleanExpression.from_value_pairs(lpks=>ds), Array(lpks))
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
220
282
|
end
|
|
221
283
|
end
|
|
222
284
|
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
module Sequel
|
|
2
|
+
module Plugins
|
|
3
|
+
# The prepared_statements plugin modifies the model to use prepared statements for
|
|
4
|
+
# instance level deletes and saves, as well as class level lookups by
|
|
5
|
+
# primary key.
|
|
6
|
+
#
|
|
7
|
+
# Note that this plugin is unsafe in some circumstances, as it can allow up to
|
|
8
|
+
# 2^N prepared statements to be created for each type of insert and update query, where
|
|
9
|
+
# N is the number of colums in the table. It is recommended that you use the
|
|
10
|
+
# +prepared_statements_safe+ plugin in addition to this plugin to reduce the number
|
|
11
|
+
# of prepared statements that can be created, unless you tightly control how your
|
|
12
|
+
# model instances are saved.
|
|
13
|
+
#
|
|
14
|
+
# This plugin probably does not work correctly with the instance filters plugin.
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
#
|
|
18
|
+
# # Make all model subclasses use prepared statements (called before loading subclasses)
|
|
19
|
+
# Sequel::Model.plugin :prepared_statements
|
|
20
|
+
#
|
|
21
|
+
# # Make the Album class use prepared statements
|
|
22
|
+
# Album.plugin :prepared_statements
|
|
23
|
+
module PreparedStatements
|
|
24
|
+
# Synchronize access to the integer sequence so that no two calls get the same integer.
|
|
25
|
+
MUTEX = Mutex.new
|
|
26
|
+
|
|
27
|
+
i = 0
|
|
28
|
+
# This plugin names prepared statements uniquely using an integer sequence, this
|
|
29
|
+
# lambda returns the next integer to use.
|
|
30
|
+
NEXT = lambda{MUTEX.synchronize{i += 1}}
|
|
31
|
+
|
|
32
|
+
# Setup the datastructure used to hold the prepared statements in the model.
|
|
33
|
+
def self.apply(model)
|
|
34
|
+
model.instance_variable_set(:@prepared_statements, :insert=>{}, :insert_select=>{}, :update=>{}, :lookup_sql=>{})
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module ClassMethods
|
|
38
|
+
# Setup the datastructure used to hold the prepared statements in the subclass.
|
|
39
|
+
def inherited(subclass)
|
|
40
|
+
super
|
|
41
|
+
subclass.instance_variable_set(:@prepared_statements, :insert=>{}, :insert_select=>{}, :update=>{}, :lookup_sql=>{})
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Create a prepared statement based on the given dataset with a unique name for the given
|
|
47
|
+
# type of query and values.
|
|
48
|
+
def prepare_statement(ds, type, vals={})
|
|
49
|
+
ds.prepare(type, :"smpsp_#{NEXT.call}", vals)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Return a sorted array of columns for use as a hash key.
|
|
53
|
+
def prepared_columns(cols)
|
|
54
|
+
RUBY_VERSION >= '1.9' ? cols.sort : cols.sort_by{|c| c.to_s}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Return a prepared statement that can be used to delete a row from this model's dataset.
|
|
58
|
+
def prepared_delete
|
|
59
|
+
@prepared_statements[:delete] ||= prepare_statement(filter(prepared_statement_key_array(primary_key)), :delete)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Return a prepared statement that can be used to insert a row using the given columns.
|
|
63
|
+
def prepared_insert(cols)
|
|
64
|
+
@prepared_statements[:insert][prepared_columns(cols)] ||= prepare_statement(dataset, :insert, prepared_statement_key_hash(cols))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Return a prepared statement that can be used to insert a row using the given columns
|
|
68
|
+
# and return that column values for the row created.
|
|
69
|
+
def prepared_insert_select(cols)
|
|
70
|
+
if dataset.supports_insert_select?
|
|
71
|
+
@prepared_statements[:insert_select][prepared_columns(cols)] ||= prepare_statement(naked.clone(:server=>dataset.opts.fetch(:server, :default)), :insert_select, prepared_statement_key_hash(cols))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Return a prepared statement that can be used to lookup a row solely based on the primary key.
|
|
76
|
+
def prepared_lookup
|
|
77
|
+
@prepared_statements[:lookup] ||= prepare_statement(filter(prepared_statement_key_array(primary_key)), :first)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Return a prepared statement that can be used to refresh a row to get new column values after insertion.
|
|
81
|
+
def prepared_refresh
|
|
82
|
+
@prepared_statements[:refresh] ||= prepare_statement(naked.clone(:server=>dataset.opts.fetch(:server, :default)).filter(prepared_statement_key_array(primary_key)), :first)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Return an array of two element arrays with the column symbol as the first entry and the
|
|
86
|
+
# placeholder symbol as the second entry.
|
|
87
|
+
def prepared_statement_key_array(keys)
|
|
88
|
+
Array(keys).map{|k| [k, :"$#{k}"]}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Return a hash mapping column symbols to placeholder symbols.
|
|
92
|
+
def prepared_statement_key_hash(keys)
|
|
93
|
+
Hash[*(prepared_statement_key_array(keys).flatten)]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Return a prepared statement that can be used to update row using the given columns.
|
|
97
|
+
def prepared_update(cols)
|
|
98
|
+
@prepared_statements[:update][prepared_columns(cols)] ||= prepare_statement(filter(prepared_statement_key_array(primary_key)), :update, prepared_statement_key_hash(cols))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Use a prepared statement to query the database for the row matching the given primary key.
|
|
102
|
+
def primary_key_lookup(pk)
|
|
103
|
+
prepared_lookup.call(primary_key_hash(pk))
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
module InstanceMethods
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# Use a prepared statement to delete the row.
|
|
111
|
+
def _delete_without_checking
|
|
112
|
+
model.send(:prepared_delete).call(pk_hash)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Use a prepared statement to insert the values into the model's dataset.
|
|
116
|
+
def _insert_raw(ds)
|
|
117
|
+
model.send(:prepared_insert, @values.keys).call(@values)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Use a prepared statement to insert the values into the model's dataset
|
|
121
|
+
# and return the new column values.
|
|
122
|
+
def _insert_select_raw(ds)
|
|
123
|
+
if ps = model.send(:prepared_insert_select, @values.keys)
|
|
124
|
+
ps.call(@values)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Use a prepared statement to refresh this model's column values.
|
|
129
|
+
def _refresh_get(ds)
|
|
130
|
+
model.send(:prepared_refresh).call(pk_hash)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Use a prepared statement to update this model's columns in the database.
|
|
134
|
+
def _update_without_checking(columns)
|
|
135
|
+
model.send(:prepared_update, columns.keys).call(columns.merge(pk_hash))
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Sequel
|
|
2
|
+
module Plugins
|
|
3
|
+
# The prepared_statements_associations plugin modifies the regular association
|
|
4
|
+
# load method to use a cached prepared statement to load the associations.
|
|
5
|
+
# It will not work on all associations, but it should skip the use of prepared
|
|
6
|
+
# statements for associations where it will not work, assuming you load the
|
|
7
|
+
# plugin before defining the associations.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
#
|
|
11
|
+
# # Make all model subclasses more safe when using prepared statements (called before loading subclasses)
|
|
12
|
+
# Sequel::Model.plugin :prepared_statements_associations
|
|
13
|
+
#
|
|
14
|
+
# # Make the Album class more safe when using prepared statements
|
|
15
|
+
# Album.plugin :prepared_statements_associations
|
|
16
|
+
module PreparedStatementsAssociations
|
|
17
|
+
# Synchronize access to the integer sequence so that no two calls get the same integer.
|
|
18
|
+
MUTEX = Mutex.new
|
|
19
|
+
|
|
20
|
+
i = 0
|
|
21
|
+
# This plugin names prepared statements uniquely using an integer sequence, this
|
|
22
|
+
# lambda returns the next integer to use.
|
|
23
|
+
NEXT = lambda{MUTEX.synchronize{i += 1}}
|
|
24
|
+
|
|
25
|
+
module ClassMethods
|
|
26
|
+
# Disable prepared statement use if a block is given, or the :dataset or :conditions
|
|
27
|
+
# options are used, or you are cloning an association.
|
|
28
|
+
def associate(type, name, opts = {}, &block)
|
|
29
|
+
if block || opts[:dataset] || opts[:conditions] || (opts[:clone] && association_reflection(opts[:clone])[:prepared_statement] == false)
|
|
30
|
+
opts = opts.merge(:prepared_statement=>false)
|
|
31
|
+
end
|
|
32
|
+
super(type, name, opts, &block)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module InstanceMethods
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Return a bound variable hash that maps the keys in +ks+ (qualified by the +table+)
|
|
40
|
+
# to the values of the results of sending the methods in +vs+.
|
|
41
|
+
def association_bound_variable_hash(table, ks, vs)
|
|
42
|
+
Hash[*ks.zip(vs).map{|k, v| [:"#{table}.#{k}", send(v)]}.flatten]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Given an association reflection, return a bound variable hash for the given
|
|
46
|
+
# association for this instance's values.
|
|
47
|
+
def association_bound_variables(opts)
|
|
48
|
+
case opts[:type]
|
|
49
|
+
when :many_to_one
|
|
50
|
+
association_bound_variable_hash(opts.associated_class.table_name, opts.primary_keys, opts[:keys])
|
|
51
|
+
when :one_to_many
|
|
52
|
+
association_bound_variable_hash(opts.associated_class.table_name, opts[:keys], opts[:primary_keys])
|
|
53
|
+
when :many_to_many
|
|
54
|
+
association_bound_variable_hash(opts[:join_table], opts[:left_keys], opts[:left_primary_keys])
|
|
55
|
+
when :many_through_many
|
|
56
|
+
opts.reverse_edges
|
|
57
|
+
association_bound_variable_hash(opts[:final_reverse_edge][:alias], Array(opts[:left_key]), opts[:left_primary_keys])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Given an association reflection, return and cache a prepared statement for this association such
|
|
62
|
+
# that, given appropriate bound variables, the prepared statement will work correctly for any
|
|
63
|
+
# instance.
|
|
64
|
+
def association_prepared_statement(opts)
|
|
65
|
+
opts[:prepared_statement] ||= _associated_dataset(opts, {}).unbind.first.prepare(opts.returns_array? ? :select : :first, :"smpsap_#{NEXT.call}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# If a prepared statement can be used to load the associated objects, execute it to retrieve them. Otherwise,
|
|
69
|
+
# fall back to the default implementation.
|
|
70
|
+
def _load_associated_objects(opts, dynamic_opts={})
|
|
71
|
+
if !opts.can_have_associated_objects?(self) || dynamic_opts[:callback] || (ps = opts[:prepared_statement]) == false
|
|
72
|
+
super
|
|
73
|
+
else
|
|
74
|
+
if bv = association_bound_variables(opts)
|
|
75
|
+
(ps || association_prepared_statement(opts)).call(bv)
|
|
76
|
+
else
|
|
77
|
+
super
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|