sequel 3.21.0 → 3.36.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 (311) hide show
  1. data/CHANGELOG +855 -3
  2. data/MIT-LICENSE +1 -1
  3. data/README.rdoc +23 -7
  4. data/Rakefile +42 -24
  5. data/bin/sequel +26 -8
  6. data/doc/active_record.rdoc +1 -0
  7. data/doc/advanced_associations.rdoc +171 -11
  8. data/doc/association_basics.rdoc +293 -43
  9. data/doc/dataset_basics.rdoc +5 -5
  10. data/doc/dataset_filtering.rdoc +8 -0
  11. data/doc/mass_assignment.rdoc +56 -0
  12. data/doc/migration.rdoc +85 -552
  13. data/doc/model_hooks.rdoc +78 -27
  14. data/doc/object_model.rdoc +541 -0
  15. data/doc/opening_databases.rdoc +97 -50
  16. data/doc/prepared_statements.rdoc +40 -14
  17. data/doc/querying.rdoc +28 -4
  18. data/doc/reflection.rdoc +30 -4
  19. data/doc/release_notes/3.22.0.txt +39 -0
  20. data/doc/release_notes/3.23.0.txt +172 -0
  21. data/doc/release_notes/3.24.0.txt +420 -0
  22. data/doc/release_notes/3.25.0.txt +88 -0
  23. data/doc/release_notes/3.26.0.txt +88 -0
  24. data/doc/release_notes/3.27.0.txt +82 -0
  25. data/doc/release_notes/3.28.0.txt +304 -0
  26. data/doc/release_notes/3.29.0.txt +459 -0
  27. data/doc/release_notes/3.30.0.txt +135 -0
  28. data/doc/release_notes/3.31.0.txt +146 -0
  29. data/doc/release_notes/3.32.0.txt +202 -0
  30. data/doc/release_notes/3.33.0.txt +157 -0
  31. data/doc/release_notes/3.34.0.txt +671 -0
  32. data/doc/release_notes/3.35.0.txt +144 -0
  33. data/doc/release_notes/3.36.0.txt +245 -0
  34. data/doc/schema_modification.rdoc +572 -0
  35. data/doc/sharding.rdoc +95 -3
  36. data/doc/testing.rdoc +129 -0
  37. data/doc/thread_safety.rdoc +17 -0
  38. data/doc/transactions.rdoc +137 -0
  39. data/doc/virtual_rows.rdoc +51 -2
  40. data/lib/sequel/adapters/ado/mssql.rb +18 -6
  41. data/lib/sequel/adapters/ado.rb +40 -18
  42. data/lib/sequel/adapters/amalgalite.rb +17 -9
  43. data/lib/sequel/adapters/db2.rb +175 -86
  44. data/lib/sequel/adapters/dbi.rb +15 -15
  45. data/lib/sequel/adapters/do/mysql.rb +17 -11
  46. data/lib/sequel/adapters/do/postgres.rb +1 -57
  47. data/lib/sequel/adapters/do/sqlite.rb +0 -10
  48. data/lib/sequel/adapters/do.rb +18 -36
  49. data/lib/sequel/adapters/firebird.rb +27 -208
  50. data/lib/sequel/adapters/ibmdb.rb +453 -0
  51. data/lib/sequel/adapters/informix.rb +6 -23
  52. data/lib/sequel/adapters/jdbc/as400.rb +16 -34
  53. data/lib/sequel/adapters/jdbc/db2.rb +47 -0
  54. data/lib/sequel/adapters/jdbc/derby.rb +287 -0
  55. data/lib/sequel/adapters/jdbc/firebird.rb +24 -0
  56. data/lib/sequel/adapters/jdbc/h2.rb +73 -31
  57. data/lib/sequel/adapters/jdbc/hsqldb.rb +184 -0
  58. data/lib/sequel/adapters/jdbc/informix.rb +21 -0
  59. data/lib/sequel/adapters/jdbc/jtds.rb +29 -0
  60. data/lib/sequel/adapters/jdbc/mssql.rb +3 -33
  61. data/lib/sequel/adapters/jdbc/mysql.rb +7 -24
  62. data/lib/sequel/adapters/jdbc/oracle.rb +88 -25
  63. data/lib/sequel/adapters/jdbc/postgresql.rb +58 -48
  64. data/lib/sequel/adapters/jdbc/progress.rb +21 -0
  65. data/lib/sequel/adapters/jdbc/sqlite.rb +20 -10
  66. data/lib/sequel/adapters/jdbc/sqlserver.rb +66 -0
  67. data/lib/sequel/adapters/jdbc/transactions.rb +83 -0
  68. data/lib/sequel/adapters/jdbc.rb +265 -94
  69. data/lib/sequel/adapters/mock.rb +361 -0
  70. data/lib/sequel/adapters/mysql.rb +118 -222
  71. data/lib/sequel/adapters/mysql2.rb +58 -57
  72. data/lib/sequel/adapters/odbc/mssql.rb +15 -7
  73. data/lib/sequel/adapters/odbc.rb +28 -9
  74. data/lib/sequel/adapters/openbase.rb +3 -5
  75. data/lib/sequel/adapters/oracle.rb +349 -53
  76. data/lib/sequel/adapters/postgres.rb +356 -107
  77. data/lib/sequel/adapters/shared/access.rb +32 -9
  78. data/lib/sequel/adapters/shared/db2.rb +342 -0
  79. data/lib/sequel/adapters/shared/firebird.rb +221 -0
  80. data/lib/sequel/adapters/shared/informix.rb +53 -0
  81. data/lib/sequel/adapters/shared/mssql.rb +335 -118
  82. data/lib/sequel/adapters/shared/mysql.rb +340 -76
  83. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +155 -0
  84. data/lib/sequel/adapters/shared/oracle.rb +243 -45
  85. data/lib/sequel/adapters/shared/postgres.rb +524 -279
  86. data/lib/sequel/adapters/shared/progress.rb +5 -8
  87. data/lib/sequel/adapters/shared/sqlite.rb +265 -89
  88. data/lib/sequel/adapters/sqlite.rb +101 -39
  89. data/lib/sequel/adapters/swift/mysql.rb +9 -12
  90. data/lib/sequel/adapters/swift/postgres.rb +9 -74
  91. data/lib/sequel/adapters/swift/sqlite.rb +8 -6
  92. data/lib/sequel/adapters/swift.rb +8 -7
  93. data/lib/sequel/adapters/tinytds.rb +161 -21
  94. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +66 -0
  95. data/lib/sequel/adapters/utils/stored_procedures.rb +1 -11
  96. data/lib/sequel/ast_transformer.rb +194 -0
  97. data/lib/sequel/connection_pool/sharded_single.rb +11 -1
  98. data/lib/sequel/connection_pool/sharded_threaded.rb +28 -2
  99. data/lib/sequel/connection_pool/single.rb +5 -0
  100. data/lib/sequel/connection_pool/threaded.rb +38 -14
  101. data/lib/sequel/connection_pool.rb +10 -2
  102. data/lib/sequel/core.rb +118 -11
  103. data/lib/sequel/database/connecting.rb +38 -28
  104. data/lib/sequel/database/dataset.rb +4 -4
  105. data/lib/sequel/database/dataset_defaults.rb +63 -2
  106. data/lib/sequel/database/logging.rb +7 -2
  107. data/lib/sequel/database/misc.rb +150 -46
  108. data/lib/sequel/database/query.rb +215 -65
  109. data/lib/sequel/database/schema_generator.rb +64 -20
  110. data/lib/sequel/database/schema_methods.rb +212 -54
  111. data/lib/sequel/dataset/actions.rb +322 -85
  112. data/lib/sequel/dataset/features.rb +96 -8
  113. data/lib/sequel/dataset/graph.rb +38 -11
  114. data/lib/sequel/dataset/misc.rb +50 -23
  115. data/lib/sequel/dataset/mutation.rb +12 -7
  116. data/lib/sequel/dataset/prepared_statements.rb +68 -30
  117. data/lib/sequel/dataset/query.rb +237 -44
  118. data/lib/sequel/dataset/sql.rb +698 -278
  119. data/lib/sequel/dataset.rb +8 -0
  120. data/lib/sequel/exceptions.rb +4 -0
  121. data/lib/sequel/extensions/_pretty_table.rb +79 -0
  122. data/lib/sequel/extensions/arbitrary_servers.rb +108 -0
  123. data/lib/sequel/extensions/columns_introspection.rb +61 -0
  124. data/lib/sequel/{core_sql.rb → extensions/core_extensions.rb} +16 -34
  125. data/lib/sequel/extensions/migration.rb +97 -11
  126. data/lib/sequel/extensions/named_timezones.rb +5 -0
  127. data/lib/sequel/extensions/null_dataset.rb +90 -0
  128. data/lib/sequel/extensions/pg_array.rb +460 -0
  129. data/lib/sequel/extensions/pg_array_ops.rb +220 -0
  130. data/lib/sequel/extensions/pg_auto_parameterize.rb +169 -0
  131. data/lib/sequel/extensions/pg_hstore.rb +296 -0
  132. data/lib/sequel/extensions/pg_hstore_ops.rb +259 -0
  133. data/lib/sequel/extensions/pg_inet.rb +89 -0
  134. data/lib/sequel/extensions/pg_json.rb +178 -0
  135. data/lib/sequel/extensions/pg_statement_cache.rb +316 -0
  136. data/lib/sequel/extensions/pretty_table.rb +5 -71
  137. data/lib/sequel/extensions/query.rb +1 -1
  138. data/lib/sequel/extensions/query_literals.rb +79 -0
  139. data/lib/sequel/extensions/schema_caching.rb +76 -0
  140. data/lib/sequel/extensions/schema_dumper.rb +251 -37
  141. data/lib/sequel/extensions/select_remove.rb +35 -0
  142. data/lib/sequel/extensions/server_block.rb +139 -0
  143. data/lib/sequel/extensions/sql_expr.rb +4 -110
  144. data/lib/sequel/extensions/thread_local_timezones.rb +1 -1
  145. data/lib/sequel/extensions/to_dot.rb +95 -83
  146. data/lib/sequel/model/associations.rb +1187 -393
  147. data/lib/sequel/model/base.rb +578 -170
  148. data/lib/sequel/model/errors.rb +1 -1
  149. data/lib/sequel/model/exceptions.rb +5 -1
  150. data/lib/sequel/model/inflections.rb +1 -1
  151. data/lib/sequel/model.rb +30 -11
  152. data/lib/sequel/no_core_ext.rb +2 -0
  153. data/lib/sequel/plugins/active_model.rb +13 -1
  154. data/lib/sequel/plugins/association_pks.rb +22 -4
  155. data/lib/sequel/plugins/caching.rb +25 -18
  156. data/lib/sequel/plugins/class_table_inheritance.rb +4 -4
  157. data/lib/sequel/plugins/composition.rb +44 -12
  158. data/lib/sequel/plugins/dataset_associations.rb +100 -0
  159. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  160. data/lib/sequel/plugins/dirty.rb +214 -0
  161. data/lib/sequel/plugins/eager_each.rb +59 -0
  162. data/lib/sequel/plugins/force_encoding.rb +6 -6
  163. data/lib/sequel/plugins/hook_class_methods.rb +1 -1
  164. data/lib/sequel/plugins/identity_map.rb +123 -8
  165. data/lib/sequel/plugins/instance_filters.rb +10 -0
  166. data/lib/sequel/plugins/instance_hooks.rb +1 -1
  167. data/lib/sequel/plugins/json_serializer.rb +58 -6
  168. data/lib/sequel/plugins/list.rb +12 -2
  169. data/lib/sequel/plugins/many_through_many.rb +79 -28
  170. data/lib/sequel/plugins/many_to_one_pk_lookup.rb +71 -0
  171. data/lib/sequel/plugins/nested_attributes.rb +139 -63
  172. data/lib/sequel/plugins/optimistic_locking.rb +8 -0
  173. data/lib/sequel/plugins/prepared_statements.rb +167 -0
  174. data/lib/sequel/plugins/prepared_statements_associations.rb +87 -0
  175. data/lib/sequel/plugins/prepared_statements_safe.rb +82 -0
  176. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  177. data/lib/sequel/plugins/rcte_tree.rb +29 -15
  178. data/lib/sequel/plugins/schema.rb +7 -2
  179. data/lib/sequel/plugins/serialization.rb +98 -49
  180. data/lib/sequel/plugins/serialization_modification_detection.rb +63 -0
  181. data/lib/sequel/plugins/sharding.rb +21 -54
  182. data/lib/sequel/plugins/single_table_inheritance.rb +5 -3
  183. data/lib/sequel/plugins/static_cache.rb +99 -0
  184. data/lib/sequel/plugins/subclasses.rb +10 -2
  185. data/lib/sequel/plugins/tactical_eager_loading.rb +7 -7
  186. data/lib/sequel/plugins/timestamps.rb +1 -1
  187. data/lib/sequel/plugins/typecast_on_load.rb +9 -12
  188. data/lib/sequel/plugins/update_primary_key.rb +2 -2
  189. data/lib/sequel/plugins/validation_class_methods.rb +1 -1
  190. data/lib/sequel/plugins/xml_serializer.rb +15 -4
  191. data/lib/sequel/sql.rb +526 -59
  192. data/lib/sequel/timezones.rb +57 -38
  193. data/lib/sequel/version.rb +1 -1
  194. data/spec/adapters/db2_spec.rb +146 -0
  195. data/spec/adapters/firebird_spec.rb +1 -1
  196. data/spec/adapters/mssql_spec.rb +142 -40
  197. data/spec/adapters/mysql_spec.rb +380 -234
  198. data/spec/adapters/oracle_spec.rb +67 -77
  199. data/spec/adapters/postgres_spec.rb +1050 -212
  200. data/spec/adapters/spec_helper.rb +8 -5
  201. data/spec/adapters/sqlite_spec.rb +139 -27
  202. data/spec/core/connection_pool_spec.rb +196 -93
  203. data/spec/core/database_spec.rb +879 -510
  204. data/spec/core/dataset_spec.rb +1415 -1086
  205. data/spec/core/expression_filters_spec.rb +554 -109
  206. data/spec/core/mock_adapter_spec.rb +449 -0
  207. data/spec/core/object_graph_spec.rb +86 -114
  208. data/spec/core/schema_generator_spec.rb +4 -4
  209. data/spec/core/schema_spec.rb +482 -38
  210. data/spec/core/spec_helper.rb +6 -48
  211. data/spec/extensions/active_model_spec.rb +13 -0
  212. data/spec/extensions/arbitrary_servers_spec.rb +110 -0
  213. data/spec/extensions/association_autoreloading_spec.rb +18 -10
  214. data/spec/extensions/association_dependencies_spec.rb +15 -25
  215. data/spec/extensions/association_pks_spec.rb +66 -32
  216. data/spec/extensions/association_proxies_spec.rb +4 -4
  217. data/spec/extensions/caching_spec.rb +47 -51
  218. data/spec/extensions/class_table_inheritance_spec.rb +31 -83
  219. data/spec/extensions/columns_introspection_spec.rb +91 -0
  220. data/spec/extensions/composition_spec.rb +18 -13
  221. data/spec/{core/core_sql_spec.rb → extensions/core_extensions_spec.rb} +119 -139
  222. data/spec/extensions/dataset_associations_spec.rb +199 -0
  223. data/spec/extensions/defaults_setter_spec.rb +64 -0
  224. data/spec/extensions/dirty_spec.rb +155 -0
  225. data/spec/extensions/eager_each_spec.rb +34 -0
  226. data/spec/extensions/force_encoding_spec.rb +4 -2
  227. data/spec/extensions/hook_class_methods_spec.rb +69 -92
  228. data/spec/extensions/identity_map_spec.rb +131 -22
  229. data/spec/extensions/inflector_spec.rb +0 -4
  230. data/spec/extensions/instance_filters_spec.rb +11 -21
  231. data/spec/extensions/instance_hooks_spec.rb +72 -0
  232. data/spec/extensions/json_serializer_spec.rb +49 -6
  233. data/spec/extensions/lazy_attributes_spec.rb +16 -20
  234. data/spec/extensions/list_spec.rb +48 -39
  235. data/spec/extensions/many_through_many_spec.rb +283 -250
  236. data/spec/extensions/many_to_one_pk_lookup_spec.rb +140 -0
  237. data/spec/extensions/migration_spec.rb +140 -35
  238. data/spec/extensions/named_timezones_spec.rb +29 -11
  239. data/spec/extensions/nested_attributes_spec.rb +251 -89
  240. data/spec/extensions/null_dataset_spec.rb +85 -0
  241. data/spec/extensions/optimistic_locking_spec.rb +24 -21
  242. data/spec/extensions/pg_array_ops_spec.rb +105 -0
  243. data/spec/extensions/pg_array_spec.rb +196 -0
  244. data/spec/extensions/pg_auto_parameterize_spec.rb +65 -0
  245. data/spec/extensions/pg_hstore_ops_spec.rb +136 -0
  246. data/spec/extensions/pg_hstore_spec.rb +195 -0
  247. data/spec/extensions/pg_inet_spec.rb +44 -0
  248. data/spec/extensions/pg_json_spec.rb +101 -0
  249. data/spec/extensions/pg_statement_cache_spec.rb +209 -0
  250. data/spec/extensions/prepared_statements_associations_spec.rb +111 -0
  251. data/spec/extensions/prepared_statements_safe_spec.rb +61 -0
  252. data/spec/extensions/prepared_statements_spec.rb +87 -0
  253. data/spec/extensions/prepared_statements_with_pk_spec.rb +31 -0
  254. data/spec/extensions/pretty_table_spec.rb +7 -6
  255. data/spec/extensions/query_literals_spec.rb +168 -0
  256. data/spec/extensions/query_spec.rb +6 -0
  257. data/spec/extensions/rcte_tree_spec.rb +50 -43
  258. data/spec/extensions/schema_caching_spec.rb +41 -0
  259. data/spec/extensions/schema_dumper_spec.rb +429 -22
  260. data/spec/extensions/schema_spec.rb +25 -21
  261. data/spec/extensions/select_remove_spec.rb +38 -0
  262. data/spec/extensions/serialization_modification_detection_spec.rb +72 -0
  263. data/spec/extensions/serialization_spec.rb +85 -43
  264. data/spec/extensions/server_block_spec.rb +90 -0
  265. data/spec/extensions/sharding_spec.rb +69 -143
  266. data/spec/extensions/single_table_inheritance_spec.rb +23 -29
  267. data/spec/extensions/skip_create_refresh_spec.rb +1 -1
  268. data/spec/extensions/spec_helper.rb +22 -61
  269. data/spec/extensions/sql_expr_spec.rb +29 -60
  270. data/spec/extensions/static_cache_spec.rb +145 -0
  271. data/spec/extensions/tactical_eager_loading_spec.rb +17 -19
  272. data/spec/extensions/thread_local_timezones_spec.rb +22 -2
  273. data/spec/extensions/timestamps_spec.rb +6 -6
  274. data/spec/extensions/to_dot_spec.rb +4 -7
  275. data/spec/extensions/touch_spec.rb +13 -14
  276. data/spec/extensions/tree_spec.rb +11 -26
  277. data/spec/extensions/typecast_on_load_spec.rb +9 -6
  278. data/spec/extensions/update_primary_key_spec.rb +30 -24
  279. data/spec/extensions/validation_class_methods_spec.rb +30 -51
  280. data/spec/extensions/validation_helpers_spec.rb +16 -35
  281. data/spec/extensions/xml_serializer_spec.rb +29 -4
  282. data/spec/files/transaction_specified_migrations/001_create_alt_basic.rb +4 -0
  283. data/spec/files/transaction_specified_migrations/002_create_basic.rb +4 -0
  284. data/spec/files/transaction_unspecified_migrations/001_create_alt_basic.rb +3 -0
  285. data/spec/files/transaction_unspecified_migrations/002_create_basic.rb +3 -0
  286. data/spec/integration/associations_test.rb +659 -21
  287. data/spec/integration/database_test.rb +9 -3
  288. data/spec/integration/dataset_test.rb +605 -91
  289. data/spec/integration/eager_loader_test.rb +19 -21
  290. data/spec/integration/migrator_test.rb +2 -3
  291. data/spec/integration/model_test.rb +81 -2
  292. data/spec/integration/plugin_test.rb +454 -143
  293. data/spec/integration/prepared_statement_test.rb +263 -128
  294. data/spec/integration/schema_test.rb +309 -43
  295. data/spec/integration/spec_helper.rb +40 -30
  296. data/spec/integration/timezone_test.rb +38 -12
  297. data/spec/integration/transaction_test.rb +183 -8
  298. data/spec/integration/type_test.rb +18 -4
  299. data/spec/model/association_reflection_spec.rb +201 -7
  300. data/spec/model/associations_spec.rb +1067 -661
  301. data/spec/model/base_spec.rb +329 -95
  302. data/spec/model/dataset_methods_spec.rb +7 -27
  303. data/spec/model/eager_loading_spec.rb +677 -676
  304. data/spec/model/hooks_spec.rb +349 -67
  305. data/spec/model/model_spec.rb +240 -168
  306. data/spec/model/plugins_spec.rb +24 -13
  307. data/spec/model/record_spec.rb +490 -232
  308. data/spec/model/spec_helper.rb +14 -71
  309. data/spec/model/validations_spec.rb +11 -0
  310. data/spec/rcov.opts +2 -0
  311. metadata +178 -80
