sequel 3.46.0 → 3.47.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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +96 -0
  3. data/Rakefile +7 -1
  4. data/bin/sequel +6 -4
  5. data/doc/active_record.rdoc +1 -1
  6. data/doc/advanced_associations.rdoc +14 -35
  7. data/doc/association_basics.rdoc +66 -4
  8. data/doc/migration.rdoc +4 -0
  9. data/doc/opening_databases.rdoc +6 -0
  10. data/doc/postgresql.rdoc +302 -0
  11. data/doc/release_notes/3.47.0.txt +270 -0
  12. data/doc/security.rdoc +6 -0
  13. data/lib/sequel/adapters/ibmdb.rb +9 -9
  14. data/lib/sequel/adapters/jdbc.rb +22 -7
  15. data/lib/sequel/adapters/jdbc/postgresql.rb +7 -2
  16. data/lib/sequel/adapters/mock.rb +2 -0
  17. data/lib/sequel/adapters/postgres.rb +44 -13
  18. data/lib/sequel/adapters/shared/mssql.rb +1 -1
  19. data/lib/sequel/adapters/shared/mysql.rb +2 -2
  20. data/lib/sequel/adapters/shared/postgres.rb +94 -55
  21. data/lib/sequel/adapters/shared/sqlite.rb +3 -1
  22. data/lib/sequel/adapters/sqlite.rb +2 -2
  23. data/lib/sequel/adapters/utils/pg_types.rb +1 -14
  24. data/lib/sequel/adapters/utils/split_alter_table.rb +3 -3
  25. data/lib/sequel/connection_pool/threaded.rb +1 -1
  26. data/lib/sequel/core.rb +1 -1
  27. data/lib/sequel/database/connecting.rb +2 -2
  28. data/lib/sequel/database/features.rb +5 -0
  29. data/lib/sequel/database/misc.rb +47 -5
  30. data/lib/sequel/database/query.rb +2 -2
  31. data/lib/sequel/dataset/actions.rb +4 -2
  32. data/lib/sequel/dataset/misc.rb +1 -1
  33. data/lib/sequel/dataset/prepared_statements.rb +1 -1
  34. data/lib/sequel/dataset/query.rb +8 -6
  35. data/lib/sequel/dataset/sql.rb +8 -6
  36. data/lib/sequel/extensions/constraint_validations.rb +5 -2
  37. data/lib/sequel/extensions/migration.rb +10 -8
  38. data/lib/sequel/extensions/pagination.rb +3 -0
  39. data/lib/sequel/extensions/pg_array.rb +85 -25
  40. data/lib/sequel/extensions/pg_hstore.rb +8 -1
  41. data/lib/sequel/extensions/pg_hstore_ops.rb +4 -1
  42. data/lib/sequel/extensions/pg_inet.rb +16 -13
  43. data/lib/sequel/extensions/pg_interval.rb +6 -2
  44. data/lib/sequel/extensions/pg_json.rb +18 -11
  45. data/lib/sequel/extensions/pg_range.rb +17 -2
  46. data/lib/sequel/extensions/pg_range_ops.rb +7 -5
  47. data/lib/sequel/extensions/pg_row.rb +29 -12
  48. data/lib/sequel/extensions/pretty_table.rb +3 -0
  49. data/lib/sequel/extensions/query.rb +3 -0
  50. data/lib/sequel/extensions/schema_caching.rb +2 -0
  51. data/lib/sequel/extensions/schema_dumper.rb +3 -1
  52. data/lib/sequel/extensions/select_remove.rb +3 -0
  53. data/lib/sequel/model.rb +8 -2
  54. data/lib/sequel/model/associations.rb +39 -27
  55. data/lib/sequel/model/base.rb +99 -38
  56. data/lib/sequel/model/plugins.rb +25 -0
  57. data/lib/sequel/plugins/association_autoreloading.rb +27 -22
  58. data/lib/sequel/plugins/association_dependencies.rb +1 -7
  59. data/lib/sequel/plugins/auto_validations.rb +110 -0
  60. data/lib/sequel/plugins/boolean_readers.rb +1 -6
  61. data/lib/sequel/plugins/caching.rb +6 -13
  62. data/lib/sequel/plugins/class_table_inheritance.rb +1 -0
  63. data/lib/sequel/plugins/composition.rb +14 -7
  64. data/lib/sequel/plugins/constraint_validations.rb +2 -13
  65. data/lib/sequel/plugins/defaults_setter.rb +1 -6
  66. data/lib/sequel/plugins/dirty.rb +8 -0
  67. data/lib/sequel/plugins/error_splitter.rb +54 -0
  68. data/lib/sequel/plugins/force_encoding.rb +1 -5
  69. data/lib/sequel/plugins/hook_class_methods.rb +1 -6
  70. data/lib/sequel/plugins/input_transformer.rb +79 -0
  71. data/lib/sequel/plugins/instance_filters.rb +7 -1
  72. data/lib/sequel/plugins/instance_hooks.rb +7 -1
  73. data/lib/sequel/plugins/json_serializer.rb +5 -10
  74. data/lib/sequel/plugins/lazy_attributes.rb +20 -7
  75. data/lib/sequel/plugins/list.rb +1 -6
  76. data/lib/sequel/plugins/many_through_many.rb +1 -2
  77. data/lib/sequel/plugins/many_to_one_pk_lookup.rb +23 -39
  78. data/lib/sequel/plugins/optimistic_locking.rb +1 -5
  79. data/lib/sequel/plugins/pg_row.rb +4 -2
  80. data/lib/sequel/plugins/pg_typecast_on_load.rb +3 -7
  81. data/lib/sequel/plugins/prepared_statements.rb +1 -5
  82. data/lib/sequel/plugins/prepared_statements_safe.rb +2 -11
  83. data/lib/sequel/plugins/rcte_tree.rb +2 -2
  84. data/lib/sequel/plugins/serialization.rb +11 -13
  85. data/lib/sequel/plugins/serialization_modification_detection.rb +13 -1
  86. data/lib/sequel/plugins/single_table_inheritance.rb +4 -4
  87. data/lib/sequel/plugins/static_cache.rb +67 -19
  88. data/lib/sequel/plugins/string_stripper.rb +7 -27
  89. data/lib/sequel/plugins/subclasses.rb +3 -5
  90. data/lib/sequel/plugins/tactical_eager_loading.rb +2 -2
  91. data/lib/sequel/plugins/timestamps.rb +2 -7
  92. data/lib/sequel/plugins/touch.rb +5 -8
  93. data/lib/sequel/plugins/tree.rb +1 -6
  94. data/lib/sequel/plugins/typecast_on_load.rb +1 -5
  95. data/lib/sequel/plugins/update_primary_key.rb +26 -14
  96. data/lib/sequel/plugins/validation_class_methods.rb +31 -16
  97. data/lib/sequel/plugins/validation_helpers.rb +50 -26
  98. data/lib/sequel/plugins/xml_serializer.rb +3 -6
  99. data/lib/sequel/sql.rb +1 -1
  100. data/lib/sequel/version.rb +1 -1
  101. data/spec/adapters/postgres_spec.rb +131 -15
  102. data/spec/adapters/sqlite_spec.rb +1 -1
  103. data/spec/core/connection_pool_spec.rb +16 -17
  104. data/spec/core/database_spec.rb +111 -40
  105. data/spec/core/dataset_spec.rb +65 -74
  106. data/spec/core/expression_filters_spec.rb +6 -5
  107. data/spec/core/object_graph_spec.rb +0 -1
  108. data/spec/core/schema_spec.rb +23 -23
  109. data/spec/core/spec_helper.rb +5 -1
  110. data/spec/extensions/association_dependencies_spec.rb +1 -1
  111. data/spec/extensions/association_proxies_spec.rb +1 -1
  112. data/spec/extensions/auto_validations_spec.rb +90 -0
  113. data/spec/extensions/caching_spec.rb +6 -0
  114. data/spec/extensions/class_table_inheritance_spec.rb +8 -1
  115. data/spec/extensions/composition_spec.rb +12 -5
  116. data/spec/extensions/constraint_validations_spec.rb +4 -4
  117. data/spec/extensions/core_refinements_spec.rb +29 -79
  118. data/spec/extensions/dirty_spec.rb +14 -0
  119. data/spec/extensions/error_splitter_spec.rb +18 -0
  120. data/spec/extensions/identity_map_spec.rb +0 -1
  121. data/spec/extensions/input_transformer_spec.rb +54 -0
  122. data/spec/extensions/instance_filters_spec.rb +6 -0
  123. data/spec/extensions/instance_hooks_spec.rb +12 -1
  124. data/spec/extensions/json_serializer_spec.rb +0 -1
  125. data/spec/extensions/lazy_attributes_spec.rb +64 -55
  126. data/spec/extensions/looser_typecasting_spec.rb +1 -1
  127. data/spec/extensions/many_through_many_spec.rb +3 -4
  128. data/spec/extensions/many_to_one_pk_lookup_spec.rb +53 -15
  129. data/spec/extensions/migration_spec.rb +16 -0
  130. data/spec/extensions/null_dataset_spec.rb +1 -1
  131. data/spec/extensions/pg_array_spec.rb +48 -1
  132. data/spec/extensions/pg_hstore_ops_spec.rb +10 -2
  133. data/spec/extensions/pg_hstore_spec.rb +5 -0
  134. data/spec/extensions/pg_inet_spec.rb +5 -0
  135. data/spec/extensions/pg_interval_spec.rb +7 -3
  136. data/spec/extensions/pg_json_spec.rb +6 -1
  137. data/spec/extensions/pg_range_ops_spec.rb +4 -1
  138. data/spec/extensions/pg_range_spec.rb +5 -0
  139. data/spec/extensions/pg_row_plugin_spec.rb +13 -0
  140. data/spec/extensions/pg_row_spec.rb +28 -19
  141. data/spec/extensions/pg_typecast_on_load_spec.rb +6 -1
  142. data/spec/extensions/prepared_statements_associations_spec.rb +1 -1
  143. data/spec/extensions/query_literals_spec.rb +1 -1
  144. data/spec/extensions/rcte_tree_spec.rb +2 -2
  145. data/spec/extensions/schema_spec.rb +2 -2
  146. data/spec/extensions/serialization_modification_detection_spec.rb +8 -0
  147. data/spec/extensions/serialization_spec.rb +15 -1
  148. data/spec/extensions/sharding_spec.rb +1 -1
  149. data/spec/extensions/single_table_inheritance_spec.rb +1 -1
  150. data/spec/extensions/static_cache_spec.rb +59 -9
  151. data/spec/extensions/tactical_eager_loading_spec.rb +19 -4
  152. data/spec/extensions/update_primary_key_spec.rb +17 -1
  153. data/spec/extensions/validation_class_methods_spec.rb +25 -0
  154. data/spec/extensions/validation_helpers_spec.rb +59 -3
  155. data/spec/integration/associations_test.rb +5 -5
  156. data/spec/integration/eager_loader_test.rb +32 -63
  157. data/spec/integration/model_test.rb +2 -2
  158. data/spec/integration/plugin_test.rb +88 -56
  159. data/spec/integration/prepared_statement_test.rb +1 -1
  160. data/spec/integration/schema_test.rb +1 -1
  161. data/spec/integration/timezone_test.rb +0 -1
  162. data/spec/integration/transaction_test.rb +0 -1
  163. data/spec/model/association_reflection_spec.rb +1 -1
  164. data/spec/model/associations_spec.rb +106 -84
  165. data/spec/model/base_spec.rb +4 -4
  166. data/spec/model/eager_loading_spec.rb +8 -8
  167. data/spec/model/model_spec.rb +27 -9
  168. data/spec/model/plugins_spec.rb +71 -0
  169. data/spec/model/record_spec.rb +99 -13
  170. metadata +12 -2
