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
@@ -45,17 +45,15 @@ module Sequel
45
45
  Sequel.synchronize{_descendents}
46
46
  end
47
47
 
48
+ Plugins.inherited_instance_variables(self, :@subclasses=>lambda{|v| []}, :@on_subclass=>nil)
49
+
48
50
  # Add the subclass to this model's current subclasses,
49
51
  # and initialize a new subclasses instance variable
50
52
  # in the subclass.
51
53
  def inherited(subclass)
52
54
  super
53
55
  Sequel.synchronize{subclasses << subclass}
54
- subclass.instance_variable_set(:@subclasses, [])
55
- if on_subclass
56
- subclass.instance_variable_set(:@on_subclass, on_subclass)
57
- on_subclass.call(subclass)
58
- end
56
+ on_subclass.call(subclass) if on_subclass
59
57
  end
60
58
 
61
59
  private
@@ -42,9 +42,9 @@ module Sequel
42
42
  # objects retrieved with the current object.
43
43
  def load_associated_objects(opts, reload=false)
44
44
  name = opts[:name]
45
- if !associations.include?(name) && retrieved_by
45
+ if !associations.include?(name) && retrieved_by && !frozen?
46
46
  begin
47
- retrieved_by.send(:eager_load, retrieved_with, name=>{})
47
+ retrieved_by.send(:eager_load, retrieved_with.reject{|o| o.frozen?}, name=>{})
48
48
  rescue Sequel::UndefinedAssociation
49
49
  # This can happen if class table inheritance is used and the association
50
50
  # is only defined in a subclass. This particular instance can use the
@@ -47,13 +47,8 @@ module Sequel
47
47
  @create_timestamp_overwrite
48
48
  end
49
49
 
50
- # Copy the class instance variables used from the superclass to the subclass
51
- def inherited(subclass)
52
- super
53
- [:@create_timestamp_field, :@update_timestamp_field, :@create_timestamp_overwrite, :@set_update_timestamp_on_create].each do |iv|
54
- subclass.instance_variable_set(iv, instance_variable_get(iv))
55
- end
56
- end
50
+ Plugins.inherited_instance_variables(self, :@create_timestamp_field=>nil, :@update_timestamp_field=>nil,
51
+ :@create_timestamp_overwrite=>nil, :@set_update_timestamp_on_create=>nil)
57
52
 
58
53
  # Whether to set the update timestamp to the create timestamp when creating
59
54
  def set_update_timestamp_on_create?
@@ -30,6 +30,10 @@ module Sequel
30
30
  # The default column to update when touching
31
31
  TOUCH_COLUMN_DEFAULT = :updated_at
32
32
 
33
+ def self.apply(model, opts={})
34
+ model.instance_variable_set(:@touched_associations, {})
35
+ end
36
+
33
37
  # Set the touch_column and touched_associations variables for the model.
34
38
  # Options:
35
39
  # * :associations - The associations to touch when a model instance is
@@ -41,7 +45,6 @@ module Sequel
41
45
  # * :column - The column to modify when touching a model instance.
42
46
  def self.configure(model, opts={})
43
47
  model.touch_column = opts[:column] || TOUCH_COLUMN_DEFAULT if opts[:column] || !model.touch_column
44
- model.instance_variable_set(:@touched_associations, {})
45
48
  model.touch_associations(opts[:associations]) if opts[:associations]
46
49
  end
47
50
 
@@ -56,13 +59,7 @@ module Sequel
56
59
  # are column name symbols.
57
60
  attr_reader :touched_associations
58
61
 
59
- # Set the touch_column for the subclass to be the same as the current class.
60
- # Also, create a copy of the touched_associations in the subclass.
61
- def inherited(subclass)
62
- super
63
- subclass.touch_column = touch_column
64
- subclass.instance_variable_set(:@touched_associations, touched_associations.dup)
65
- end
62
+ Plugins.inherited_instance_variables(self, :@touched_associations=>:dup, :@touch_column=>nil)
66
63
 
67
64
  # Add additional associations to be touched. See the :association option
68
65
  # of the Sequel::Plugin::Touch.configure method for the format of the associations
@@ -55,12 +55,7 @@ module Sequel
55
55
  # parent of the leaf.
56
56
  attr_accessor :parent_column
57
57
 
58
- # Copy the +parent_column+ and +order_column+ to the subclass.
59
- def inherited(subclass)
60
- super
61
- subclass.parent_column = parent_column
62
- subclass.tree_order = tree_order
63
- end
58
+ Plugins.inherited_instance_variables(self, :@parent_column=>nil, :@tree_order=>nil)
64
59
 
65
60
  # Returns list of all root nodes (those with no parent nodes).
66
61
  #
