sequel 3.38.0 → 3.39.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. data/CHANGELOG +62 -0
  2. data/README.rdoc +2 -2
  3. data/bin/sequel +12 -2
  4. data/doc/advanced_associations.rdoc +1 -1
  5. data/doc/association_basics.rdoc +13 -0
  6. data/doc/release_notes/3.39.0.txt +237 -0
  7. data/doc/schema_modification.rdoc +4 -4
  8. data/lib/sequel/adapters/jdbc/derby.rb +1 -0
  9. data/lib/sequel/adapters/mock.rb +5 -0
  10. data/lib/sequel/adapters/mysql.rb +8 -1
  11. data/lib/sequel/adapters/mysql2.rb +10 -3
  12. data/lib/sequel/adapters/postgres.rb +72 -8
  13. data/lib/sequel/adapters/shared/db2.rb +1 -0
  14. data/lib/sequel/adapters/shared/mssql.rb +57 -0
  15. data/lib/sequel/adapters/shared/mysql.rb +95 -19
  16. data/lib/sequel/adapters/shared/oracle.rb +14 -0
  17. data/lib/sequel/adapters/shared/postgres.rb +63 -24
  18. data/lib/sequel/adapters/shared/sqlite.rb +6 -9
  19. data/lib/sequel/connection_pool/sharded_threaded.rb +8 -3
  20. data/lib/sequel/connection_pool/threaded.rb +9 -4
  21. data/lib/sequel/database/query.rb +60 -48
  22. data/lib/sequel/database/schema_generator.rb +13 -6
  23. data/lib/sequel/database/schema_methods.rb +65 -12
  24. data/lib/sequel/dataset/actions.rb +22 -4
  25. data/lib/sequel/dataset/features.rb +5 -0
  26. data/lib/sequel/dataset/graph.rb +2 -3
  27. data/lib/sequel/dataset/misc.rb +2 -2
  28. data/lib/sequel/dataset/query.rb +0 -2
  29. data/lib/sequel/dataset/sql.rb +33 -12
  30. data/lib/sequel/extensions/constraint_validations.rb +451 -0
  31. data/lib/sequel/extensions/eval_inspect.rb +17 -2
  32. data/lib/sequel/extensions/pg_array_ops.rb +15 -5
  33. data/lib/sequel/extensions/pg_interval.rb +2 -2
  34. data/lib/sequel/extensions/pg_row_ops.rb +18 -0
  35. data/lib/sequel/extensions/schema_dumper.rb +3 -11
  36. data/lib/sequel/model/associations.rb +3 -2
  37. data/lib/sequel/model/base.rb +57 -13
  38. data/lib/sequel/model/exceptions.rb +20 -2
  39. data/lib/sequel/plugins/constraint_validations.rb +198 -0
  40. data/lib/sequel/plugins/defaults_setter.rb +15 -1
  41. data/lib/sequel/plugins/dirty.rb +2 -2
  42. data/lib/sequel/plugins/identity_map.rb +12 -8
  43. data/lib/sequel/plugins/subclasses.rb +19 -1
  44. data/lib/sequel/plugins/tree.rb +3 -3
  45. data/lib/sequel/plugins/validation_helpers.rb +24 -4
  46. data/lib/sequel/sql.rb +64 -24
  47. data/lib/sequel/timezones.rb +10 -2
  48. data/lib/sequel/version.rb +1 -1
  49. data/spec/adapters/mssql_spec.rb +26 -25
  50. data/spec/adapters/mysql_spec.rb +57 -23
  51. data/spec/adapters/oracle_spec.rb +34 -49
  52. data/spec/adapters/postgres_spec.rb +226 -128
  53. data/spec/adapters/sqlite_spec.rb +50 -49
  54. data/spec/core/connection_pool_spec.rb +22 -0
  55. data/spec/core/database_spec.rb +53 -47
  56. data/spec/core/dataset_spec.rb +36 -32
  57. data/spec/core/expression_filters_spec.rb +14 -2
  58. data/spec/core/mock_adapter_spec.rb +4 -0
  59. data/spec/core/object_graph_spec.rb +0 -13
  60. data/spec/core/schema_spec.rb +64 -5
  61. data/spec/core_extensions_spec.rb +1 -0
  62. data/spec/extensions/constraint_validations_plugin_spec.rb +196 -0
  63. data/spec/extensions/constraint_validations_spec.rb +316 -0
  64. data/spec/extensions/defaults_setter_spec.rb +24 -0
  65. data/spec/extensions/eval_inspect_spec.rb +9 -0
  66. data/spec/extensions/identity_map_spec.rb +11 -2
  67. data/spec/extensions/pg_array_ops_spec.rb +9 -0
  68. data/spec/extensions/pg_row_ops_spec.rb +11 -1
  69. data/spec/extensions/pg_row_plugin_spec.rb +4 -0
  70. data/spec/extensions/schema_dumper_spec.rb +8 -5
  71. data/spec/extensions/subclasses_spec.rb +14 -0
  72. data/spec/extensions/validation_helpers_spec.rb +15 -2
  73. data/spec/integration/dataset_test.rb +75 -1
  74. data/spec/integration/plugin_test.rb +146 -0
  75. data/spec/integration/schema_test.rb +34 -0
  76. data/spec/model/dataset_methods_spec.rb +38 -0
  77. data/spec/model/hooks_spec.rb +6 -0
  78. data/spec/model/validations_spec.rb +27 -2
  79. metadata +8 -2