@@ -22,6 +22,10 @@ module Sequel
22
22
  # with all of these modules.
23
23
  attr_reader :dataset_method_modules
24
24
 
25
+ # The default options to use for Model#set_fields. These are merged with
26
+ # the options given to set_fields.
27
+ attr_accessor :default_set_fields_options
28
+
25
29
  # SQL string fragment used for faster DELETE statement creation when deleting/destroying
26
30
  # model instances, or nil if the optimization should not be used. For internal use only.
27
31
  attr_reader :fast_instance_delete_sql
@@ -49,10 +53,9 @@ module Sequel
49
53
  # Whether to raise an error when unable to typecast data for a column
50
54
  # (default: true). This should be set to false if you want to use
51
55
  # validations to display nice error messages to the user (e.g. most
52
- # web applications). You can use the validates_not_string validations
53
- # (from either the validation_helpers or validation_class_methods standard
54
- # plugins) in connection with option to check for typecast failures for
55
- # columns that aren't blobs or strings.
56
+ # web applications). You can use the validates_schema_types validation
57
+ # (from the validation_helpers plugin) in connection with this setting to
58
+ # check for typecast failures during validation.
56
59
  attr_accessor :raise_on_typecast_failure
57
60
 
58
61
  # Whether to raise an error if an UPDATE or DELETE query related to