@@ -36,11 +36,7 @@ module Sequel
36
36
  @typecast_on_load_columns.concat(columns)
37
37
  end
38
38
 
39
- # Give the subclass a copy of the typecast on load columns.
40
- def inherited(subclass)
41
- super
42
- subclass.instance_variable_set(:@typecast_on_load_columns, typecast_on_load_columns.dup)
43
- end
39
+ Plugins.inherited_instance_variables(self, :@typecast_on_load_columns=>:dup)
44
40
  end
45
41
 
46
42
  module InstanceMethods
@@ -20,27 +20,39 @@ module Sequel
20
20
  # # Make the Album class support primary key updates
21
21
  # Album.plugin :update_primary_key
22
22
  module UpdatePrimaryKey
23
- module ClassMethods
24
- # Cache the pk_hash when loading records
25
- def call(h)
26
- r = super(h)
27
- r.pk_hash
28
- r
29
- end
30
- end
31
-
32
23
  module InstanceMethods
33
- # Clear the pk_hash and object dataset cache, and recache
34
- # the pk_hash
24
+ # Clear the cached primary key.
35
25
  def after_update
36
26
  super
37
27
  @pk_hash = nil
38
- pk_hash
39
28
  end
40
29
 
41
- # Cache the pk_hash instead of generating it every time
30
+ # Use the cached primary key if one is present.
42
31
  def pk_hash
43
- @pk_hash ||= super
32
+ @pk_hash || super
33
+ end
34
+
35
+ private
36
+
37
+ # If the primary key column changes, clear related associations and cache
38
+ # the previous primary key values.
39
+ def change_column_value(column, value)
40
+ pk = primary_key
41
+ if (pk.is_a?(Array) ? pk.include?(column) : pk == column)
42
+ @pk_hash ||= pk_hash unless new?
43
+ clear_associations_using_primary_key
44
+ end
45
+ super
46
+ end
47
+
48
+ # Clear associations that are likely to be tied to the primary key.
49
+ # Note that this currently can clear additional options that don't reference
50
+ # the primary key (such as one_to_many columns referencing a column other than the
51
+ # primary key).
52
+ def clear_associations_using_primary_key
53
+ associations.keys.each do |k|
54
+ associations.delete(k) if model.association_reflection(k)[:type] != :many_to_one
55
+ end
44
56
  end
45
57
  end
46
58
  end
@@ -21,7 +21,6 @@ module Sequel
21
21
  # Setup the validations hash for the given model.
22
22
  def self.apply(model)
23
23
  model.class_eval do
24
- @validation_mutex = Mutex.new
25
24
  @validations = {}
26
25
  @validation_reflections = {}
27
26
  end
@@ -63,21 +62,15 @@ module Sequel
63
62
  !validations.empty?
64
63
  end
65
64
 
66
- # Setup the validations and validation_reflections hash in the subclass.
67
- def inherited(subclass)
68
- vr = @validation_reflections
69
- subclass.class_eval do
70
- @validation_mutex = Mutex.new
71
- @validations = {}
72
- h = {}
73
- vr.each{|k,v| h[k] = v.dup}
74
- @validation_reflections = h
75
- end
76
- super
77
- end
78
-
65
+ Plugins.inherited_instance_variables(self, :@validations=>:hash_dup, :@validation_reflections=>:hash_dup)
66
+
79
67
  # Instructs the model to skip validations defined in superclasses
80
68
  def skip_superclass_validations
69
+ superclass.validations.each do |att, procs|
70
+ if ps = @validations[att]
71
+ @validations[att] -= procs
72
+ end
73
+ end
81
74
  @skip_superclass_validations = true
82
75
  end
83
76
 
@@ -107,7 +100,6 @@ module Sequel
107
100
 
108
101
  # Validates the given instance.
109
102
  def validate(o)
110
- superclass.validate(o) if superclass.respond_to?(:validate) && !skip_superclass_validations?
111
103
  validations.each do |att, procs|
112
104
  v = case att
113
105
  when Array
@@ -200,7 +192,7 @@ module Sequel
200
192
  end
201
193
  tag = opts[:tag]
202
194
  atts.each do |a|
203
- a_vals = @validation_mutex.synchronize{validations[a] ||= []}
195
+ a_vals = Sequel.synchronize{validations[a] ||= []}
204
196
  if tag && (old = a_vals.find{|x| x[0] == tag})
205
197
  old[1] = blk
206
198
  else
@@ -362,6 +354,28 @@ module Sequel
362
354
  end
363
355
  end
364
356
 