@@ -28,13 +28,14 @@ module Sequel
28
28
  when Hash
29
29
  "{#{obj.map{|k, v| "#{eval_inspect(k)} => #{eval_inspect(v)}"}.join(', ')}}"
30
30
  when Time
31
+ datepart = "%Y-%m-%dT" unless obj.is_a?(Sequel::SQLTime)
31
32
  if RUBY_VERSION < '1.9'
32
33
  # Time on 1.8 doesn't handle %N (or %z on Windows), manually set the usec value in the string
33
34
  hours, mins = obj.utc_offset.divmod(3600)
34
35
  mins /= 60
35
- "#{obj.class}.parse(#{obj.strftime("%Y-%m-%dT%H:%M:%S.#{sprintf('%06i%+03i%02i', obj.usec, hours, mins)}").inspect})#{'.utc' if obj.utc?}"
36
+ "#{obj.class}.parse(#{obj.strftime("#{datepart}%H:%M:%S.#{sprintf('%06i%+03i%02i', obj.usec, hours, mins)}").inspect})#{'.utc' if obj.utc?}"
36
37
  else
37
- "#{obj.class}.parse(#{obj.strftime('%FT%T.%N%z').inspect})#{'.utc' if obj.utc?}"
38
+ "#{obj.class}.parse(#{obj.strftime("#{datepart}%T.%N%z").inspect})#{'.utc' if obj.utc?}"
38
39
  end
39
40
  when DateTime
40
41
  # Ignore date of calendar reform
@@ -91,6 +92,20 @@ module Sequel
91
92
  end
92
93
  end
93
94
 
95
+ class Constant
96
+ # Constants to lookup in the Sequel module.
97
+ INSPECT_LOOKUPS = [:CURRENT_DATE, :CURRENT_TIMESTAMP, :CURRENT_TIME, :SQLTRUE, :SQLFALSE, :NULL, :NOTNULL]
98
+
99
+ # Reference the constant in the Sequel module if there is
100
+ # one that matches.
101
+ def inspect
102
+ INSPECT_LOOKUPS.each do |c|
103
+ return "Sequel::#{c}" if Sequel.const_get(c) == self
104
+ end
105
+ super
106
+ end
107
+ end
108
+
94
109
  class CaseExpression
95
110
  private
96
111
 
@@ -106,14 +106,14 @@ module Sequel
106
106
  #
107
107
  # array_op.contains(:a) # (array @> a)
108
108
  def contains(other)
109
- bool_op(CONTAINS, other)
109
+ bool_op(CONTAINS, wrap_array(other))
110
110
  end
111
111
 
112
112
  # Use the contained by (<@) operator:
113
113
  #
114
114
  # array_op.contained_by(:a) # (array <@ a)
115
115
  def contained_by(other)
116
- bool_op(CONTAINED_BY, other)
116
+ bool_op(CONTAINED_BY, wrap_array(other))
117
117
  end
118
118
 
119
119
  # Call the array_dims method:
@@ -143,7 +143,7 @@ module Sequel
143
143
  #
144
144
  # array_op.overlaps(:a) # (array && a)
145
145
  def overlaps(other)
146
- bool_op(OVERLAPS, other)
146
+ bool_op(OVERLAPS, wrap_array(other))
147
147
  end
148
148
 