@@ -245,6 +248,10 @@ module Sequel
245
248
  #
246
249
  # Sequel::Model.db = DB1
247
250
  # Artist.db = DB2
251
+ #
252
+ # Note that you should not use this to change the model's database
253
+ # at runtime. If you have that need, you should look into Sequel's
254
+ # sharding support.
248
255
  def db=(db)
249
256
  @db = db
250
257
  set_dataset(db.dataset(@dataset.opts)) if @dataset
@@ -350,11 +357,22 @@ module Sequel
350
357
  def inherited(subclass)
351
358
  super
352
359
  ivs = subclass.instance_variables.collect{|x| x.to_s}
353
- EMPTY_INSTANCE_VARIABLES.each{|iv| subclass.instance_variable_set(iv, nil) unless ivs.include?(iv.to_s)}
354
- INHERITED_INSTANCE_VARIABLES.each do |iv, dup|
360
+ inherited_instance_variables.each do |iv, dup|
355
361
  next if ivs.include?(iv.to_s)
356
- sup_class_value = instance_variable_get(iv)
357
- sup_class_value = sup_class_value.dup if dup == :dup && sup_class_value
362
+ if (sup_class_value = instance_variable_get(iv)) && dup
363
+ sup_class_value = case dup
364
+ when :dup
365
+ sup_class_value.dup
366
+ when :hash_dup
367
+ h = {}
368
+ sup_class_value.each{|k,v| h[k] = v.dup}
369
+ h
370
+ when Proc
371
+ dup.call(sup_class_value)
372
+ else
373
+ raise Error, "bad inherited instance variable type: #{dup.inspect}"
374
+ end
375
+ end
358
376
  subclass.instance_variable_set(iv, sup_class_value)
