sequel 3.21.0 → 3.32.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 (234) hide show
  1. data/CHANGELOG +539 -3
  2. data/MIT-LICENSE +1 -1
  3. data/README.rdoc +20 -6
  4. data/Rakefile +37 -19
  5. data/bin/sequel +1 -5
  6. data/doc/advanced_associations.rdoc +17 -0
  7. data/doc/association_basics.rdoc +293 -43
  8. data/doc/dataset_basics.rdoc +4 -4
  9. data/doc/dataset_filtering.rdoc +8 -0
  10. data/doc/mass_assignment.rdoc +54 -0
  11. data/doc/migration.rdoc +15 -538
  12. data/doc/model_hooks.rdoc +72 -27
  13. data/doc/opening_databases.rdoc +89 -50
  14. data/doc/prepared_statements.rdoc +40 -13
  15. data/doc/reflection.rdoc +8 -2
  16. data/doc/release_notes/3.22.0.txt +39 -0
  17. data/doc/release_notes/3.23.0.txt +172 -0
  18. data/doc/release_notes/3.24.0.txt +420 -0
  19. data/doc/release_notes/3.25.0.txt +88 -0
  20. data/doc/release_notes/3.26.0.txt +88 -0
  21. data/doc/release_notes/3.27.0.txt +82 -0
  22. data/doc/release_notes/3.28.0.txt +304 -0
  23. data/doc/release_notes/3.29.0.txt +459 -0
  24. data/doc/release_notes/3.30.0.txt +135 -0
  25. data/doc/release_notes/3.31.0.txt +146 -0
  26. data/doc/release_notes/3.32.0.txt +202 -0
  27. data/doc/schema_modification.rdoc +547 -0
  28. data/doc/sharding.rdoc +7 -1
  29. data/doc/testing.rdoc +129 -0
  30. data/doc/transactions.rdoc +137 -0
  31. data/doc/virtual_rows.rdoc +2 -2
  32. data/lib/sequel/adapters/ado/mssql.rb +18 -6
  33. data/lib/sequel/adapters/ado.rb +40 -18
  34. data/lib/sequel/adapters/amalgalite.rb +17 -9
  35. data/lib/sequel/adapters/db2.rb +175 -86
  36. data/lib/sequel/adapters/dbi.rb +15 -15
  37. data/lib/sequel/adapters/do/mysql.rb +5 -7
  38. data/lib/sequel/adapters/do/postgres.rb +0 -5
  39. data/lib/sequel/adapters/do/sqlite.rb +0 -5
  40. data/lib/sequel/adapters/do.rb +17 -36
  41. data/lib/sequel/adapters/firebird.rb +27 -208
  42. data/lib/sequel/adapters/ibmdb.rb +448 -0
  43. data/lib/sequel/adapters/informix.rb +6 -23
  44. data/lib/sequel/adapters/jdbc/as400.rb +16 -34
  45. data/lib/sequel/adapters/jdbc/db2.rb +52 -0
  46. data/lib/sequel/adapters/jdbc/derby.rb +260 -0
  47. data/lib/sequel/adapters/jdbc/firebird.rb +29 -0
  48. data/lib/sequel/adapters/jdbc/h2.rb +66 -30
  49. data/lib/sequel/adapters/jdbc/hsqldb.rb +177 -0
  50. data/lib/sequel/adapters/jdbc/informix.rb +26 -0
  51. data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
  52. data/lib/sequel/adapters/jdbc/mssql.rb +3 -33
  53. data/lib/sequel/adapters/jdbc/mysql.rb +11 -11
  54. data/lib/sequel/adapters/jdbc/oracle.rb +88 -25
  55. data/lib/sequel/adapters/jdbc/postgresql.rb +9 -13
  56. data/lib/sequel/adapters/jdbc/sqlite.rb +2 -6
  57. data/lib/sequel/adapters/jdbc/sqlserver.rb +71 -0
  58. data/lib/sequel/adapters/jdbc/transactions.rb +83 -0
  59. data/lib/sequel/adapters/jdbc.rb +231 -78
  60. data/lib/sequel/adapters/mock.rb +339 -0
  61. data/lib/sequel/adapters/mysql.rb +85 -184
  62. data/lib/sequel/adapters/mysql2.rb +26 -25
  63. data/lib/sequel/adapters/odbc/db2.rb +17 -0
  64. data/lib/sequel/adapters/odbc/mssql.rb +1 -6
  65. data/lib/sequel/adapters/odbc.rb +28 -9
  66. data/lib/sequel/adapters/openbase.rb +3 -5
  67. data/lib/sequel/adapters/oracle.rb +355 -53
  68. data/lib/sequel/adapters/postgres.rb +194 -33
  69. data/lib/sequel/adapters/shared/access.rb +32 -9
  70. data/lib/sequel/adapters/shared/db2.rb +328 -0
  71. data/lib/sequel/adapters/shared/firebird.rb +221 -0
  72. data/lib/sequel/adapters/shared/informix.rb +53 -0
  73. data/lib/sequel/adapters/shared/mssql.rb +278 -107
  74. data/lib/sequel/adapters/shared/mysql.rb +203 -65
  75. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +149 -0
  76. data/lib/sequel/adapters/shared/oracle.rb +237 -46
  77. data/lib/sequel/adapters/shared/postgres.rb +214 -82
  78. data/lib/sequel/adapters/shared/progress.rb +5 -8
  79. data/lib/sequel/adapters/shared/sqlite.rb +200 -50
  80. data/lib/sequel/adapters/sqlite.rb +29 -21
  81. data/lib/sequel/adapters/swift/mysql.rb +12 -7
  82. data/lib/sequel/adapters/swift/postgres.rb +0 -5
  83. data/lib/sequel/adapters/swift/sqlite.rb +8 -6
  84. data/lib/sequel/adapters/swift.rb +5 -5
  85. data/lib/sequel/adapters/tinytds.rb +160 -19
  86. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +66 -0
  87. data/lib/sequel/adapters/utils/stored_procedures.rb +1 -11
  88. data/lib/sequel/ast_transformer.rb +190 -0
  89. data/lib/sequel/connection_pool/threaded.rb +3 -2
  90. data/lib/sequel/connection_pool.rb +1 -1
  91. data/lib/sequel/core.rb +68 -5
  92. data/lib/sequel/database/connecting.rb +5 -6
  93. data/lib/sequel/database/dataset.rb +4 -4
  94. data/lib/sequel/database/dataset_defaults.rb +59 -1
  95. data/lib/sequel/database/logging.rb +1 -1
  96. data/lib/sequel/database/misc.rb +104 -19
  97. data/lib/sequel/database/query.rb +163 -59
  98. data/lib/sequel/database/schema_generator.rb +17 -7
  99. data/lib/sequel/database/schema_methods.rb +59 -29
  100. data/lib/sequel/dataset/actions.rb +240 -76
  101. data/lib/sequel/dataset/features.rb +91 -8
  102. data/lib/sequel/dataset/graph.rb +38 -11
  103. data/lib/sequel/dataset/misc.rb +50 -23
  104. data/lib/sequel/dataset/mutation.rb +12 -7
  105. data/lib/sequel/dataset/prepared_statements.rb +59 -27
  106. data/lib/sequel/dataset/query.rb +236 -43
  107. data/lib/sequel/dataset/sql.rb +676 -267
  108. data/lib/sequel/dataset.rb +8 -0
  109. data/lib/sequel/exceptions.rb +4 -0
  110. data/lib/sequel/extensions/columns_introspection.rb +61 -0
  111. data/lib/sequel/extensions/migration.rb +6 -4
  112. data/lib/sequel/extensions/named_timezones.rb +5 -0
  113. data/lib/sequel/extensions/query.rb +1 -1
  114. data/lib/sequel/extensions/thread_local_timezones.rb +1 -1
  115. data/lib/sequel/extensions/to_dot.rb +95 -83
  116. data/lib/sequel/model/associations.rb +1081 -344
  117. data/lib/sequel/model/base.rb +420 -133
  118. data/lib/sequel/model/errors.rb +1 -1
  119. data/lib/sequel/model/exceptions.rb +5 -1
  120. data/lib/sequel/model.rb +20 -10
  121. data/lib/sequel/plugins/active_model.rb +13 -1
  122. data/lib/sequel/plugins/association_pks.rb +22 -4
  123. data/lib/sequel/plugins/class_table_inheritance.rb +4 -4
  124. data/lib/sequel/plugins/composition.rb +43 -10
  125. data/lib/sequel/plugins/dataset_associations.rb +100 -0
  126. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  127. data/lib/sequel/plugins/force_encoding.rb +6 -6
  128. data/lib/sequel/plugins/identity_map.rb +114 -7
  129. data/lib/sequel/plugins/many_through_many.rb +68 -7
  130. data/lib/sequel/plugins/nested_attributes.rb +65 -10
  131. data/lib/sequel/plugins/prepared_statements.rb +151 -0
  132. data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
  133. data/lib/sequel/plugins/prepared_statements_safe.rb +82 -0
  134. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  135. data/lib/sequel/plugins/rcte_tree.rb +29 -15
  136. data/lib/sequel/plugins/serialization.rb +98 -49
  137. data/lib/sequel/plugins/serialization_modification_detection.rb +63 -0
  138. data/lib/sequel/plugins/sharding.rb +21 -54
  139. data/lib/sequel/plugins/single_table_inheritance.rb +3 -1
  140. data/lib/sequel/plugins/typecast_on_load.rb +9 -12
  141. data/lib/sequel/plugins/update_primary_key.rb +2 -2
  142. data/lib/sequel/plugins/xml_serializer.rb +3 -3
  143. data/lib/sequel/sql.rb +109 -52
  144. data/lib/sequel/timezones.rb +57 -38
  145. data/lib/sequel/version.rb +1 -1
  146. data/spec/adapters/db2_spec.rb +146 -0
  147. data/spec/adapters/mssql_spec.rb +135 -30
  148. data/spec/adapters/mysql_spec.rb +244 -212
  149. data/spec/adapters/oracle_spec.rb +67 -77
  150. data/spec/adapters/postgres_spec.rb +267 -78
  151. data/spec/adapters/spec_helper.rb +6 -5
  152. data/spec/adapters/sqlite_spec.rb +35 -21
  153. data/spec/core/connection_pool_spec.rb +71 -92
  154. data/spec/core/core_sql_spec.rb +21 -30
  155. data/spec/core/database_spec.rb +789 -512
  156. data/spec/core/dataset_spec.rb +1176 -1061
  157. data/spec/core/expression_filters_spec.rb +159 -42
  158. data/spec/core/mock_adapter_spec.rb +419 -0
  159. data/spec/core/object_graph_spec.rb +86 -114
  160. data/spec/core/schema_generator_spec.rb +3 -3
  161. data/spec/core/schema_spec.rb +298 -38
  162. data/spec/core/spec_helper.rb +6 -48
  163. data/spec/extensions/active_model_spec.rb +13 -0
  164. data/spec/extensions/association_autoreloading_spec.rb +1 -10
  165. data/spec/extensions/association_dependencies_spec.rb +2 -12
  166. data/spec/extensions/association_pks_spec.rb +66 -32
  167. data/spec/extensions/caching_spec.rb +23 -50
  168. data/spec/extensions/class_table_inheritance_spec.rb +31 -83
  169. data/spec/extensions/columns_introspection_spec.rb +91 -0
  170. data/spec/extensions/composition_spec.rb +18 -13
  171. data/spec/extensions/dataset_associations_spec.rb +199 -0
  172. data/spec/extensions/defaults_setter_spec.rb +64 -0
  173. data/spec/extensions/hook_class_methods_spec.rb +65 -91
  174. data/spec/extensions/identity_map_spec.rb +114 -22
  175. data/spec/extensions/instance_filters_spec.rb +11 -21
  176. data/spec/extensions/instance_hooks_spec.rb +53 -0
  177. data/spec/extensions/json_serializer_spec.rb +4 -5
  178. data/spec/extensions/lazy_attributes_spec.rb +16 -20
  179. data/spec/extensions/list_spec.rb +17 -39
  180. data/spec/extensions/many_through_many_spec.rb +283 -250
  181. data/spec/extensions/migration_spec.rb +35 -32
  182. data/spec/extensions/named_timezones_spec.rb +23 -3
  183. data/spec/extensions/nested_attributes_spec.rb +97 -88
  184. data/spec/extensions/optimistic_locking_spec.rb +9 -20
  185. data/spec/extensions/prepared_statements_associations_spec.rb +111 -0
  186. data/spec/extensions/prepared_statements_safe_spec.rb +61 -0
  187. data/spec/extensions/prepared_statements_spec.rb +53 -0
  188. data/spec/extensions/prepared_statements_with_pk_spec.rb +31 -0
  189. data/spec/extensions/pretty_table_spec.rb +1 -6
  190. data/spec/extensions/query_spec.rb +6 -0
  191. data/spec/extensions/rcte_tree_spec.rb +41 -43
  192. data/spec/extensions/schema_dumper_spec.rb +5 -8
  193. data/spec/extensions/schema_spec.rb +13 -21
  194. data/spec/extensions/serialization_modification_detection_spec.rb +72 -0
  195. data/spec/extensions/serialization_spec.rb +81 -43
  196. data/spec/extensions/sharding_spec.rb +66 -140
  197. data/spec/extensions/single_table_inheritance_spec.rb +18 -29
  198. data/spec/extensions/spec_helper.rb +13 -61
  199. data/spec/extensions/sql_expr_spec.rb +20 -60
  200. data/spec/extensions/tactical_eager_loading_spec.rb +9 -19
  201. data/spec/extensions/thread_local_timezones_spec.rb +22 -2
  202. data/spec/extensions/timestamps_spec.rb +6 -6
  203. data/spec/extensions/to_dot_spec.rb +4 -7
  204. data/spec/extensions/touch_spec.rb +13 -14
  205. data/spec/extensions/tree_spec.rb +11 -26
  206. data/spec/extensions/typecast_on_load_spec.rb +1 -6
  207. data/spec/extensions/update_primary_key_spec.rb +30 -24
  208. data/spec/extensions/validation_class_methods_spec.rb +30 -51
  209. data/spec/extensions/validation_helpers_spec.rb +16 -35
  210. data/spec/extensions/xml_serializer_spec.rb +16 -4
  211. data/spec/integration/associations_test.rb +661 -21
  212. data/spec/integration/database_test.rb +4 -2
  213. data/spec/integration/dataset_test.rb +500 -67
  214. data/spec/integration/eager_loader_test.rb +19 -21
  215. data/spec/integration/model_test.rb +80 -1
  216. data/spec/integration/plugin_test.rb +367 -119
  217. data/spec/integration/prepared_statement_test.rb +222 -125
  218. data/spec/integration/schema_test.rb +217 -39
  219. data/spec/integration/spec_helper.rb +39 -30
  220. data/spec/integration/timezone_test.rb +38 -12
  221. data/spec/integration/transaction_test.rb +182 -5
  222. data/spec/integration/type_test.rb +17 -3
  223. data/spec/model/association_reflection_spec.rb +201 -7
  224. data/spec/model/associations_spec.rb +955 -662
  225. data/spec/model/base_spec.rb +275 -95
  226. data/spec/model/dataset_methods_spec.rb +7 -27
  227. data/spec/model/eager_loading_spec.rb +677 -676
  228. data/spec/model/hooks_spec.rb +349 -67
  229. data/spec/model/model_spec.rb +222 -168
  230. data/spec/model/plugins_spec.rb +24 -13
  231. data/spec/model/record_spec.rb +381 -218
  232. data/spec/model/spec_helper.rb +13 -71
  233. data/spec/model/validations_spec.rb +11 -0
  234. metadata +117 -73
