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.
Files changed (158) hide show
  1. data/CHANGELOG +294 -0
  2. data/README.rdoc +20 -6
  3. data/Rakefile +20 -15
  4. data/doc/association_basics.rdoc +210 -43
  5. data/doc/dataset_basics.rdoc +4 -4
  6. data/doc/mass_assignment.rdoc +54 -0
  7. data/doc/migration.rdoc +15 -538
  8. data/doc/model_hooks.rdoc +64 -27
  9. data/doc/opening_databases.rdoc +37 -10
  10. data/doc/prepared_statements.rdoc +16 -10
  11. data/doc/reflection.rdoc +8 -2
  12. data/doc/release_notes/3.22.0.txt +39 -0
  13. data/doc/release_notes/3.23.0.txt +172 -0
  14. data/doc/release_notes/3.24.0.txt +420 -0
  15. data/doc/release_notes/3.25.0.txt +88 -0
  16. data/doc/release_notes/3.26.0.txt +88 -0
  17. data/doc/release_notes/3.27.0.txt +82 -0
  18. data/doc/release_notes/3.28.0.txt +304 -0
  19. data/doc/schema_modification.rdoc +547 -0
  20. data/doc/testing.rdoc +106 -0
  21. data/doc/transactions.rdoc +97 -0
  22. data/doc/virtual_rows.rdoc +2 -2
  23. data/lib/sequel/adapters/ado.rb +12 -1
  24. data/lib/sequel/adapters/amalgalite.rb +4 -0
  25. data/lib/sequel/adapters/db2.rb +95 -58
  26. data/lib/sequel/adapters/do.rb +12 -0
  27. data/lib/sequel/adapters/firebird.rb +25 -203
  28. data/lib/sequel/adapters/ibmdb.rb +440 -0
  29. data/lib/sequel/adapters/informix.rb +4 -19
  30. data/lib/sequel/adapters/jdbc/as400.rb +0 -7
  31. data/lib/sequel/adapters/jdbc/db2.rb +49 -0
  32. data/lib/sequel/adapters/jdbc/firebird.rb +34 -0
  33. data/lib/sequel/adapters/jdbc/h2.rb +16 -5
  34. data/lib/sequel/adapters/jdbc/informix.rb +31 -0
  35. data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
  36. data/lib/sequel/adapters/jdbc/mssql.rb +0 -32
  37. data/lib/sequel/adapters/jdbc/mysql.rb +9 -0
  38. data/lib/sequel/adapters/jdbc/oracle.rb +2 -27
  39. data/lib/sequel/adapters/jdbc/postgresql.rb +6 -0
  40. data/lib/sequel/adapters/jdbc/sqlserver.rb +46 -0
  41. data/lib/sequel/adapters/jdbc/transactions.rb +34 -0
  42. data/lib/sequel/adapters/jdbc.rb +62 -29
  43. data/lib/sequel/adapters/mysql.rb +22 -139
  44. data/lib/sequel/adapters/mysql2.rb +9 -14
  45. data/lib/sequel/adapters/odbc/db2.rb +21 -0
  46. data/lib/sequel/adapters/odbc.rb +15 -3
  47. data/lib/sequel/adapters/oracle.rb +17 -1
  48. data/lib/sequel/adapters/postgres.rb +111 -16
  49. data/lib/sequel/adapters/shared/access.rb +21 -0
  50. data/lib/sequel/adapters/shared/db2.rb +290 -0
  51. data/lib/sequel/adapters/shared/firebird.rb +214 -0
  52. data/lib/sequel/adapters/shared/informix.rb +45 -0
  53. data/lib/sequel/adapters/shared/mssql.rb +85 -47
  54. data/lib/sequel/adapters/shared/mysql.rb +50 -7
  55. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +143 -0
  56. data/lib/sequel/adapters/shared/oracle.rb +0 -4
  57. data/lib/sequel/adapters/shared/postgres.rb +75 -43
  58. data/lib/sequel/adapters/shared/sqlite.rb +56 -8
  59. data/lib/sequel/adapters/sqlite.rb +12 -11
  60. data/lib/sequel/adapters/swift/mysql.rb +9 -0
  61. data/lib/sequel/adapters/tinytds.rb +139 -7
  62. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +55 -0
  63. data/lib/sequel/ast_transformer.rb +190 -0
  64. data/lib/sequel/connection_pool/threaded.rb +3 -2
  65. data/lib/sequel/connection_pool.rb +1 -1
  66. data/lib/sequel/core.rb +6 -5
  67. data/lib/sequel/database/connecting.rb +5 -5
  68. data/lib/sequel/database/dataset.rb +1 -1
  69. data/lib/sequel/database/dataset_defaults.rb +1 -1
  70. data/lib/sequel/database/logging.rb +1 -1
  71. data/lib/sequel/database/misc.rb +38 -17
  72. data/lib/sequel/database/query.rb +50 -19
  73. data/lib/sequel/database/schema_generator.rb +8 -5
  74. data/lib/sequel/database/schema_methods.rb +52 -27
  75. data/lib/sequel/dataset/actions.rb +167 -48
  76. data/lib/sequel/dataset/features.rb +57 -8
  77. data/lib/sequel/dataset/graph.rb +1 -1
  78. data/lib/sequel/dataset/misc.rb +39 -20
  79. data/lib/sequel/dataset/mutation.rb +3 -3
  80. data/lib/sequel/dataset/prepared_statements.rb +29 -14
  81. data/lib/sequel/dataset/query.rb +182 -32
  82. data/lib/sequel/dataset/sql.rb +31 -58
  83. data/lib/sequel/dataset.rb +8 -0
  84. data/lib/sequel/exceptions.rb +4 -0
  85. data/lib/sequel/extensions/columns_introspection.rb +61 -0
  86. data/lib/sequel/extensions/migration.rb +6 -4
  87. data/lib/sequel/extensions/to_dot.rb +95 -83
  88. data/lib/sequel/model/associations.rb +893 -309
  89. data/lib/sequel/model/base.rb +302 -105
  90. data/lib/sequel/model/errors.rb +1 -1
  91. data/lib/sequel/model/exceptions.rb +5 -1
  92. data/lib/sequel/model.rb +13 -7
  93. data/lib/sequel/plugins/association_pks.rb +22 -4
  94. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  95. data/lib/sequel/plugins/identity_map.rb +113 -6
  96. data/lib/sequel/plugins/many_through_many.rb +67 -5
  97. data/lib/sequel/plugins/prepared_statements.rb +140 -0
  98. data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
  99. data/lib/sequel/plugins/prepared_statements_safe.rb +82 -0
  100. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  101. data/lib/sequel/plugins/serialization_modification_detection.rb +51 -0
  102. data/lib/sequel/plugins/sharding.rb +12 -20
  103. data/lib/sequel/plugins/single_table_inheritance.rb +2 -0
  104. data/lib/sequel/plugins/update_primary_key.rb +1 -1
  105. data/lib/sequel/plugins/xml_serializer.rb +3 -3
  106. data/lib/sequel/sql.rb +107 -51
  107. data/lib/sequel/timezones.rb +12 -3
  108. data/lib/sequel/version.rb +1 -1
  109. data/spec/adapters/db2_spec.rb +146 -0
  110. data/spec/adapters/mssql_spec.rb +36 -0
  111. data/spec/adapters/mysql_spec.rb +36 -19
  112. data/spec/adapters/postgres_spec.rb +115 -28
  113. data/spec/adapters/spec_helper.rb +6 -0
  114. data/spec/adapters/sqlite_spec.rb +11 -0
  115. data/spec/core/connection_pool_spec.rb +62 -77
  116. data/spec/core/database_spec.rb +244 -287
  117. data/spec/core/dataset_spec.rb +383 -34
  118. data/spec/core/expression_filters_spec.rb +159 -41
  119. data/spec/core/schema_spec.rb +326 -3
  120. data/spec/core/spec_helper.rb +45 -0
  121. data/spec/extensions/association_pks_spec.rb +38 -0
  122. data/spec/extensions/columns_introspection_spec.rb +91 -0
  123. data/spec/extensions/defaults_setter_spec.rb +64 -0
  124. data/spec/extensions/identity_map_spec.rb +162 -0
  125. data/spec/extensions/many_through_many_spec.rb +195 -20
  126. data/spec/extensions/migration_spec.rb +17 -17
  127. data/spec/extensions/nested_attributes_spec.rb +1 -0
  128. data/spec/extensions/prepared_statements_associations_spec.rb +126 -0
  129. data/spec/extensions/prepared_statements_safe_spec.rb +79 -0
  130. data/spec/extensions/prepared_statements_spec.rb +72 -0
  131. data/spec/extensions/prepared_statements_with_pk_spec.rb +38 -0
  132. data/spec/extensions/schema_dumper_spec.rb +2 -2
  133. data/spec/extensions/schema_spec.rb +12 -20
  134. data/spec/extensions/serialization_modification_detection_spec.rb +36 -0
  135. data/spec/extensions/single_table_inheritance_spec.rb +11 -0
  136. data/spec/extensions/spec_helper.rb +3 -1
  137. data/spec/extensions/to_dot_spec.rb +3 -5
  138. data/spec/extensions/xml_serializer_spec.rb +16 -4
  139. data/spec/integration/associations_test.rb +405 -15
  140. data/spec/integration/database_test.rb +4 -2
  141. data/spec/integration/dataset_test.rb +240 -20
  142. data/spec/integration/plugin_test.rb +142 -5
  143. data/spec/integration/prepared_statement_test.rb +174 -95
  144. data/spec/integration/schema_test.rb +128 -16
  145. data/spec/integration/spec_helper.rb +15 -0
  146. data/spec/integration/transaction_test.rb +40 -0
  147. data/spec/integration/type_test.rb +16 -2
  148. data/spec/model/association_reflection_spec.rb +91 -0
  149. data/spec/model/associations_spec.rb +476 -5
  150. data/spec/model/base_spec.rb +91 -1
  151. data/spec/model/eager_loading_spec.rb +519 -31
  152. data/spec/model/hooks_spec.rb +161 -0
  153. data/spec/model/model_spec.rb +89 -2
  154. data/spec/model/plugins_spec.rb +17 -0
  155. data/spec/model/record_spec.rb +184 -12
  156. data/spec/model/spec_helper.rb +5 -0
  157. data/spec/model/validations_spec.rb +11 -0
  158. metadata +85 -34
@@ -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
- class BeforeHookFailed < Error; end
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
- # Symbol :: 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.
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, the only method not part of a
53
- # plugin is the plugin method itself. Plugins can override any class, instance, or
54
- # dataset method defined by a previous plugin and call super to get the default
55
- # behavior.
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 disassociated from primary keys not provided to the method.
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[:primary_key])
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
- ds.unfiltered.filter(opts[:primary_key]=>pks).update(opts[:key]=>pk)
80
- ds.exclude(opts[:primary_key]=>pks).update(opts[:key]=>nil)
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 standard eager loading of
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[:primary_key] == klass.primary_key and
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
- eo[:rows].each{|object| object.associations[name] = []}
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).all do |assoc_record|
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 |ds, assoc_alias, table_alias|
208
- iq = table_alias
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=>assoc_alias, :join_type=>join_type, &graph_block)
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