359
377
  end
360
378
  unless ivs.include?("@dataset")
@@ -369,7 +387,7 @@ module Sequel
369
387
  end
370
388
  end
371
389
  end
372
-
390
+
373
391
  # Returns the implicit table name for the model class, which is the demodulized,
374
392
  # underscored, pluralized name of the class.
375
393
  #
@@ -497,6 +515,10 @@ module Sequel
497
515
  #
498
516
  # Artist.set_dataset(:tbl_artists)
499
517
  # Artist.set_dataset(DB[:artists])
518
+ #
519
+ # Note that you should not use this to change the model's dataset
520
+ # at runtime. If you have that need, you should look into Sequel's
521
+ # sharding support.
500
522
  def set_dataset(ds, opts={})
501
523
  inherited = opts[:inherited]
502
524
  @dataset = case ds
@@ -647,7 +669,7 @@ module Sequel
647
669
  # module if the model has a dataset. Add dataset methods to the class for all
648
670
  # public dataset methods.
649
671
  def dataset_extend(mod)
650
- @dataset.extend(mod) if @dataset
672
+ @dataset.extend(mod) if defined?(@dataset) && @dataset
651
673
  reset_instance_dataset
652
674
  dataset_method_modules << mod
653
675
  mod.public_instance_methods.each{|meth| def_model_dataset_method(meth)}