@@ -47,20 +47,18 @@ module Sequel
47
47
  class ManyThroughManyAssociationReflection < Sequel::Model::Associations::ManyToManyAssociationReflection
48
48
  Sequel::Model::Associations::ASSOCIATION_TYPES[:many_through_many] = self
49
49
 
50
- # The table containing the column to use for the associated key when eagerly loading
51
- def associated_key_table
52
- self[:associated_key_table] = self[:final_reverse_edge][:alias]
53
- end
54
-
55
50
  # The default associated key alias(es) to use when eager loading
56
51
  # associations via eager.
57
52
  def default_associated_key_alias
58
53
  self[:uses_left_composite_keys] ? (0...self[:through].first[:left].length).map{|i| :"x_foreign_key_#{i}_x"} : :x_foreign_key_x
59
54
  end
60
55
 
61
- # The list of joins to use when eager graphing
62
- def edges
63
- self[:edges] || calculate_edges || self[:edges]
56
+ %w'associated_key_table eager_loading_predicate_key edges final_edge final_reverse_edge reverse_edges'.each do |meth|
57
+ class_eval(<<-END, __FILE__, __LINE__+1)
58
+ def #{meth}
59
+ cached_fetch(:#{meth}){calculate_edges[:#{meth}]}
60
+ end
61
+ END
64
62
  end
65
63
 
66
64
  # Many through many associations don't have a reciprocal
@@ -68,11 +66,6 @@ module Sequel
68
66
  nil
69
67
  end
70
68
 
71
- # The list of joins to use when lazy loading or eager loading
72
- def reverse_edges
73
- self[:reverse_edges] || calculate_edges || self[:reverse_edges]
74
- end
75
-
76
69
  private
77
70
 
78
71
  # Make sure to use unique table aliases when lazy loading or eager loading
@@ -109,13 +102,21 @@ module Sequel
109
102
  reverse_edges = es.reverse.map{|e| {:table=>e[:left_table], :left=>e[:left_key], :right=>e[:right_key]}}
110
103
  reverse_edges.pop
111
104
  calculate_reverse_edge_aliases(reverse_edges)
112
- self[:final_edge] = edges.pop
113
- self[:final_reverse_edge] = reverse_edges.pop
114
- self[:edges] = edges
115
- self[:reverse_edges] = reverse_edges
116
- nil
105
+ final_reverse_edge = reverse_edges.pop
106
+ final_reverse_alias = final_reverse_edge[:alias]
107
+
108
+ h = {:final_edge=>edges.pop,
109
+ :final_reverse_edge=>final_reverse_edge,
110
+ :edges=>edges,
111
+ :reverse_edges=>reverse_edges,
112
+ :eager_loading_predicate_key=>qualify(final_reverse_alias, edges.first[:right]),
113
+ :associated_key_table=>final_reverse_edge[:alias],
114
+ }
115
+ h.each{|k, v| cached_set(k, v)}
116
+ h
117
117
  end
118
118
  end
119
+
119
120
  module ClassMethods
120
121
  # Create a many_through_many association. Arguments:
121
122
  # * name - Same as associate, the name of the association.
@@ -174,20 +175,34 @@ module Sequel
174
175
  opts[:dataset] ||= lambda do
175
176
  ds = opts.associated_class
176
177
  opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
177
- ft = opts[:final_reverse_edge]
178
+ ft = opts.final_reverse_edge
178
179
  ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + left_keys.zip(left_pks.map{|k| send(k)}), :table_alias=>ft[:alias])