149
149
  # Use the concatentation (||) operator:
@@ -151,7 +151,7 @@ module Sequel
151
151
  # array_op.push(:a) # (array || a)
152
152
  # array_op.concat(:a) # (array || a)
153
153
  def push(other)
154
- array_op(CONCAT, [self, other])
154
+ array_op(CONCAT, [self, wrap_array(other)])
155
155
  end
156
156
  alias concat push
157
157
 
@@ -182,7 +182,7 @@ module Sequel
182
182
  #
183
183
  # array_op.unshift(:a) # (a || array)
184
184
  def unshift(other)
185
- array_op(CONCAT, [other, self])
185
+ array_op(CONCAT, [wrap_array(other), self])
186
186
  end
187
187
 
188
188
  private
@@ -204,6 +204,16 @@ module Sequel
204
204
  def function(name, *args)
205
205
  SQL::Function.new(name, self, *args)
206
206
  end
207
+
208
+ # Automatically wrap argument in a PGArray if it is a plain Array.
209
+ # Requires that the pg_array extension has been loaded to work.
210
+ def wrap_array(arg)
211
+ if arg.instance_of?(Array)
212
+ Sequel.pg_array(arg)
213
+ else
214
+ arg
215
+ end
216
+ end
207
217
  end
208
218
 
209
219
  module ArrayOpMethods
@@ -5,7 +5,7 @@
5
5
  # as instances of ActiveSupport::Duration.
6
6
  #
7
7
  # In addition to the parser, this extension adds literalizers for
8
- # ActiveSupport::Duration ithat use the standard Sequel literalization
8
+ # ActiveSupport::Duration that use the standard Sequel literalization
9
9
  # callbacks, so they work on all adapters.
10
10
  #
11
11
  # If you would like to use interval columns in your model objects, you
@@ -122,7 +122,7 @@ module Sequel
122
122
  db.extend_datasets(IntervalDatasetMethods)
123
123
  end
124
124
 
125
- # Handle ActiveSupport::Duration values in bound variables
125
+ # Handle ActiveSupport::Duration values in bound variables.
126
126
  def bound_variable_arg(arg, conn)
127
127
  case arg
128
128
  when ActiveSupport::Duration
@@ -112,6 +112,21 @@ module Sequel
112
112
  end
113
113
  end
114
114
 
115
+ # Use the (identifier).* syntax to reference the members
116
+ # of the composite type as separate columns. Generally
117
+ # used when you want to expand the columns of a composite
118
+ # type to be separate columns in the result set.
119
+ #
120
+ # Sequel.pg_row_op(:a).* # (a).*
121
+ # Sequel.pg_row_op(:a)[:b].* # ((a).b).*
122
+ def *(ce=(arg=false;nil))
123
+ if arg == false
124
+ Sequel::SQL::ColumnAll.new([self])
125
+ else
126
+ super(ce)
127
+ end
128
+ end
129
+
115
130
  # Use the (identifier.*) syntax to indicate that this
116
131
  # expression represents the composite type of one
117
132
  # of the tables being referenced, if it has the same
@@ -120,6 +135,9 @@ module Sequel
120
135
  # (which should be a symbol representing the composite type).
121
136
  # This is used if you want to return whole table row as a
122
137
  # composite type.
138
+ #
139
+ # Sequel.pg_row_op(:a).splat[:b] # (a.*).b
140
+ # Sequel.pg_row_op(:a).splat(:a) # (a.*)::a
123
141
  def splat(cast_to=nil)
124
142
  if args.length > 1
125
143
  raise Error, 'cannot splat a PGRowOp with multiple arguments'
@@ -8,6 +8,8 @@
8
8
  #
9
9
  # Sequel.extension :schema_dumper
10
10
 
11
+ Sequel.extension :eval_inspect
12
+
11
13
  module Sequel
12
14
  class Database
13
15
  # Dump foreign key constraints for all tables as a migration. This complements
@@ -137,7 +139,6 @@ END_MIG
137
139
  else
138
140
  gen.column(name, type, col_opts)
139
141
  if [Integer, Bignum, Float].include?(type) && schema[:db_type] =~ / unsigned\z/io
140
- Sequel.extension :eval_inspect
141
142
  gen.check(Sequel::SQL::Identifier.new(name) >= 0)
142
143
  end
143
144
  end
@@ -473,16 +474,7 @@ END_MIG
473
474
  def opts_inspect(opts)