@@ -732,6 +754,14 @@ module Sequel
732
754
  schema_hash
733
755
  end
734
756
 
757
+ # A hash of instance variables to automatically set up in subclasses.
758
+ # See Sequel::Model::INHERITED_INSTANCE_VARIABLES. It is safe to modify
759
+ # the hash returned by this method, though it may not be safe to modify
760
+ # values of the hash.
761
+ def inherited_instance_variables
762
+ INHERITED_INSTANCE_VARIABLES.dup
763
+ end
764
+
735
765
  # For the given opts hash and default name or :class option, add a
736
766
  # :class_name option unless already present which contains the name
737
767
  # of the class to use as a string. The purpose is to allow late
@@ -750,7 +780,7 @@ module Sequel
750
780
  # Module that the class includes that holds methods the class adds for column accessors and
751
781
  # associations so that the methods can be overridden with +super+.
752
782
  def overridable_methods_module
753
- include(@overridable_methods_module = Module.new) unless @overridable_methods_module
783
+ include(@overridable_methods_module = Module.new) unless defined?(@overridable_methods_module) && @overridable_methods_module
754
784
  @overridable_methods_module
755
785
  end
756
786
 
@@ -818,7 +848,7 @@ module Sequel
818
848
  # Reset the instance dataset to a modified copy of the current dataset,
819
849
  # should be used whenever the model's dataset is modified.
820
850
  def reset_instance_dataset
821
- @instance_dataset = @dataset.limit(1).naked if @dataset
851
+ @instance_dataset = @dataset.limit(1).naked if defined?(@dataset) && @dataset
822
852
  end
823
853
 
824
854
  # Set the columns for this model and create accessor methods for each column.
@@ -841,7 +871,7 @@ module Sequel
841
871
  end
842
872
 
843
873
  # Add model methods that call dataset methods
844
- DATASET_METHODS.each{|arg| class_eval("def #{arg}(*args, &block); dataset.#{arg}(*args, &block) end", __FILE__, __LINE__)}
874
+ Plugins.def_dataset_methods(self, DATASET_METHODS)
845
875
 
846
876
  # Returns a copy of the model's dataset with custom SQL
847
877
  #
@@ -891,7 +921,7 @@ module Sequel
891
921
  private_class_method :class_attr_overridable, :class_attr_reader
892
922
 
893
923
  class_attr_reader :columns, :db, :primary_key, :db_schema
894
- class_attr_overridable *BOOLEAN_SETTINGS
924
+ class_attr_overridable(*BOOLEAN_SETTINGS)
895
925
 
896
926
  # The hash of attribute values. Keys are symbols with the names of the
897
927
  # underlying database columns.
@@ -1077,7 +1107,7 @@ module Sequel
1077
1107
  errors
1078
1108
  validate
1079
1109
  errors.freeze
1080
- this.freeze unless new?
1110
+ this.freeze if !new? && model.primary_key
1081
1111
  super
1082
1112
  end
1083
1113
 
@@ -1153,7 +1183,18 @@ module Sequel
1153
1183
  # a.save_changes # No callbacks run, as no changes
1154
1184
  # a.modified!
1155
1185
  # a.save_changes # Callbacks run, even though no changes made
1156
- def modified!
1186
+ #
1187
+ # If a column is given, specifically marked that column as modified,
1188
+ # so that +save_changes+/+update+ will include that column in the
1189
+ # update. This should be used if you plan on mutating the column
1190
+ # value instead of assigning a new column value:
1191
+ #
1192
+ # a.modified!(:name)
1193
+ # a.name.gsub!(/[aeou]/, 'i')
1194
+ def modified!(column=nil)
1195
+ if column && !changed_columns.include?(column)
1196
+ changed_columns << column
1197
+ end
1157
1198
  @modified = true
1158
1199
  end
1159
1200
 