357
+ # Validates whether an attribute has the correct ruby type for the associated
358
+ # database type. This is generally useful in conjunction with
359
+ # raise_on_typecast_failure = false, to handle typecasting errors at validation
360
+ # time instead of at setter time.
361
+ #
362
+ # Possible Options:
363
+ # * :message - The message to use (default: 'is not a valid (integer|datetime|etc.)')
364
+ def validates_schema_type(*atts)
365
+ opts = {
366
+ :tag => :schema_type,
367
+ }.merge!(extract_options!(atts))
368
+ reflect_validation(:schema_type, opts, atts)
369
+ atts << opts
370
+ validates_each(*atts) do |o, a, v|
371
+ next if v.nil? || (klass = o.send(:schema_type_class, a)).nil?
372
+ if klass.is_a?(Array) ? !klass.any?{|kls| v.is_a?(kls)} : !v.is_a?(klass)
373
+ message = opts[:message] || "is not a valid #{Array(klass).join(" or ").downcase}"
374
+ o.errors.add(a, message)
375
+ end
376
+ end
377
+ end
378
+
365
379
  # Validates only if the fields in the model (specified by atts) are
366
380
  # unique in the database. Pass an array of fields instead of multiple
367
381
  # fields to specify that the combination of fields must be unique,
@@ -446,6 +460,7 @@ module Sequel
446
460
  # Validates the object.
447
461
  def validate
448
462
  model.validate(self)
463
+ super
449
464
  end
450
465
  end
451
466
  end
@@ -6,35 +6,36 @@ module Sequel
6
6
  # Sequel::Model.plugin :validation_helpers
7
7
  # class Album < Sequel::Model
8
8
  # def validate
9
+ # super
9
10
  # validates_min_length 1, :num_tracks
10
11
  # end
11
12
  # end
12
13
  #
13
- # The validates_unique validation has a unique API, but the other validations have
14
- # the API explained here:
14
+ # The validates_unique and validates_schema_types methods have a unique API, but the other
15
+ # validations have the API explained here:
15
16
  #
16
17
  # Arguments:
17
- # * atts - Single attribute symbol or an array of attribute symbols specifying the
18
- # attribute(s) to validate.
18
+ # atts :: Single attribute symbol or an array of attribute symbols specifying the
19
+ # attribute(s) to validate.
19
20
  # Options:
20
- # * :allow_blank - Whether to skip the validation if the value is blank. You should
21
- # make sure all objects respond to blank if you use this option, which you can do by:
21
+ # :allow_blank :: Whether to skip the validation if the value is blank. You should
22
+ # make sure all objects respond to blank if you use this option, which you can do by:
22
23
  # Sequel.extension :blank
23
- # * :allow_missing - Whether to skip the validation if the attribute isn't a key in the
24
- # values hash. This is different from allow_nil, because Sequel only sends the attributes
25
- # in the values when doing an insert or update. If the attribute is not present, Sequel
26
- # doesn't specify it, so the database will use the table's default value. This is different
27
- # from having an attribute in values with a value of nil, which Sequel will send as NULL.
28
- # If your database table has a non NULL default, this may be a good option to use. You
29
- # don't want to use allow_nil, because if the attribute is in values but has a value nil,
30
- # Sequel will attempt to insert a NULL value into the database, instead of using the
31
- # database's default.
32
- # * :allow_nil - Whether to skip the validation if the value is nil.
33
- # * :message - The message to use. Can be a string which is used directly, or a
34
- # proc which is called. If the validation method takes a argument before the array of attributes,
35
- # that argument is passed as an argument to the proc. The exception is the
36
- # validates_not_string method, which doesn't take an argument, but passes
37
- # the schema type symbol as the argument to the proc.
24
+ # :allow_missing :: Whether to skip the validation if the attribute isn't a key in the
25
+ # values hash. This is different from allow_nil, because Sequel only sends the attributes
26
+ # in the values when doing an insert or update. If the attribute is not present, Sequel
27
+ # doesn't specify it, so the database will use the table's default value. This is different
28
+ # from having an attribute in values with a value of nil, which Sequel will send as NULL.
29
+ # If your database table has a non NULL default, this may be a good option to use. You
30
+ # don't want to use allow_nil, because if the attribute is in values but has a value nil,
31
+ # Sequel will attempt to insert a NULL value into the database, instead of using the
32
+ # database's default.
33
+ # :allow_nil :: Whether to skip the validation if the value is nil.
34
+ # :message :: The message to use. Can be a string which is used directly, or a
35
+ # proc which is called. If the validation method takes a argument before the array of attributes,
36
+ # that argument is passed as an argument to the proc. The exception is the
37
+ # validates_not_string method, which doesn't take an argument, but passes
38
+ # the schema type symbol as the argument to the proc.
38
39
  #
39
40
  # The default validation options for all models can be modified by