474
475
  if opts[:default]
475
476
  opts = opts.dup
476
- de = case d = opts.delete(:default)
477
- when BigDecimal, Sequel::SQL::Blob
478
- "#{d.class.name}.new(#{d.to_s.inspect})"
479
- when DateTime, Date
480
- "#{d.class.name}.parse(#{d.to_s.inspect})"
481
- when Time
482
- "#{d.class.name}.parse(#{d.strftime('%H:%M:%S').inspect})"
483
- else
484
- d.inspect
485
- end
477
+ de = Sequel.eval_inspect(opts.delete(:default))
486
478
  ", :default=>#{de}#{", #{opts.inspect[1...-1]}" if opts.length > 0}"
487
479
  else
488
480
  ", #{opts.inspect[1...-1]}" if opts.length > 0
@@ -745,9 +745,10 @@ module Sequel
745
745
  # than one row for each row in the current table (default: 0 for
746
746
  # many_to_one and one_to_one associations, 1 for one_to_many and
747
747
  # many_to_many associations).
748
- # :class :: The associated class or its name. If not
748
+ # :class :: The associated class or its name as a string or symbol. If not
749
749
  # given, uses the association's name, which is camelized (and
750
- # singularized unless the type is :many_to_one or :one_to_one)
750
+ # singularized unless the type is :many_to_one or :one_to_one). If this is specified
751
+ # as a string or symbol, you must specify the full class name (e.g. "SomeModule::MyModel").
751
752
  # :clone :: Merge the current options and block into the options and block used in defining
752
753
  # the given association. Can be used to DRY up a bunch of similar associations that
753
754
  # all share the same options such as :class and :key, while changing the order and block used.
@@ -557,7 +557,7 @@ module Sequel
557
557
  # approach such as set_allowed_columns or the instance level set_only or set_fields methods
558
558
  # is usually a better choice. So use of this method is generally a bad idea.
559
559
  #
560
- # Artist.set_restricted_column(:records_sold)
560
+ # Artist.set_restricted_columns(:records_sold)
561
561
  # Artist.set(:name=>'Bob', :hometown=>'Sactown') # No Error
562
562
  # Artist.set(:name=>'Bob', :records_sold=>30000) # Error
563
563
  def set_restricted_columns(*cols)
@@ -1226,7 +1226,7 @@ module Sequel
1226
1226
  set_server(opts[:server]) if opts[:server]
1227
1227
  if opts[:validate] != false
1228
1228
  unless checked_save_failure(opts){_valid?(true, opts)}
1229
- raise(ValidationFailed.new(errors)) if raise_on_failure?(opts)
1229
+ raise(ValidationFailed.new(self)) if raise_on_failure?(opts)
1230
1230
  return
1231
1231
  end
1232
1232
  end
@@ -1477,12 +1477,12 @@ module Sequel
1477
1477
  called = false
1478
1478
  around_destroy do
1479
1479
  called = true
1480
- raise_hook_failure(:destroy) if before_destroy == false
1480
+ raise_hook_failure(:before_destroy) if before_destroy == false
1481
1481
  _destroy_delete
1482
1482
  after_destroy
1483
1483
  true
1484
1484
  end
1485
- raise_hook_failure(:destroy) unless called
1485
+ raise_hook_failure(:around_destroy) unless called
1486
1486
  db.after_commit(sh){after_destroy_commit} if uacr
1487
1487
  self
1488
1488
  end
@@ -1551,12 +1551,12 @@ module Sequel
1551
1551
  called_cu = false
1552
1552
  around_save do
1553
1553
  called_save = true
1554
- raise_hook_failure(:save) if before_save == false
1554
+ raise_hook_failure(:before_save) if before_save == false
1555
1555
  if new?
1556
1556
  was_new = true
1557
1557
  around_create do
1558
1558
  called_cu = true
1559
- raise_hook_failure(:create) if before_create == false
1559
+ raise_hook_failure(:before_create) if before_create == false
1560
1560
  pk = _insert
1561
1561
  @this = nil
1562
1562
  @new = false
@@ -1564,11 +1564,11 @@ module Sequel
1564
1564
  after_create
1565
1565
  true
1566
1566
  end
1567
- raise_hook_failure(:create) unless called_cu
1567
+ raise_hook_failure(:around_create) unless called_cu
1568
1568
  else
1569
1569
  around_update do