179
180
  end
180
181
 
181
182
  left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
182
183
  opts[:eager_loader] ||= lambda do |eo|
183
184
  h = eo[:key_hash][left_pk]
184
- eo[:rows].each{|object| object.associations[name] = []}
185
+ rows = eo[:rows]
186
+ rows.each{|object| object.associations[name] = []}
185
187
  ds = opts.associated_class
186
188
  opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
187
- 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|
189
+ ft = opts.final_reverse_edge
190
+ ds = ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + [[opts.eager_loading_predicate_key, h.keys]], :table_alias=>ft[:alias])
191
+ ds = model.eager_loading_dataset(opts, ds, nil, eo[:associations], eo)
192
+ case opts.eager_limit_strategy
193
+ when :window_function
194
+ delete_rn = true
195
+ rn = ds.row_number_column
196
+ ds = apply_window_function_eager_limit_strategy(ds, opts)
197
+ when :correlated_subquery
198
+ ds = apply_correlated_subquery_eager_limit_strategy(ds, opts) do |xds|
199
+ dsa = ds.send(:dataset_alias, 2)
200
+ opts.reverse_edges.each{|t| xds = xds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
201
+ 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)
202
+ end
203
+ end
204
+ ds.all do |assoc_record|
205
+ assoc_record.values.delete(rn) if delete_rn
191
206
  hash_key = if uses_lcks