@@ -58,6 +58,16 @@ 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
+ f = self[:final_reverse_edge]
67
+ qualify(f[:alias], e[:right])
68
+ end
69
+ end
70
+
61
71
  # The list of joins to use when eager graphing
62
72
  def edges
63
73
  self[:edges] || calculate_edges || self[:edges]
@@ -116,6 +126,7 @@ module Sequel
116
126
  nil
117
127
  end
118
128
  end
129
+
119
130
  module ClassMethods
120
131
  # Create a many_through_many association. Arguments:
121
132
  # * name - Same as associate, the name of the association.
@@ -181,13 +192,27 @@ module Sequel
181
192
  left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
182
193
  opts[:eager_loader] ||= lambda do |eo|
183
194
  h = eo[:key_hash][left_pk]
184
- eo[:rows].each{|object| object.associations[name] = []}
195
+ rows = eo[:rows]
196
+ rows.each{|object| object.associations[name] = []}
185
197
  ds = opts.associated_class
186
198
  opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
187
199
  ft = opts[:final_reverse_edge]
188
- conds = uses_lcks ? [[left_keys.map{|k| SQL::QualifiedIdentifier.new(ft[:table], k)}, h.keys]] : [[left_key, h.keys]]
189
- 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|
200
+ ds = ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + [[opts.eager_loading_predicate_key, h.keys]], :table_alias=>ft[:alias])
201
+ ds = model.eager_loading_dataset(opts, ds, nil, eo[:associations], eo)
202
+ case opts.eager_limit_strategy
203
+ when :window_function
204
+ delete_rn = true
205
+ rn = ds.row_number_column
206
+ ds = apply_window_function_eager_limit_strategy(ds, opts)
207
+ when :correlated_subquery
208
+ ds = apply_correlated_subquery_eager_limit_strategy(ds, opts) do |xds|
209
+ dsa = ds.send(:dataset_alias, 2)
210
+ opts.reverse_edges.each{|t| xds = xds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
211
+ 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)
212
+ end
213
+ end
214
+ ds.all do |assoc_record|
215
+ assoc_record.values.delete(rn) if delete_rn
191
216
  hash_key = if uses_lcks