@@ -1165,8 +1206,19 @@ module Sequel
1165
1206
  # a.modified? # => false
1166
1207
  # a.set(:name=>'Jim')
1167
1208
  # a.modified? # => true
1168
- def modified?
1169
- @modified || !changed_columns.empty?
1209
+ #
1210
+ # If a column is given, specifically check if the given column has
1211
+ # been modified:
1212
+ #
1213
+ # a.modified?(:num_albums) # => false
1214
+ # a.num_albums = 10
1215
+ # a.modified?(:num_albums) # => true
1216
+ def modified?(column=nil)
1217
+ if column
1218
+ changed_columns.include?(column)
1219
+ else
1220
+ @modified || !changed_columns.empty?
1221
+ end
1170
1222
  end
1171
1223
 
1172
1224
  # Returns true if the current instance represents a new record.
@@ -1331,28 +1383,30 @@ module Sequel
1331
1383
  # artist.set_fields({}, [:name], :missing=>:raise)
1332
1384
  # # Sequel::Error raised
1333
1385
  def set_fields(hash, fields, opts=nil)
1334
- if opts
1335
- case opts[:missing]
1336
- when :skip
1337
- fields.each do |f|
1338
- if hash.has_key?(f)
1339
- send("#{f}=", hash[f])
1340
- elsif f.is_a?(Symbol) && hash.has_key?(sf = f.to_s)
1341
- send("#{sf}=", hash[sf])
1342
- end
1386
+ opts = if opts
1387
+ model.default_set_fields_options.merge(opts)
1388
+ else
1389
+ model.default_set_fields_options
1390
+ end
1391
+
1392
+ case opts[:missing]
1393
+ when :skip
1394
+ fields.each do |f|
1395
+ if hash.has_key?(f)
1396
+ send("#{f}=", hash[f])
1397
+ elsif f.is_a?(Symbol) && hash.has_key?(sf = f.to_s)
1398
+ send("#{sf}=", hash[sf])
1343
1399
  end
1344
- when :raise
1345
- fields.each do |f|
1346
- if hash.has_key?(f)
1347
- send("#{f}=", hash[f])
1348
- elsif f.is_a?(Symbol) && hash.has_key?(sf = f.to_s)
1349
- send("#{sf}=", hash[sf])
1350
- else
1351
- raise(Sequel::Error, "missing field in hash: #{f.inspect} not in #{hash.inspect}")
1352
- end
1400
+ end
1401
+ when :raise
1402
+ fields.each do |f|
1403
+ if hash.has_key?(f)
1404
+ send("#{f}=", hash[f])
1405
+ elsif f.is_a?(Symbol) && hash.has_key?(sf = f.to_s)
1406
+ send("#{sf}=", hash[sf])
1407
+ else
1408
+ raise(Sequel::Error, "missing field in hash: #{f.inspect} not in #{hash.inspect}")
1353
1409
  end
1354
- else
1355
- fields.each{|f| send("#{f}=", hash[f])}
1356
1410
  end
1357
1411
  else
1358
1412
  fields.each{|f| send("#{f}=", hash[f])}
@@ -1761,6 +1815,13 @@ module Sequel
1761
1815
  raise HookFailed.new("the #{type} hook failed", self)
1762
1816
  end
1763
1817
 
1818
+ # Get the ruby class or classes related to the given column's type.
1819
+ def schema_type_class(column)
1820
+ if (sch = db_schema[column]) && (type = sch[:type])
1821
+ db.schema_type_class(type)
1822
+ end
1823
+ end
1824
+
1764
1825
  # Set the columns, filtered by the only and except arrays.
1765
1826
  def set_restricted(hash, only, except)
1766
1827
  return self if hash.empty?
@@ -20,5 +20,30 @@ module Sequel
20
20
  # every time the Model.plugin method is called, after including/extending
21
21
  # any modules.
22
22
  module Plugins