192
207
  left_key_alias.map{|k| assoc_record.values.delete(k)}
193
208
  else
@@ -196,6 +211,10 @@ module Sequel
196
211
  next unless objects = h[hash_key]
197
212
  objects.each{|object| object.associations[name].push(assoc_record)}
198
213
  end
214
+ if opts.eager_limit_strategy == :ruby
215
+ limit, offset = opts.limit_and_offset
216
+ rows.each{|o| o.associations[name] = o.associations[name].slice(offset||0, limit) || []}
217
+ end
199
218
  end
200
219
 
201
220
  join_type = opts[:graph_join_type]
@@ -204,19 +223,51 @@ module Sequel
204
223
  only_conditions = opts[:graph_only_conditions]
205
224
  use_only_conditions = opts.include?(:graph_only_conditions)
206
225
  conditions = opts[:graph_conditions]
207
- opts[:eager_grapher] ||= proc do |ds, assoc_alias, table_alias|
208
- iq = table_alias
226
+ opts[:eager_grapher] ||= proc do |eo|
227
+ ds = eo[:self]
228
+ iq = eo[:implicit_qualifier]
209
229
  opts.edges.each do |t|
210
230
  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
231
  iq = nil
212
232
  end
213
- 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)
233
+ fe = opts.final_edge
234
+ 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
235
  end