1570
1570
  called_cu = true
1571
- raise_hook_failure(:update) if before_update == false
1571
+ raise_hook_failure(:before_update) if before_update == false
1572
1572
  if columns.empty?
1573
1573
  @columns_updated = if opts[:changed]
1574
1574
  @values.reject{|k,v| !changed_columns.include?(k)}
@@ -1585,12 +1585,12 @@ module Sequel
1585
1585
  after_update
1586
1586
  true
1587
1587
  end
1588
- raise_hook_failure(:update) unless called_cu
1588
+ raise_hook_failure(:around_update) unless called_cu
1589
1589
  end
1590
1590
  after_save
1591
1591
  true
1592
1592
  end
1593
- raise_hook_failure(:save) unless called_save
1593
+ raise_hook_failure(:around_save) unless called_save
1594
1594
  if was_new
1595
1595
  @was_new = nil
1596
1596
  pk ? _save_refresh : changed_columns.clear
@@ -1659,7 +1659,7 @@ module Sequel
1659
1659
  called = true
1660
1660
  if before_validation == false
1661
1661
  if raise_errors
1662
- raise_hook_failure(:validation)
1662
+ raise_hook_failure(:before_validation)
1663
1663
  else
1664
1664
  error = true
1665
1665
  end
@@ -1673,7 +1673,7 @@ module Sequel
1673
1673
  error = true unless called
1674
1674
  if error
1675
1675
  if raise_errors
1676
- raise_hook_failure(:validation)
1676
+ raise_hook_failure(:around_validation)
1677
1677
  else
1678
1678
  false
1679
1679
  end
@@ -1731,7 +1731,7 @@ module Sequel
1731
1731
  # Raise an error appropriate to the hook type. May be swallowed by
1732
1732
  # checked_save_failure depending on the raise_on_failure? setting.
1733
1733
  def raise_hook_failure(type)
1734
- raise HookFailed, "one of the before_#{type} hooks returned false"
1734
+ raise HookFailed.new("the #{type} hook failed", self)
1735
1735
  end
1736
1736
 
1737
1737
  # Set the columns, filtered by the only and except arrays.
@@ -1871,6 +1871,50 @@ module Sequel
1871
1871
  model.use_transactions ? @db.transaction(:server=>opts[:server], &pr) : pr.call
1872
1872
  end
1873
1873
 
1874
+ # Allow Sequel::Model classes to be used as dataset arguments when graphing:
1875
+ #
1876
+ # Artist.graph(Album, :artist_id=>id)
1877
+ # # SELECT artists.id, artists.name, albums.id AS albums_id, albums.artist_id, albums.name AS albums_name
1878
+ # # FROM artists LEFT OUTER JOIN albums ON (albums.artist_id = artists.id)
1879
+ def graph(table, *args, &block)
1880
+ if table.is_a?(Class) && table < Sequel::Model
1881
+ super(table.dataset, *args, &block)
1882
+ else
1883
+ super
1884
+ end
1885
+ end
1886
+
1887
+ # Handle Sequel::Model instances when inserting, using the model instance's
1888
+ # values for the insert, unless the model instance can be used directly in
1889
+ # SQL.
1890
+ #
1891
+ # Album.insert(Album.load(:name=>'A'))
1892
+ # # INSERT INTO albums (name) VALUES ('A')
1893
+ def insert_sql(*values)
1894
+ if values.size == 1 && (v = values.at(0)).is_a?(Sequel::Model) && !v.respond_to?(:sql_literal_append)
1895
+ super(v.to_hash)
1896
+ else
1897
+ super
1898
+ end
1899
+ end
1900
+
1901
+ # Allow Sequel::Model classes to be used as table name arguments in dataset
1902
+ # join methods:
1903
+ #
1904
+ # Artist.join(Album, :artist_id=>id)
1905
+ # # SELECT * FROM artists INNER JOIN albums ON (albums.artist_id = artists.id)
1906
+ def join_table(type, table, *args, &block)
1907
+ if table.is_a?(Class) && table < Sequel::Model
1908
+ if table.dataset.simple_select_all?
1909
+ super(type, table.table_name, *args, &block)
1910
+ else
1911
+ super(type, table.dataset, *args, &block)
1912
+ end
1913
+ else
1914
+ super
1915
+ end
1916
+ end
1917
+
1874
1918
  # This allows you to call +to_hash+ without any arguments, which will
