sequel 3.46.0 → 3.47.0

Sign up to get free protection for your applications and to get access to all the features.
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}]