40
41
  # changing the values of the Sequel::Plugins::ValidationHelpers::DEFAULT_OPTIONS hash. You
@@ -83,13 +84,14 @@ module Sequel
83
84
  :length_range=>{:message=>lambda{|range| "is too short or too long"}},
84
85
  :max_length=>{:message=>lambda{|max| "is longer than #{max} characters"}, :nil_message=>lambda{"is not present"}},
85
86
  :min_length=>{:message=>lambda{|min| "is shorter than #{min} characters"}},
87
+ :not_null=>{:message=>lambda{"is not present"}},
86
88
  :not_string=>{:message=>lambda{|type| type ? "is not a valid #{type}" : "is a string"}},
87
89
  :numeric=>{:message=>lambda{"is not a number"}},
88
- :type=>{:message=>lambda{|klass| "is not a #{klass}"}},
90
+ :type=>{:message=>lambda{|klass| klass.is_a?(Array) ? "is not a valid #{klass.join(" or ").downcase}" : "is not a valid #{klass.to_s.downcase}"}},
89
91
  :presence=>{:message=>lambda{"is not present"}},
90
92
  :unique=>{:message=>lambda{'is already taken'}}
91
93
  }
92
-
94
+
93
95
  module InstanceMethods
94
96
  # Check that the attribute values are the given exact length.
95
97
  def validates_exact_length(exact, atts, opts={})
@@ -136,6 +138,11 @@ module Sequel
136
138
  validatable_attributes_for_type(:min_length, atts, opts){|a,v,m| validation_error_message(m, min) unless v && v.length >= min}
137
139
  end
138
140
 
141
+ # Check attribute value(s) are not NULL/nil.
142
+ def validates_not_null(atts, opts={})
143
+ validatable_attributes_for_type(:not_null, atts, opts){|a,v,m| validation_error_message(m) if v.nil?}
144
+ end
145
+
139
146
  # Check that the attribute value(s) is not a string. This is generally useful
140
147
  # in conjunction with raise_on_typecast_failure = false, where you are
141
148
  # passing in string values for non-string attributes (such as numbers and dates).
@@ -158,12 +165,28 @@ module Sequel
158
165
  end
159
166
  end
160
167
 
161
- # Check if value is an instance of a class
168
+ # Validates for all of the model columns (or just the given columns)
169
+ # that the column value is an instance of the expected class based on
170
+ # the column's schema type.
171
+ def validates_schema_types(atts=keys)
172
+ Array(atts).each do |k|
173
+ next unless type = schema_type_class(k)
174
+ validates_type(type, k)
175
+ end
176
+ end
177
+
178
+ # Check if value is an instance of a class. If +klass+ is an array,
179
+ # the value must be an instance of one of the classes in the array.
162
180
  def validates_type(klass, atts, opts={})
163
181
  klass = klass.to_s.constantize if klass.is_a?(String) || klass.is_a?(Symbol)
164
- validatable_attributes_for_type(:type, atts, opts){|a,v,m| validation_error_message(m, klass) if !v.nil? && !v.is_a?(klass)}
182
+ validatable_attributes_for_type(:type, atts, opts) do |a,v,m|
183
+ next if v.nil?
184
+ if klass.is_a?(Array) ? !klass.any?{|kls| v.is_a?(kls)} : !v.is_a?(klass)
185
+ validation_error_message(m, klass)
186
+ end
187
+ end
165
188
  end
166
-
189
+
167
190
  # Check attribute value(s) is not considered blank by the database, but allow false values.
168
191
  def validates_presence(atts, opts={})
169
192
  validatable_attributes_for_type(:presence, atts, opts){|a,v,m| validation_error_message(m) if model.db.send(:blank_object?, v) && v != false}
@@ -220,6 +243,7 @@ module Sequel
220
243
  where = opts[:where]
221
244
  atts.each do |a|
222
245
  arr = Array(a)
246
+ next if arr.any?{|x| errors.on(x)}
223
247
  next if opts[:only_if_modified] && !new? && !arr.any?{|x| changed_columns.include?(x)}
224
248
  ds = if where
225
249
  where.call(model.dataset, self, arr)
@@ -151,11 +151,6 @@ module Sequel
151
151
  new.from_xml_node(parent, opts)
152
152
  end
153
153
 
154
- # Call the dataset +to_xml+ method.
155
- def to_xml(opts={})
156
- dataset.to_xml(opts)
157
- end
158
-
159
154
  # Return an appropriate Nokogiri::XML::Builder instance
160
155
  # used to create the XML. This should probably not be used
161
156
  # directly by user code.
@@ -201,6 +196,8 @@ module Sequel
201
196
  end
202
197
  proc{|s| "#{pr[s]}_"}
203
198
  end