192
217
  left_key_alias.map{|k| assoc_record.values.delete(k)}
193
218
  else
@@ -196,6 +221,10 @@ module Sequel
196
221
  next unless objects = h[hash_key]
197
222
  objects.each{|object| object.associations[name].push(assoc_record)}
198
223
  end
224
+ if opts.eager_limit_strategy == :ruby
225
+ limit, offset = opts.limit_and_offset
226
+ rows.each{|o| o.associations[name] = o.associations[name].slice(offset||0, limit) || []}
227
+ end
199
228
  end
200
229
 
201
230
  join_type = opts[:graph_join_type]
@@ -204,19 +233,51 @@ module Sequel
204
233
  only_conditions = opts[:graph_only_conditions]
205
234
  use_only_conditions = opts.include?(:graph_only_conditions)
206
235
  conditions = opts[:graph_conditions]
207
- opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
208
- iq = table_alias
236
+ opts[:eager_grapher] ||= proc do |eo|
237
+ ds = eo[:self]
238
+ iq = eo[:implicit_qualifier]
209
239
  opts.edges.each do |t|
210
240
  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
241
  iq = nil
212
242
  end
213
243
  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)
244
+ 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
245
  end
216
246
 
217
247
  def_association_dataset_methods(opts)
218
248
  end