23
+ # In the given module +mod+, define methods that are call the same method
24
+ # on the dataset. This is designed for plugins to define dataset methods
25
+ # inside ClassMethods that call the implementations in DatasetMethods.
26
+ def self.def_dataset_methods(mod, meths)
27
+ Array(meths).each do |meth|
28
+ mod.class_eval("def #{meth}(*args, &block); dataset.#{meth}(*args, &block) end", __FILE__, __LINE__)
29
+ end
30
+ end
31
+
32
+ # Add method to +mod+ that overrides inherited_instance_variables to include the
33
+ # values in this hash.
34
+ def self.inherited_instance_variables(mod, hash)
35
+ mod.send(:define_method, :inherited_instance_variables) do ||
36
+ super().merge!(hash)
37
+ end
38
+ end
39
+
40
+ # Add method to +mod+ that overrides set_dataset to call the method afterward.
41
+ def self.after_set_dataset(mod, meth)
42
+ mod.send(:define_method, :set_dataset) do |*a|
43
+ r = super(*a)
44
+ send(meth)
45
+ r
46
+ end
47
+ end
23
48
  end
24
49
  end
@@ -12,37 +12,42 @@ module Sequel
12
12
  # album.artist # reloads associated artist
13
13
  #
14
14
  module AssociationAutoreloading
15
+ def self.apply(model)
16
+ model.instance_variable_set(:@autoreloading_associations, {})
17
+ end
18
+
15
19
  module ClassMethods
16
- private
20
+ # Hash with column symbol keys and arrays of many_to_one
21
+ # association symbols that should be cleared when the column
22
+ # value changes.
23
+ attr_reader :autoreloading_associations
17
24
 
18
- # Create a setter method for +key+ in an anonymous module included
19
- # in the class that calls super and clears the cache for
20
- # the given array of associations.
21
- def create_autoreloading_association_setter(key, assocs)
22
- include(@autoreloading_associations_module ||= Module.new) unless @autoreloading_associations_module
23
- @autoreloading_associations_module.class_eval do
24
- unless method_defined?("#{key}=")
25
- define_method("#{key}=") do |v|
26
- o = send(key)
27
- super(v)
28
- assocs.each{|a| associations.delete(a)} if send(key) != o
29
- end
30
- end
31
- end
32
- end
25
+ Plugins.inherited_instance_variables(self, :@autoreloading_associations=>:hash_dup)
26
+
27
+ private
33
28
 
34
- # For each of the foreign keys in the association, create
35
- # a setter method that will clear the association cache.
29
+ # Add the association to the array of associations to clear for
30
+ # each of the foreign key columns.
36
31
  def def_many_to_one(opts)
37
32
  super
38
- @autoreloading_associations ||= {}
39
33
  opts[:keys].each do |key|
40
- assocs = @autoreloading_associations[key] ||= []
41
- assocs << opts[:name]
42
- create_autoreloading_association_setter(key, assocs)
34
+ (@autoreloading_associations[key] ||= []) << opts[:name]
43
35
  end
44
36
  end
45
37
  end
38
+
39
+ module InstanceMethods
40
+ private
41
+
42
+ # If a foreign key column value changes, clear the related
43
+ # cached associations.
44
+ def change_column_value(column, value)
45
+ if assocs = model.autoreloading_associations[column]
46
+ assocs.each{|a| associations.delete(a)}
47
+ end
48
+ super
49
+ end
50
+ end
46
51
  end
47
52
  end
48
53
  end
@@ -71,13 +71,7 @@ module Sequel
71
71
  end
72
72
  end
73
73
 
74
- # Copy the current model object's association_dependencies into the subclass.
75
- def inherited(subclass)
76
- super
77
- ad = association_dependencies.dup
78
- ad.keys.each{|k| ad[k] = ad[k].dup}
79
- subclass.instance_variable_set(:@association_dependencies, ad)
80
- end
74
+ Plugins.inherited_instance_variables(self, :@association_dependencies=>:hash_dup)
81
75
  end
82
76
 
83
77
  module InstanceMethods