199
+
200
+ Plugins.def_dataset_methods(self, :to_xml)
204
201
  end
205
202
 
206
203
  module InstanceMethods
@@ -282,7 +279,7 @@ module Sequel
282
279
  parent.children.each do |node|
283
280
  next if node.is_a?(Nokogiri::XML::Text)
284
281
  k = name_proc[node.name]
285
- if assocs_hash && (assoc = assocs_hash[k])
282
+ if assocs_hash && assocs_hash[k]
286
283
  assocs_present << [k.to_sym, node]
287
284
  else
288
285
  hash[k] = node.key?('nil') ? nil : node.children.first.to_s
@@ -1606,7 +1606,7 @@ module Sequel
1606
1606
  if args.empty?
1607
1607
  Function.new(m)
1608
1608
  else
1609
- case arg = args.shift
1609
+ case args.shift
1610
1610
  when :*
1611
1611
  Function.new(m, WILDCARD)
1612
1612
  when :distinct
@@ -3,7 +3,7 @@ module Sequel
3
3
  MAJOR = 3
4
4
  # The minor version of Sequel. Bumped for every non-patch level
5
5
  # release, generally around once a month.
6
- MINOR = 46
6
+ MINOR = 47
7
7
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
8
8
  # releases that fix regressions from previous versions.
9
9
  TINY = 0
@@ -116,6 +116,25 @@ describe "A PostgreSQL database" do
116
116
  end
117
117
  end
118
118
 
119
+ describe "A PostgreSQL database with domain types" do
120
+ before(:all) do
121
+ @db = POSTGRES_DB
122
+ @db << "DROP DOMAIN IF EXISTS positive_number CASCADE"
123
+ @db << "CREATE DOMAIN positive_number AS numeric(10,2) CHECK (VALUE > 0)"
124
+ @db.create_table!(:testfk){positive_number :id, :primary_key=>true}
125
+ end
126
+ after(:all) do
127
+ @db.drop_table?(:testfk)
128
+ @db << "DROP DOMAIN positive_number"
129
+ end
130
+
131
+ specify "should correctly parse the schema" do
132
+ sch = @db.schema(:testfk, :reload=>true)
133
+ sch.first.last.delete(:domain_oid).should be_a_kind_of(Integer)
134
+ sch.should == [[:id, {:type=>:decimal, :ruby_default=>nil, :db_type=>"numeric(10,2)", :default=>nil, :oid=>1700, :primary_key=>true, :allow_null=>false, :db_domain_type=>'positive_number'}]]
135
+ end
136
+ end
137
+
119
138
  describe "A PostgreSQL dataset" do
120
139
  before(:all) do
121
140
  @db = POSTGRES_DB
@@ -482,6 +501,23 @@ describe "A PostgreSQL dataset with a timestamp field" do
482
501
  @db[:test3].get(:time).should == 'infinity'
483
502
  @db.convert_infinite_timestamps = :float
484
503
  @db[:test3].get(:time).should == 1.0/0.0
504
+ @db.convert_infinite_timestamps = 'nil'
505
+ @db[:test3].get(:time).should == nil
506
+ @db.convert_infinite_timestamps = 'string'
507
+ @db[:test3].get(:time).should == 'infinity'
508
+ @db.convert_infinite_timestamps = 'float'
509
+ @db[:test3].get(:time).should == 1.0/0.0
510
+ @db.convert_infinite_timestamps = 't'
511
+ @db[:test3].get(:time).should == 1.0/0.0
512
+ if ((Time.parse('infinity'); nil) rescue true)
513
+ # Skip for loose time parsing (e.g. old rbx)
514
+ @db.convert_infinite_timestamps = 'f'
515
+ proc{@db[:test3].get(:time)}.should raise_error
516
+ @db.convert_infinite_timestamps = nil
517
+ proc{@db[:test3].get(:time)}.should raise_error
518
+ @db.convert_infinite_timestamps = false
519
+ proc{@db[:test3].get(:time)}.should raise_error
520
+ end
485
521
 
486
522
  @d.update(:time=>Sequel.cast('-infinity', DateTime))
487
523
  @db.convert_infinite_timestamps = :nil
@@ -1217,7 +1253,7 @@ describe "Postgres::Database functions, languages, schemas, and triggers" do
1217
1253
  args = ['tf', 'SELECT 1', {:returns=>:integer}]
1218
1254
  @d.send(:create_function_sql, *args).should =~ /\A\s*CREATE FUNCTION tf\(\)\s+RETURNS integer\s+LANGUAGE SQL\s+AS 'SELECT 1'\s*\z/
1219
1255
  @d.create_function(*args)
