sequel 3.38.0 → 3.39.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 (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