216
236
 
217
237
  def_association_dataset_methods(opts)
218
238
  end
219
239
  end
240
+
241
+ module DatasetMethods
242
+ private
243
+
244
+ # Use a subquery to filter rows to those related to the given associated object
245
+ def many_through_many_association_filter_expression(op, ref, obj)
246
+ lpks = ref[:left_primary_keys]
247
+ lpks = lpks.first if lpks.length == 1
248
+ lpks = ref.qualify(model.table_name, lpks)
249
+ edges = ref.edges
250
+ first, rest = edges.first, edges[1..-1]
251
+ last = edges.last
252
+ ds = model.db[first[:table]].select(*Array(ref.qualify(first[:table], first[:right])))
253
+ 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])}
254
+ last_alias = if rest.empty?
255
+ first[:table]
256
+ else
257
+ last_join = ds.opts[:join].last
258
+ last_join.table_alias || last_join.table
259
+ end
260
+ meths = ref.right_primary_keys
261
+ meths = ref.qualify(obj.model.table_name, meths) if obj.is_a?(Sequel::Dataset)
262
+ exp = association_filter_key_expression(ref.qualify(last_alias, Array(ref.final_edge[:left])), meths, obj)
263
+ if exp == SQL::Constants::FALSE
264
+ association_filter_handle_inversion(op, exp, Array(lpks))
265
+ else
266
+ ds = ds.where(exp).exclude(SQL::BooleanExpression.from_value_pairs(ds.opts[:select].zip([]), :OR))
267
+ association_filter_handle_inversion(op, SQL::BooleanExpression.from_value_pairs(lpks=>ds), Array(lpks))
268
+ end
269
+ end
270
+ end
220
271
  end