1220
- rows = @d['SELECT tf()'].all.should == [{:tf=>1}]
1256
+ @d['SELECT tf()'].all.should == [{:tf=>1}]
1221
1257
  @d.send(:drop_function_sql, 'tf').should == 'DROP FUNCTION tf()'
1222
1258
  @d.drop_function('tf')
1223
1259
  proc{@d['SELECT tf()'].all}.should raise_error(Sequel::DatabaseError)
@@ -1229,7 +1265,7 @@ describe "Postgres::Database functions, languages, schemas, and triggers" do
1229
1265
  @d.create_function(*args)
1230
1266
  # Make sure replace works
1231
1267
  @d.create_function(*args)
1232
- rows = @d['SELECT tf(1, 2)'].all.should == [{:tf=>3}]
1268
+ @d['SELECT tf(1, 2)'].all.should == [{:tf=>3}]
1233
1269
  args = ['tf', {:if_exists=>true, :cascade=>true, :args=>[[:integer, :a], :integer]}]
1234
1270
  @d.send(:drop_function_sql,*args).should == 'DROP FUNCTION IF EXISTS tf(a integer, integer) CASCADE'
1235
1271
  @d.drop_function(*args)
@@ -1323,6 +1359,7 @@ if POSTGRES_DB.adapter_scheme == :postgres
1323
1359
  before do
1324
1360
  @db = POSTGRES_DB
1325
1361
  Sequel::Postgres::PG_NAMED_TYPES[:interval] = lambda{|v| v.reverse}
1362
+ @db.extension :pg_array
1326
1363
  @db.reset_conversion_procs
1327
1364
  end
1328
1365
  after do
@@ -1336,6 +1373,12 @@ if POSTGRES_DB.adapter_scheme == :postgres
1336
1373
  @db[:foo].insert(Sequel.cast('21 days', :interval))
1337
1374
  @db[:foo].get(:bar).should == 'syad 12'
1338
1375
  end
1376
+
1377
+ specify "should handle array types of named types" do
1378
+ @db.create_table!(:foo){column :bar, 'interval[]'}
1379
+ @db[:foo].insert(Sequel.pg_array(['21 days'], :interval))
1380
+ @db[:foo].get(:bar).should == ['syad 12']
1381
+ end
1339
1382
  end
1340
1383
  end
1341
1384
 
@@ -1792,6 +1835,42 @@ describe 'PostgreSQL array handling' do
1792
1835
  end
1793
1836
  end
1794
1837
 
1838
+ specify 'insert and retrieve custom array types' do
1839
+ int2vector = Class.new do
1840
+ attr_reader :array
1841
+ def initialize(array)
1842
+ @array = array
1843
+ end
1844
+ def sql_literal_append(ds, sql)
1845
+ sql << "'#{array.join(' ')}'"
1846
+ end
1847
+ def ==(other)
1848
+ if other.is_a?(self.class)
1849
+ array == other.array
1850
+ else
1851
+ super
1852
+ end
1853
+ end
1854
+ end
1855
+ @db.register_array_type(:int2vector){|s| int2vector.new(s.split.map{|i| i.to_i})}
1856
+ @db.create_table!(:items) do
1857
+ column :b, 'int2vector[]'
1858
+ end
1859
+ @tp.call.should == [:int2vector_array]
1860
+ int2v = int2vector.new([1, 2])
1861
+ @ds.insert(Sequel.pg_array([int2v], :int2vector))
1862
+ @ds.count.should == 1
1863
+ rs = @ds.all
1864
+ if @native
1865
+ rs.should == [{:b=>[int2v]}]
1866
+ rs.first.values.each{|v| v.should_not be_a_kind_of(Array)}
1867
+ rs.first.values.each{|v| v.to_a.should be_a_kind_of(Array)}
1868
+ @ds.delete
1869
+ @ds.insert(rs.first)
1870
+ @ds.all.should == rs
1871
+ end
1872
+ end unless POSTGRES_DB.adapter_scheme == :jdbc
1873
+
1795
1874
  specify 'use arrays in bound variables' do
1796
1875
  @db.create_table!(:items) do
1797
1876
  column :i, 'int4[]'
@@ -1929,7 +2008,7 @@ end
1929
2008
  describe 'PostgreSQL hstore handling' do
1930
2009
  before(:all) do
1931
2010
  @db = POSTGRES_DB
1932
- @db.extension :pg_hstore
2011
+ @db.extension :pg_array, :pg_hstore
1933
2012
  @ds = @db[:items]
1934
2013
  @h = {'a'=>'b', 'c'=>nil, 'd'=>'NULL', 'e'=>'\\\\" \\\' ,=>'}
1935
2014
  @native = POSTGRES_DB.adapter_scheme == :postgres
@@ -1956,6 +2035,24 @@ describe 'PostgreSQL hstore handling' do
1956
2035
  end