219
249
  end
250
+
251
+ module DatasetMethods
252
+ private
253
+
254
+ # Use a subquery to filter rows to those related to the given associated object
255
+ def many_through_many_association_filter_expression(op, ref, obj)
256
+ lpks = ref[:left_primary_keys]
257
+ lpks = lpks.first if lpks.length == 1
258
+ lpks = ref.qualify(model.table_name, lpks)
259
+ edges = ref.edges
260
+ first, rest = edges.first, edges[1..-1]
261
+ last = edges.last
262
+ ds = model.db[first[:table]].select(*Array(ref.qualify(first[:table], first[:right])))
263
+ 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])}
264
+ last_alias = if rest.empty?
265
+ first[:table]
266
+ else
267
+ last_join = ds.opts[:join].last
268
+ last_join.table_alias || last_join.table
269
+ end
270
+ meths = ref.right_primary_keys
271
+ meths = ref.qualify(obj.model.table_name, meths) if obj.is_a?(Sequel::Dataset)
272
+ exp = association_filter_key_expression(ref.qualify(last_alias, Array(ref[:final_edge][:left])), meths, obj)
273
+ if exp == SQL::Constants::FALSE
274
+ association_filter_handle_inversion(op, exp, Array(lpks))
275
+ else
276
+ ds = ds.where(exp).exclude(SQL::BooleanExpression.from_value_pairs(ds.opts[:select].zip([]), :OR))
277
+ association_filter_handle_inversion(op, SQL::BooleanExpression.from_value_pairs(lpks=>ds), Array(lpks))
278
+ end
279
+ end
280
+ end
220
281
  end