221
272
  end
222
273
  end
@@ -0,0 +1,71 @@
1
+ module Sequel
2
+ module Plugins
3
+ # This is a fairly simple plugin that modifies the internal association loading logic
4
+ # for many_to_one associations to use a simple primary key lookup on the associated
5
+ # class, which is generally faster as it uses mostly static SQL. Additional, if the
6
+ # associated class is caching primary key lookups, you get the benefit of a cached
7
+ # lookup.
8
+ #
9
+ # This plugin is generally not as fast as the prepared_statements_associations plugin
10
+ # in the case where the model is not caching primary key lookups, however, it is
11
+ # probably significantly faster if the model is caching primary key lookups. If
12
+ # the prepared_statements_associations plugin has been loaded first, this
13
+ # plugin will only use the primary key lookup code if the associated model is
14
+ # caching primary key lookups.
15
+ #
16
+ # This plugin attempts to determine cases where the primary key lookup would have
17
+ # different results than the regular lookup, and use the regular lookup in that case,
18
+ # but it cannot handle all situations correctly, which is why it is not Sequel's
19
+ # default behavior.
20
+ #
21
+ # You can disable primary key lookups on a per association basis with this
22
+ # plugin using the :many_to_one_pk_lookup=>false association option.
23
+ #
24
+ # Usage:
25
+ #
26
+ # # Make all model subclass instances use primary key lookups for many_to_one
27
+ # # association loading
28
+ # Sequel::Model.plugin :many_to_one_pk_lookup
29
+ #
30
+ # # Do so for just the album class.
31
+ # Album.plugin :many_to_one_pk_lookup
32
+ module ManyToOnePkLookup
33
+ module InstanceMethods
34
+ private
35
+
36
+ # If the current association is a fairly simple many_to_one association, use
37
+ # a simple primary key lookup on the associated model, which can benefit from
38
+ # caching if the associated model is using caching.
39
+ def _load_associated_object(opts, dynamic_opts)
40
+ klass = opts.associated_class
41
+ cache_lookup = opts.send(:cached_fetch, :many_to_one_pk_lookup) do
42
+ opts[:type] == :many_to_one &&
43
+ opts[:key] &&
44
+ opts.primary_key == klass.primary_key
45
+ end
46
+ if cache_lookup &&
47
+ !dynamic_opts[:callback] &&
48
+ (o = klass.send(:primary_key_lookup, ((fk = opts[:key]).is_a?(Array) ? fk.map{|c| send(c)} : send(fk))))
49
+ o
50
+ else
51
+ super
52
+ end
53
+ end
54
+
55
+ # Deal with the situation where the prepared_statements_associations plugin is
56
+ # loaded first, by using a primary key lookup for many_to_one associations if
57
+ # the associated class is using caching, and using the default code otherwise.
58
+ # This is done because the prepared_statements_associations code is probably faster
59
+ # than the primary key lookup this plugin uses if the model is not caching lookups,
60
+ # but probably slower if the model is caching lookups.
61
+ def _load_associated_objects(opts, dynamic_opts={})
62
+ if opts.can_have_associated_objects?(self) && opts[:type] == :many_to_one && opts.associated_class.respond_to?(:cache_get_pk)
63
+ _load_associated_object(opts, dynamic_opts)
64
+ else
65
+ super
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ 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)
@@ -32,20 +87,30 @@ module Sequel
32
87
  # modified through the association_attributes= method to the specific fields given.
33
88
  # * :limit - For *_to_many associations, a limit on the number of records
34
89
  # that will be processed, to prevent denial of service attacks.
90
+ # * :reject_if - A proc that is given each attribute hash before it is
91
+ # passed to its associated object. If the proc returns a truthy
92
+ # value, the attribute hash is ignored.
35
93
  # * :remove - Allow disassociation of nested records (can remove the associated