1957
2036
  end
1958
2037
 
2038
+ specify 'insert and retrieve hstore[] values' do
2039
+ @db.create_table!(:items) do
2040
+ column :h, 'hstore[]'
2041
+ end
2042
+ @ds.insert(Sequel.pg_array([Sequel.hstore(@h)], :hstore))
2043
+ @ds.count.should == 1
2044
+ if @native
2045
+ rs = @ds.all
2046
+ v = rs.first[:h].first
2047
+ v.should_not be_a_kind_of(Hash)
2048
+ v.to_hash.should be_a_kind_of(Hash)
2049
+ v.to_hash.should == @h
2050
+ @ds.delete
2051
+ @ds.insert(rs.first)
2052
+ @ds.all.should == rs
2053
+ end
2054
+ end
2055
+
1959
2056
  specify 'use hstore in bound variables' do
1960
2057
  @db.create_table!(:items) do
1961
2058
  column :i, :hstore
@@ -2136,6 +2233,7 @@ describe 'PostgreSQL hstore handling' do
2136
2233
  @ds.from(:items___i).select(Sequel.hstore('t'=>'s').op.record_set(:i).as(:r)).from_self(:alias=>:s).select(Sequel.lit('(r).*')).from_self.select_map(:t).should == ['s']
2137
2234
 
2138
2235
  @ds.from(Sequel.hstore('t'=>'s', 'a'=>'b').op.skeys.as(:s)).select_order_map(:s).should == %w'a t'
2236
+ @ds.from((Sequel.hstore('t'=>'s', 'a'=>'b').op - 'a').skeys.as(:s)).select_order_map(:s).should == %w't'
2139
2237
 
2140
2238
  @ds.get(h1.slice(Sequel.pg_array(%w'a c')).keys.pg_array.length).should == 2
2141
2239
  @ds.get(h1.slice(Sequel.pg_array(%w'd c')).keys.pg_array.length).should == 1
@@ -2300,7 +2398,7 @@ describe 'PostgreSQL inet/cidr types' do
2300
2398
  @ds.count.should == 1
2301
2399
  if @native
2302
2400
  rs = @ds.all
2303
- v = rs.first[:j]
2401
+ rs.first[:j]
2304
2402
  rs.first[:i].should == @ipv6
2305
2403
  rs.first[:c].should == @ipv6nm
2306
2404
  rs.first[:i].should be_a_kind_of(IPAddr)
@@ -2472,7 +2570,7 @@ describe 'PostgreSQL range types' do
2472
2570
  c.plugin :pg_typecast_on_load, :i4, :i8, :n, :d, :t, :tz unless @native
2473
2571
  v = c.create(@ra).values
2474
2572
  v.delete(:id)
2475
- v.each{|k,v| v.should == @ra[k].to_a}
2573
+ v.each{|k,v1| v1.should == @ra[k].to_a}
2476
2574
  end
2477
2575
  end
2478
2576
 
@@ -2500,15 +2598,19 @@ describe 'PostgreSQL range types' do
2500
2598
  @db.get(Sequel.pg_range(1..5, :int4range).op.right_of(-1..0)).should be_true
2501
2599
  @db.get(Sequel.pg_range(1..5, :int4range).op.right_of(-1..3)).should be_false
2502
2600
 
2503
- @db.get(Sequel.pg_range(1..5, :int4range).op.starts_before(6..10)).should be_true
2504
- @db.get(Sequel.pg_range(1..5, :int4range).op.starts_before(5..10)).should be_true
2505
- @db.get(Sequel.pg_range(1..5, :int4range).op.starts_before(-1..0)).should be_false
2506
- @db.get(Sequel.pg_range(1..5, :int4range).op.starts_before(-1..3)).should be_false
2601
+ @db.get(Sequel.pg_range(1..5, :int4range).op.ends_before(6..10)).should be_true
2602
+ @db.get(Sequel.pg_range(1..5, :int4range).op.ends_before(5..10)).should be_true
2603
+ @db.get(Sequel.pg_range(1..5, :int4range).op.ends_before(-1..0)).should be_false
2604
+ @db.get(Sequel.pg_range(1..5, :int4range).op.ends_before(-1..3)).should be_false
2605
+ @db.get(Sequel.pg_range(1..5, :int4range).op.ends_before(-1..7)).should be_true
2507
2606
 