221
282
  end
222
283
  end
@@ -1,20 +1,75 @@
1
1
  module Sequel
2
2
  module Plugins
3
- # The nested_attributes plugin allows you to update attributes for associated
4
- # objects directly through the parent object, similar to ActiveRecord's
5
- # Nested Attributes feature.
6
- #
7
- # Nested attributes are created using the nested_attributes method:
3
+ # The nested_attributes plugin allows you to create, update, and delete
4
+ # associated objects directly by calling a method on the current object.
5
+ # Nested attributes are defined using the nested_attributes class method:
8
6
  #
9
7
  # Artist.one_to_many :albums
10
8
  # Artist.plugin :nested_attributes
11
9
  # Artist.nested_attributes :albums
12
- # a = Artist.new(:name=>'YJM',
13
- # :albums_attributes=>[{:name=>'RF'}, {:name=>'MO'}])
14
- # # No database activity yet
15
10
  #
16
- # a.save # Saves artist and both albums
17
- # a.albums.map{|x| x.name} # ['RF', 'MO']
11
+ # The nested_attributes call defines a single method, <tt><i>association</i>_attributes=</tt>,
12
+ # (e.g. <tt>albums_attributes=</tt>). So if you have an Artist instance:
13
+ #
14
+ # a = Artist.new(:name=>'YJM')
15
+ #
16
+ # You can create new album instances related to this artist:
17
+ #
18
+ # a.albums_attributes = [{:name=>'RF'}, {:name=>'MO'}]
19
+ #
20
+ # Note that this doesn't send any queries to the database yet. That doesn't happen till
21
+ # you save the object:
22
+ #
23
+ # a.save
24
+ #
25
+ # That will save the artist first, and then save both albums. If either the artist
26
+ # is invalid or one of the albums is invalid, none of the objects will be saved to the
27
+ # database, and all related validation errors will be available in the artist's validation
28
+ # errors.
29
+ #
30
+ # In addition to creating new associated objects, you can also update existing associated
31
+ # objects. You just need to make sure that the primary key field is filled in for the
32
+ # associated object:
33
+ #
34
+ # a.update(:albums_attributes => [{:id=>1, :name=>'T'}])
35
+ #
36
+ # Since the primary key field is filled in, the plugin will update the album with id 1 instead
37
+ # of creating a new album.
38
+ #
39
+ # If you would like to delete the associated object instead of updating it, you add a _delete
40
+ # entry to the hash:
41
+ #
42
+ # a.update(:albums_attributes => [{:id=>1, :_delete=>true}])
43
+ #
44
+ # This will delete the related associated object from the database. If you want to leave the
45
+ # associated object in the database, but just remove it from the association, add a _remove
46
+ # entry in the hash:
47
+ #
48
+ # a.update(:albums_attributes => [{:id=>1, :_remove=>true}])
49
+ #
50
+ # The above example was for a one_to_many association, but the plugin also works similarly
51
+ # for other association types. For one_to_one and many_to_one associations, you need to
52
+ # pass a single hash instead of an array of hashes.
53
+ #
54
+ # This plugin is mainly designed to make it easy to use on html forms, where a single form
55
+ # submission can contained nested attributes (and even nested attributes of those attributes).
56
+ # You just need to name your form inputs correctly:
57
+ #
58
+ # artist[name]
59
+ # artist[albums_attributes][0][:name]
60
+ # artist[albums_attributes][1][:id]
61
+ # artist[albums_attributes][1][:name]
62
+ #
63
+ # Your web stack will probably parse that into a nested hash similar to:
64
+ #
65
+ # {:artist=>{:name=>?, :albums_attributes=>{0=>{:name=>?}, 1=>{:id=>?, :name=>?}}}}
66
+ #
67
+ # Then you can do:
68
+ #
69
+ # artist.update(params[:artist])
70
+ #
71
+ # To save changes to the artist, create the first album and associate it to the artist,
72
+ # and update the other existing associated album.
18
73
  module NestedAttributes