1875
1919
  # result in a hash with the primary key value being the key and the
1876
1920
  # model object being the value.
@@ -1,7 +1,15 @@
1
1
  module Sequel
2
2
  # Exception class raised when +raise_on_save_failure+ is set and a before hook returns false
3
3
  # or an around hook doesn't call super or yield.
4
- class HookFailed < Error; end
4
+ class HookFailed < Error
5
+ # The Sequel::Model instance related to this error.
6
+ attr_reader :model
7
+
8
+ def initialize(message, model=nil)
9
+ @model = model
10
+ super(message)
11
+ end
12
+ end
5
13
 
6
14
  # Deprecated alias for HookFailed, kept for backwards compatibility
7
15
  BeforeHookFailed = HookFailed
@@ -15,7 +23,18 @@ module Sequel
15
23
 
16
24
  # Exception class raised when +raise_on_save_failure+ is set and validation fails
17
25
  class ValidationFailed < Error
26
+ # The Sequel::Model object related to this exception.
27
+ attr_reader :model
28
+
29
+ # The Sequel::Model::Errors object related to this exception.
30
+ attr_reader :errors
31
+
18
32
  def initialize(errors)
33
+ if errors.is_a?(Sequel::Model)
34
+ @model = errors
35
+ errors = @model.errors
36
+ end
37
+
19
38
  if errors.respond_to?(:full_messages)
20
39
  @errors = errors
21
40
  super(errors.full_messages.join(', '))
@@ -23,6 +42,5 @@ module Sequel
23
42
  super
24
43
  end
25
44
  end
26
- attr_reader :errors
27
45
  end
28
46
  end