2508
- @db.get(Sequel.pg_range(1..5, :int4range).op.ends_after(6..10)).should be_false
2509
- @db.get(Sequel.pg_range(1..5, :int4range).op.ends_after(5..10)).should be_false
2510
- @db.get(Sequel.pg_range(1..5, :int4range).op.ends_after(-1..0)).should be_true
2511
- @db.get(Sequel.pg_range(1..5, :int4range).op.ends_after(-1..3)).should be_true
2607
+ @db.get(Sequel.pg_range(1..5, :int4range).op.starts_after(6..10)).should be_false
2608
+ @db.get(Sequel.pg_range(1..5, :int4range).op.starts_after(5..10)).should be_false
2609
+ @db.get(Sequel.pg_range(1..5, :int4range).op.starts_after(3..10)).should be_false
2610
+ @db.get(Sequel.pg_range(1..5, :int4range).op.starts_after(-1..10)).should be_true
2611
+ @db.get(Sequel.pg_range(1..5, :int4range).op.starts_after(-1..0)).should be_true
2612
+ @db.get(Sequel.pg_range(1..5, :int4range).op.starts_after(-1..3)).should be_true
2613
+ @db.get(Sequel.pg_range(1..5, :int4range).op.starts_after(-5..-1)).should be_true
2512
2614
 
2513
2615
  @db.get(Sequel.pg_range(1..5, :int4range).op.adjacent_to(6..10)).should be_true
2514
2616
  @db.get(Sequel.pg_range(1...5, :int4range).op.adjacent_to(6..10)).should be_false
@@ -2637,7 +2739,7 @@ describe 'PostgreSQL interval types' do
2637
2739
  v = c.create(:i=>'1 year 2 mons 25 days 05:06:07').i
2638
2740
  v.is_a?(ActiveSupport::Duration).should be_true
2639
2741
  v.should == ActiveSupport::Duration.new(31557600 + 2*86400*30 + 3*86400*7 + 4*86400 + 5*3600 + 6*60 + 7, [[:years, 1], [:months, 2], [:days, 25], [:seconds, 18367]])
2640
- v.parts.sort_by{|k,v| k.to_s}.should == [[:years, 1], [:months, 2], [:days, 25], [:seconds, 18367]].sort_by{|k,v| k.to_s}
2742
+ v.parts.sort_by{|k,_| k.to_s}.should == [[:years, 1], [:months, 2], [:days, 25], [:seconds, 18367]].sort_by{|k,_| k.to_s}
2641
2743
  end
2642
2744
  end if (begin require 'active_support/duration'; require 'active_support/inflector'; require 'active_support/core_ext/string/inflections'; true; rescue LoadError; false end)
2643
2745
 
@@ -2697,6 +2799,21 @@ describe 'PostgreSQL row-valued/composite types' do
2697
2799
  end
2698
2800
  end
2699
2801
 
2802
+ specify 'insert and retrieve row types containing domains' do
2803
+ begin
2804
+ @db << "DROP DOMAIN IF EXISTS positive_integer CASCADE"
2805
+ @db << "CREATE DOMAIN positive_integer AS integer CHECK (VALUE > 0)"
2806
+ @db.create_table!(:domain_check) do
2807
+ positive_integer :id
2808
+ end
2809
+ @db.register_row_type(:domain_check)
2810
+ @db.get(@db.row_type(:domain_check, [1])).should == {:id=>1}
2811
+ ensure
2812
+ @db.drop_table(:domain_check)
2813
+ @db << "DROP DOMAIN positive_integer"
2814
+ end
2815
+ end if POSTGRES_DB.adapter_scheme == :postgres
2816
+
2700
2817
  specify 'insert and retrieve arrays of row types' do
2701
2818
  @ds = @db[:company]
2702
2819
  @ds.insert(:id=>1, :employees=>Sequel.pg_array([@db.row_type(:person, [1, Sequel.pg_row(['123 Sesame St', 'Somewhere', '12345'])])]))
@@ -2734,7 +2851,6 @@ describe 'PostgreSQL row-valued/composite types' do
2734
2851
  @ds.filter(:employees=>Sequel.cast(:$employees, 'person[]')).call(:first, :employees=>Sequel.pg_array([@db.row_type(:person, [1, Sequel.pg_row(['123 Sesame St', 'Somewhere', '12345'])])]))[:id].should == 1
2735
2852
  @ds.filter(:employees=>Sequel.cast(:$employees, 'person[]')).call(:first, :employees=>Sequel.pg_array([@db.row_type(:person, [1, Sequel.pg_row(['123 Sesame St', 'Somewhere', '12356'])])])).should == nil
2736
2853
 
2737
-
2738
2854
  @ds.delete
2739
2855
  @ds.call(:insert, {:employees=>Sequel.pg_array([@db.row_type(:person, [1, Sequel.pg_row([nil, nil, nil])])])}, {:employees=>:$employees, :id=>1})
2740
2856
  @ds.get(:employees).should == [{:address=>{:city=>nil, :zip=>nil, :street=>nil}, :id=>1}]