36
94
  # object from the parent object, but not destroy the associated object).
37
- # * :strict - Set to false to not raise an error message if a primary key
38
- # is provided in a record, but it doesn't match an existing associated
39
- # object.
95
+ # * :strict - Kept for backward compatibility. Setting it to false is
96
+ # equivalent to setting :unmatched_pk to :ignore.
97
+ # * :transform - A proc to transform attribute hashes before they are
98
+ # passed to associated object. Takes two arguments, the parent object and
99
+ # the attribute hash. Uses the return value as the new attribute hash.
100
+ # * :unmatched_pk - Specify the action to be taken if a primary key is
101
+ # provided in a record, but it doesn't match an existing associated
102
+ # object. Set to :create to create a new object with that primary
103
+ # key, :ignore to ignore the record, or :raise to raise an error.
104
+ # The default is :raise.
40
105
  #
41
- # If a block is provided, it is passed each nested attribute hash. If
42
- # the hash should be ignored, the block should return anything except false or nil.
106
+ # If a block is provided, it is used to set the :reject_if option.
43
107
  def nested_attributes(*associations, &block)
44
108
  include(self.nested_attributes_module ||= Module.new) unless nested_attributes_module
45
109
  opts = associations.last.is_a?(Hash) ? associations.pop : {}
46
110
  reflections = associations.map{|a| association_reflection(a) || raise(Error, "no association named #{a} for #{self}")}
47
111
  reflections.each do |r|
48
112
  r[:nested_attributes] = opts
113
+ r[:nested_attributes][:unmatched_pk] ||= opts.delete(:strict) == false ? :ignore : :raise
49
114
  r[:nested_attributes][:reject_if] ||= block
50
115
  def_nested_attribute_method(r)
51
116
  end
@@ -78,7 +143,9 @@ module Sequel
78
143
  def nested_attributes_check_key_modifications(reflection, obj)
79
144
  keys = reflection.associated_object_keys.map{|x| obj.send(x)}
80
145
  yield
81
- raise(Error, "Modifying association dependent key(s) when updating associated objects is not allowed") unless keys == reflection.associated_object_keys.map{|x| obj.send(x)}
146
+ unless keys == reflection.associated_object_keys.map{|x| obj.send(x)}
147
+ raise(Error, "Modifying association dependent key(s) when updating associated objects is not allowed")
148
+ end
82
149
  end
83
150
 
84
151
  # Create a new associated object with the given attributes, validate
@@ -93,6 +160,12 @@ module Sequel
93
160
  after_save_hook{send(reflection.add_method, obj)}
94
161
  else
95
162
  associations[reflection[:name]] = obj
163
+
164
+ # Because we are modifying the associations cache manually before the
165
+ # setter is called, we still want to run the setter code even though
166
+ # the cached value will be the same as the given value.
167
+ @set_associated_object_if_same = true
168
+
96
169
  # Don't need to validate the object twice if :validate association option is not false
97
170
  # and don't want to validate it at all if it is false.
98
171
  send(reflection[:type] == :many_to_one ? :before_save_hook : :after_save_hook){send(reflection.setter_method, obj.save(:validate=>false))}
@@ -100,16 +173,6 @@ module Sequel
100
173
  obj
101
174
  end
102
175
 
103
- # Find an associated object with the matching pk. If a matching option
104
- # is not found and the :strict option is not false, raise an Error.
105
- def nested_attributes_find(reflection, pk)
106
- pk = pk.to_s
107
- unless obj = Array(associated_objects = send(reflection[:name])).find{|x| x.pk.to_s == pk}
108
- raise(Error, "no matching associated object with given primary key (association: #{reflection[:name]}, pk: #{pk})") unless reflection[:nested_attributes][:strict] == false
109
- end
110
- obj
111
- end
112
-
113
176
  # Take an array or hash of attribute hashes and set each one individually.
114
177
  # If a hash is provided it, sort it by key and then use the values.
115
178
  # If there is a limit on the nested attributes for this association,
@@ -122,24 +185,22 @@ module Sequel
122
185
  attributes_list.each{|a| nested_attributes_setter(reflection, a)}
123
186
  end
124
187
 
125
- # Remove the matching associated object from the current object.
126
- # If the :destroy option is given, destroy the object after disassociating it
188
+ # Remove the given associated object from the current object. If the
189
+ # :destroy option is given, destroy the object after disassociating it
127
190
  # (unless destroying the object would automatically disassociate it).
128
- # Returns the object removed, if it exists.
129
- def nested_attributes_remove(reflection, pk, opts={})
130
- if obj = nested_attributes_find(reflection, pk)
131
- if !opts[:destroy] || reflection.remove_before_destroy?
132
- before_save_hook do
133
- if reflection.returns_array?
134
- send(reflection.remove_method, obj)
135
- else
136
- send(reflection.setter_method, nil)
137
- end
191
+ # Returns the object removed.
192
+ def nested_attributes_remove(reflection, obj, opts={})
193
+ if !opts[:destroy] || reflection.remove_before_destroy?
194
+ before_save_hook do
195
+ if reflection.returns_array?
196
+ send(reflection.remove_method, obj)
197
+ else
198
+ send(reflection.setter_method, nil)
138
199
  end
139
200
  end
140
- after_save_hook{obj.destroy} if opts[:destroy]
141
- obj
142
201
  end
202
+ after_save_hook{obj.destroy} if opts[:destroy]
203
+ obj
143
204
  end
144
205
 
145
206
  # Set the fields in the obj based on the association, only allowing