@@ -0,0 +1,198 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The constraint_validations plugin is designed to be used with databases
4
+ # that used the constraint_validations extension when creating their
5
+ # tables. The extension adds validation metadata for constraints created,
6
+ # and this plugin reads that metadata and automatically creates validations
7
+ # for all of the constraints. For example, if you used the extension
8
+ # and created your albums table like this:
9
+ #
10
+ # DB.create_table(:albums) do
11
+ # primary_key :id
12
+ # String :name
13
+ # validate do
14
+ # min_length 5, :name
15
+ # end
16
+ # end
17
+ #
18
+ # Then when you went to save an album that uses this plugin:
19
+ #
20
+ # Album.create(:name=>'abc')
21
+ # # raises Sequel::ValidationFailed: name is shorter than 5 characters
22
+ #
23
+ # Usage:
24
+ #
25
+ # # Make all model subclasses use constraint validations (called before loading subclasses)
26
+ # Sequel::Model.plugin :constraint_validations
27
+ #
28
+ # # Make the Album class use constraint validations
29
+ # Album.plugin :constraint_validations
30
+ module ConstraintValidations
31
+ # The default constraint validation metadata table name.
32
+ DEFAULT_CONSTRAINT_VALIDATIONS_TABLE = :sequel_constraint_validations
33
+
34
+ # Automatically load the validation_helpers plugin to run the actual validations.
35
+ def self.apply(model, opts={})
36
+ model.plugin :validation_helpers
37
+ end
38
+
39
+ # Parse the constraint validations metadata from the database. Options:
40
+ # :constraint_validations_table :: Override the name of the constraint validations
41
+ # metadata table. Should only be used if the table
42
+ # name was overridden when creating the constraint
43
+ # validations.
44
+ def self.configure(model, opts={})
45
+ model.instance_variable_set(:@constraint_validations_table, opts[:constraint_validations_table] || DEFAULT_CONSTRAINT_VALIDATIONS_TABLE)
46
+ model.send(:parse_constraint_validations)
47
+ end
48
+
49
+ module DatabaseMethods
50
+ # A hash of validation method call metadata for all tables in the database.
51
+ # The hash is keyed by table name string and contains arrays of validation
52
+ # method call arrays.
53
+ attr_accessor :constraint_validations
54
+ end
55
+
56
+ module ClassMethods
57
+ # An array of validation method call arrays. Each array is an array that
58
+ # is splatted to send to perform a validation via validation_helpers.
59
+ attr_reader :constraint_validations
60
+
61
+ # The name of the table containing the constraint validations metadata.
62
+ attr_reader :constraint_validations_table
63
+
64
+ # Copy the name of the constraint validations metadata table into the subclass.
65
+ def inherited(subclass)
66
+ super
67
+ subclass.instance_variable_set(:@constraint_validations_table, @constraint_validations_table)
68
+ end
69
+
70
+ # Parse the constraint validations from the database whenever the dataset
71
+ # changes.
72
+ def set_dataset(*)
73
+ r = super
74
+ parse_constraint_validations
75
+ r
76
+ end
77
+
78
+ private
79
+
80
+ # If the database has not already parsed constraint validation
81
+ # metadata, then run a query to get the metadata data and transform it
82
+ # into arrays of validation method calls.
83
+ #
84
+ # If this model has associated dataset, use the model's table name
85
+ # to get the validations for just this model.
86
+ def parse_constraint_validations
87
+ db.extend(DatabaseMethods)
88
+
89
+ unless hash = Sequel.synchronize{db.constraint_validations}
90
+ hash = {}
91
+ db.from(constraint_validations_table).each do |r|
92
+ (hash[r[:table]] ||= []) << constraint_validation_array(r)
93
+ end
94
+ Sequel.synchronize{db.constraint_validations = hash}
95
+ end
96
+
97
+ if @dataset
98
+ ds = @dataset.clone
99
+ ds.quote_identifiers = false
100
+ table_name = ds.literal(model.table_name)
101
+ @constraint_validations = Sequel.synchronize{hash[table_name]} || []
102
+ end
103
+ end
104
+
105
+ # Given a specific database constraint validation metadata row hash, transform
106
+ # it in an validation method call array suitable for splatting to send.
107
+ def constraint_validation_array(r)
108
+ opts = {}
109
+ opts[:message] = r[:message] if r[:message]
110
+ opts[:allow_nil] = true if db.typecast_value(:boolean, r[:allow_nil])
111
+ type = r[:validation_type].to_sym
112
+ arg = r[:argument]
113
+ column = r[:column]
114
+
115
+ case type
116
+ when :like, :ilike
117
+ arg = constraint_validation_like_to_regexp(arg, type == :ilike)
118
+ type = :format
119
+ when :exact_length, :min_length, :max_length
120
+ arg = arg.to_i
121
+ when :length_range
122
+ arg = constraint_validation_int_range(arg)
123
+ when :format
124
+ arg = Regexp.new(arg)
125
+ when :iformat
126
+ arg = Regexp.new(arg, Regexp::IGNORECASE)
127
+ type = :format
128
+ when :includes_str_array
129
+ arg = arg.split(',')
130
+ type = :includes
131
+ when :includes_int_array
132
+ arg = arg.split(',').map{|x| x.to_i}
133
+ type = :includes
134
+ when :includes_int_range
135
+ arg = constraint_validation_int_range(arg)
136
+ type = :includes
137
+ end
138
+
139
+ column = if type == :unique
140
+ column.split(',').map{|c| c.to_sym}
141
+ else
142
+ column.to_sym
143
+ end
144
+
145
+ a = [:"validates_#{type}"]
146
+ if arg
147
+ a << arg
148
+ end
149
+ a << column
150
+ unless opts.empty?
151
+ a << opts
152
+ end
153
+ a
154
+ end
155
+
156
+ # Return a range of integers assuming the argument is in
157
+ # 1..2 or 1...2 format.
158
+ def constraint_validation_int_range(arg)
159
+ arg =~ /(\d+)\.\.(\.)?(\d+)/
160
+ Range.new($1.to_i, $3.to_i, $2 == '.')
161
+ end
162
+
163
+ # Transform the LIKE pattern string argument into a
164
+ # Regexp argument suitable for use with validates_format.
165
+ def constraint_validation_like_to_regexp(arg, case_insensitive)
166
+ arg = Regexp.escape(arg).gsub(/%%|%|_/) do |s|
167
+ case s
168
+ when '%%'
169
+ '%'
170
+ when '%'
171
+ '.*'
172
+ when '_'
173
+ '.'
174
+ end
175
+ end
176
+ arg = "\\A#{arg}\\z"
177
+
178
+ if case_insensitive
179
+ Regexp.new(arg, Regexp::IGNORECASE)
180
+ else
181
+ Regexp.new(arg)
182
+ end
183
+ end
184
+ end
185
+
186
+ module InstanceMethods
187
+ # Run all of the constraint validations parsed from the database
188
+ # when validating the instance.
189
+ def validate
190
+ super
191
+ model.constraint_validations.each do |v|
192
+ send(*v)
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end