19
74
  # Depend on the instance_hooks plugin.
20
75
  def self.apply(model)
@@ -0,0 +1,151 @@
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
+ if dataset.requires_placeholder_type_specifiers?
89
+ sch = db_schema
90
+ Array(keys).map do |k|
91
+ if (s = sch[k]) && (t = s[:type])
92
+ [k, :"$#{k}__#{t}"]
93
+ else
94
+ [k, :"$#{k}"]
95
+ end
96
+ end
97
+ else
98
+ Array(keys).map{|k| [k, :"$#{k}"]}
99
+ end
100
+ end
101
+
102
+ # Return a hash mapping column symbols to placeholder symbols.
103
+ def prepared_statement_key_hash(keys)
104
+ Hash[*(prepared_statement_key_array(keys).flatten)]
105
+ end
106
+
107
+ # Return a prepared statement that can be used to update row using the given columns.
108
+ def prepared_update(cols)
109
+ @prepared_statements[:update][prepared_columns(cols)] ||= prepare_statement(filter(prepared_statement_key_array(primary_key)), :update, prepared_statement_key_hash(cols))
110
+ end
111
+
112
+ # Use a prepared statement to query the database for the row matching the given primary key.
113
+ def primary_key_lookup(pk)
114
+ prepared_lookup.call(primary_key_hash(pk))
115
+ end
116
+ end
117
+
118
+ module InstanceMethods
119
+ private
120
+
121
+ # Use a prepared statement to delete the row.
122
+ def _delete_without_checking
123
+ model.send(:prepared_delete).call(pk_hash)
124
+ end
125
+
126
+ # Use a prepared statement to insert the values into the model's dataset.
127
+ def _insert_raw(ds)
128
+ model.send(:prepared_insert, @values.keys).call(@values)
129
+ end
130
+
131
+ # Use a prepared statement to insert the values into the model's dataset
132
+ # and return the new column values.
133
+ def _insert_select_raw(ds)
134
+ if ps = model.send(:prepared_insert_select, @values.keys)
135
+ ps.call(@values)
136
+ end
137
+ end
138
+
139
+ # Use a prepared statement to refresh this model's column values.
140
+ def _refresh_get(ds)
141
+ model.send(:prepared_refresh).call(pk_hash)
142
+ end
143
+
144
+ # Use a prepared statement to update this model's columns in the database.
145
+ def _update_without_checking(columns)
146
+ model.send(:prepared_update, columns.keys).call(columns.merge(pk_hash))
147
+ end
148
+ end
149
+ end
150
+ end
151
+ 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_alias, 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
@@ -0,0 +1,82 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The prepared_statements_safe plugin modifies the model to reduce the number of
4
+ # prepared statements that can be created, by setting as many columns as possible
5
+ # before creating, and by changing +save_changes+ to save all columns instead of
6
+ # just the changed ones.
7
+ #
8
+ # This plugin depends on the +prepared_statements+ plugin.
9
+ #
10
+ # Usage:
11
+ #
12
+ # # Make all model subclasses more safe when using prepared statements (called before loading subclasses)
13
+ # Sequel::Model.plugin :prepared_statements_safe
14
+ #
15
+ # # Make the Album class more safe when using prepared statements
16
+ # Album.plugin :prepared_statements_safe
17
+ module PreparedStatementsSafe
18
+ # Depend on the prepared_statements plugin
19
+ def self.apply(model)
20
+ model.plugin(:prepared_statements)
21
+ end
22
+
23
+ # Set the column defaults to use when creating on the model.
24
+ def self.configure(model)
25
+ model.send(:set_prepared_statements_column_defaults)
26
+ end
27
+
28
+ module ClassMethods
29
+ # A hash with column symbol keys and default values. Instance's
30
+ # values are merged into this hash before creating to reduce the
31
+ # number of free columns (columns that may or may not be present
32
+ # in the INSERT statement), as the number of prepared statements
33
+ # that can be created is 2^N (where N is the number of free columns).
34
+ attr_reader :prepared_statements_column_defaults
35
+
36
+ def inherited(subclass)
37
+ super
38
+ subclass.instance_variable_set(:@prepared_statements_column_defaults, @prepared_statements_column_defaults) if @prepared_statements_column_defaults && !subclass.prepared_statements_column_defaults
39
+ end
40
+
41
+ # Set the column defaults to use when creating on the subclass.
42
+ def set_dataset(*)
43
+ x = super
44
+ set_prepared_statements_column_defaults
45
+ x
46
+ end
47
+
48
+ private
49
+
50
+ # Set the column defaults based on the database schema. All columns
51
+ # are set to a default value unless they are a primary key column or
52
+ # they don't have a parseable default.
53
+ def set_prepared_statements_column_defaults
54
+ if db_schema
55
+ h = {}
56
+ db_schema.each do |k, v|
57
+ h[k] = v[:ruby_default] if (v[:ruby_default] || !v[:default]) && !v[:primary_key]
58
+ end
59
+ @prepared_statements_column_defaults = h
60
+ end
61
+ end
62
+ end
63
+
64
+ module InstanceMethods
65
+ # Merge the current values into the default values to reduce the number
66
+ # of free columns.
67
+ def before_create
68
+ if v = model.prepared_statements_column_defaults
69
+ set_values(v.merge(values))
70
+ end
71
+ super
72
+ end
73
+
74
+ # Always do a full save of all columns to reduce the number of prepared
75
+ # statements that can be used.
76
+ def save_changes(opts={})
77
+ save(opts) || false if modified?
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,59 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The prepared_statements_with_pk plugin allows Dataset#with_pk for model datasets
4
+ # to use prepared statements by extract the values of previously bound variables
5
+ # using <tt>Dataset#unbind</tt>, and attempting to use a prepared statement if the
6
+ # variables can be unbound correctly. See +Unbinder+ for details about what types of
7
+ # dataset filters can be unbound correctly.
8
+ #
9
+ # This plugin depends on the +prepared_statements+ plugin and should be considered unsafe.
10
+ # Unbinding dataset values cannot be done correctly in all cases, and use of this plugin
11
+ # in cases where not there are variables that are not unbound can lead to an denial of
12
+ # service attack by allocating an arbitrary number of prepared statements. You have been
13
+ # warned.
14
+ #
15
+ # Usage:
16
+ #
17
+ # # Make all model subclasses use prepared statements for Dataset#with_pk (called before loading subclasses)
18
+ # Sequel::Model.plugin :prepared_statements_with_pk
19
+ #
20
+ # # Make the Album class use prepared statements for Dataset#with_pk
21
+ # Album.plugin :prepared_statements_with_pk
22
+ module PreparedStatementsWithPk
23
+ # Depend on the prepared_statements plugin
24
+ def self.apply(model)
25
+ model.plugin(:prepared_statements)
26
+ end
27
+
28
+ module ClassMethods
29
+ private
30
+
31
+ # Return a prepared statement that can be used to lookup a row given a dataset for the row matching
32
+ # the primary key.
33
+ def prepared_lookup_dataset(ds)
34
+ @prepared_statements[:lookup_sql][ds.sql] ||= prepare_statement(ds.filter(prepared_statement_key_array(primary_key).map{|k, v| [SQL::QualifiedIdentifier.new(ds.model.table_name, k), v]}), :first)
35
+ end
36
+ end
37
+
38
+ module DatasetMethods
39
+ # Use a prepared statement to find a row with the matching primary key
40
+ # inside this dataset.
41
+ def with_pk(pk)
42
+ begin
43
+ ds, bv = unbind
44
+ rescue UnbindDuplicate
45
+ super
46
+ else
47
+ begin
48
+ bv = bv.merge!(model.primary_key_hash(pk)){|k, v1, v2| ((v1 == v2) ? v1 : raise(UnbindDuplicate))}
49
+ rescue UnbindDuplicate
50
+ super
51
+ else
52
+ model.send(:prepared_lookup_dataset, ds).call(bv)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end