@@ -152,42 +213,57 @@ module Sequel
152
213
  end
153
214
  end
154
215
 
155
- # Modify the associated object based on the contents of the attribtues hash:
216
+ # Modify the associated object based on the contents of the attributes hash:
217
+ # * If a :transform block was given to nested_attributes, use it to modify the attribute hash.
156
218
  # * If a block was given to nested_attributes, call it with the attributes and return immediately if the block returns true.
219
+ # * If a primary key exists in the attributes hash and it matches an associated object:
220
+ # ** If _delete is a key in the hash and the :destroy option is used, destroy the matching associated object.
221
+ # ** If _remove is a key in the hash and the :remove option is used, disassociated the matching associated object.
222
+ # ** Otherwise, update the matching associated object with the contents of the hash.
223
+ # * If a primary key exists in the attributes hash but it does not match an associated object,
224
+ # either raise an error, create a new object or ignore the hash, depending on the :unmatched_pk option.
157
225
  # * If no primary key exists in the attributes hash, create a new object.
158
- # * If _delete is a key in the hash and the :destroy option is used, destroy the matching associated object.
159
- # * If _remove is a key in the hash and the :remove option is used, disassociated the matching associated object.
160
- # * Otherwise, update the matching associated object with the contents of the hash.
161
226
  def nested_attributes_setter(reflection, attributes)
227
+ if a = reflection[:nested_attributes][:transform]
228
+ attributes = a.call(self, attributes)
229
+ end
162
230
  return if (b = reflection[:nested_attributes][:reject_if]) && b.call(attributes)
163
231
  modified!
164
232
  klass = reflection.associated_class
165
- if pk = attributes.delete(klass.primary_key) || attributes.delete(klass.primary_key.to_s)
166
- if klass.db.send(:typecast_value_boolean, attributes[:_delete] || attributes['_delete']) && reflection[:nested_attributes][:destroy]
167
- nested_attributes_remove(reflection, pk, :destroy=>true)
168
- elsif klass.db.send(:typecast_value_boolean, attributes[:_remove] || attributes['_remove']) && reflection[:nested_attributes][:remove]
169
- nested_attributes_remove(reflection, pk)
233
+ sym_keys = Array(klass.primary_key)
234
+ str_keys = sym_keys.map{|k| k.to_s}
235
+ if (pk = attributes.values_at(*sym_keys)).all? || (pk = attributes.values_at(*str_keys)).all?
236
+ pk = pk.map{|k| k.to_s}
237
+ obj = Array(send(reflection[:name])).find{|x| Array(x.pk).map{|k| k.to_s} == pk}
238
+ end
239
+ if obj
240
+ attributes = attributes.dup.delete_if{|k,v| str_keys.include? k.to_s}
241
+ if reflection[:nested_attributes][:destroy] && klass.db.send(:typecast_value_boolean, attributes.delete(:_delete) || attributes.delete('_delete'))
242
+ nested_attributes_remove(reflection, obj, :destroy=>true)
243
+ elsif reflection[:nested_attributes][:remove] && klass.db.send(:typecast_value_boolean, attributes.delete(:_remove) || attributes.delete('_remove'))
244
+ nested_attributes_remove(reflection, obj)
170
245
  else
171
- nested_attributes_update(reflection, pk, attributes)
246
+ nested_attributes_update(reflection, obj, attributes)
247
+ end
248
+ elsif pk.all? && reflection[:nested_attributes][:unmatched_pk] != :create
249
+ if reflection[:nested_attributes][:unmatched_pk] == :raise
250
+ raise(Error, "no matching associated object with given primary key (association: #{reflection[:name]}, pk: #{pk})")
172
251
  end
173
252
  else
174
253
  nested_attributes_create(reflection, attributes)
175
254
  end
176
255
  end
177
256
 
178
- # Update the matching associated object with the attributes,
179
- # validating it when the parent object is validated and saving it
180
- # when the parent is saved.
181
- # Returns the object updated, if it exists.
182
- def nested_attributes_update(reflection, pk, attributes)
183
- if obj = nested_attributes_find(reflection, pk)
184
- nested_attributes_update_attributes(reflection, obj, attributes)
185
- after_validation_hook{validate_associated_object(reflection, obj)}
186
- # Don't need to validate the object twice if :validate association option is not false
187
- # and don't want to validate it at all if it is false.
188
- after_save_hook{obj.save_changes(:validate=>false)}
189
- obj
190
- end
257
+ # Update the given object with the attributes, validating it when the
258
+ # parent object is validated and saving it when the parent is saved.
259
+ # Returns the object updated.
260
+ def nested_attributes_update(reflection, obj, attributes)
261
+ nested_attributes_update_attributes(reflection, obj, attributes)
262
+ after_validation_hook{validate_associated_object(reflection, obj)}
263
+ # Don't need to validate the object twice if :validate association option is not false
264
+ # and don't want to validate it at all if it is false.
265
+ after_save_hook{obj.save_changes(:validate=>false)}
266
+ obj
191
267
  end
192
268
 
193
269
  # Update the attributes for the given object related to the current object through the association.
@@ -65,6 +65,14 @@ module Sequel
65
65
  lc = model.lock_column
66
66
  instance_filter(lc=>send(lc))
67
67
  end
68
+
69
+ # Clear the instance filters when refreshing, so that attempting to
70
+ # refresh after a failed save removes the previous lock column filter
71
+ # (the new one will be added before updating).
72
+ def _refresh(ds)
73
+ clear_instance_filters
74
+ super
75
+ end
68
76
 
69
77
  # Only update the row if it has the same lock version, and increment the
70
78
  # lock version.