@@ -0,0 +1,110 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The auto_validations plugin automatically sets up three types of validations
4
+ # for your model columns:
5
+ #
6
+ # 1. type validations for all columns
7
+ # 2. presence validations on NOT NULL columns
8
+ # 3. unique validations on columns or sets of columns with unique indexes
9
+ #
10
+ # To determine the columns to use for the presence validations and the types for the type validations,
11
+ # the plugin looks at the database schema for the model's table. To determine
12
+ # the unique validations, Sequel looks at the indexes on the table. In order
13
+ # for this plugin to be fully functional, the underlying database adapter needs
14
+ # to support both schema and index parsing.
15
+ #
16
+ # This plugin uses the validation_helpers plugin underneath to implement the
17
+ # validations. It does not allow for any per-column validation message
18
+ # customization, but you can alter the messages for the given type of validation
19
+ # on a per-model basis (see the validation_helpers documentation).
20
+ #
21
+ # You can skip certain types of validations from being automatically added via:
22
+ #
23
+ # Model.skip_auto_validations(:presence)
24
+ #
25
+ # If you want to skip all auto validations (only useful if loading the plugin
26
+ # in a superclass):
27
+ #
28
+ # Model.skip_auto_validations(:all)
29
+ #
30
+ # Usage:
31
+ #
32
+ # # Make all model subclass use auto validations (called before loading subclasses)
33
+ # Sequel::Model.plugin :auto_validations
34
+ #
35
+ # # Make the Album class use auto validations
36
+ # Album.plugin :auto_validations
37
+ module AutoValidations
38
+ # Load the validation_helpers plugin and setup data structures.
39
+ def self.apply(model)
40
+ model.instance_eval do
41
+ plugin :validation_helpers
42
+ @auto_validate_presence_columns = []
43
+ @auto_validate_unique_columns = []
44
+ @auto_validate_types = true
45
+ end
46
+ end
47
+
48
+ # Setup auto validations for the model if it has a dataset.
49
+ def self.configure(model)
50
+ model.instance_eval do
51
+ setup_auto_validations if @dataset
52
+ end
53
+ end
54
+
55
+ module ClassMethods
56
+ # The columns with automatic presence validations
57
+ attr_reader :auto_validate_presence_columns
58
+
59
+ # The columns or sets of columns with automatic unique validations
60
+ attr_reader :auto_validate_unique_columns
61
+
62
+ Plugins.inherited_instance_variables(self, :@auto_validate_types=>nil, :@auto_validate_presence_columns=>:dup, :@auto_validate_unique_columns=>:dup)
63
+ Plugins.after_set_dataset(self, :setup_auto_validations)
64
+
65
+ # Whether to automatically validate schema types for all columns
66
+ def auto_validate_types?
67
+ @auto_validate_types
68
+ end
69
+
70
+ # Skip automatic validations for the given validation type (:presence, :types, :unique).
71
+ # If :all is given as the type, skip all auto validations.
72
+ def skip_auto_validations(type)
73
+ if type == :all
74
+ [:presence, :types, :unique].each{|v| skip_auto_validations(v)}
75
+ elsif type == :types
76
+ @auto_validate_types = false
77
+ else
78
+ send("auto_validate_#{type}_columns").clear
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # Parse the database schema and indexes and record the columns to automatically validate.
85
+ def setup_auto_validations
86
+ @auto_validate_presence_columns = db_schema.select{|col, sch| sch[:allow_null] == false && sch[:ruby_default].nil?}.map{|col, sch| col} - Array(primary_key)
87
+ @auto_validate_unique_columns = if db.respond_to?(:indexes)
88
+ db.indexes(dataset.first_source_table).select{|name, idx| idx[:unique] == true}.map{|name, idx| idx[:columns]}
89
+ else
90
+ []
91
+ end
92
+ end
93
+ end
94
+
95
+ module InstanceMethods
96
+ # Validate the model's auto validations columns
97
+ def validate
98
+ super
99
+ if presence_columns = model.auto_validate_presence_columns
100
+ validates_not_null(presence_columns)
101
+ end
102
+
103
+ validates_schema_types if model.auto_validate_types?
104
+
105
+ model.auto_validate_unique_columns.each{|cols| validates_unique(cols)}
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end