sequel 3.38.0 → 3.39.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +62 -0
- data/README.rdoc +2 -2
- data/bin/sequel +12 -2
- data/doc/advanced_associations.rdoc +1 -1
- data/doc/association_basics.rdoc +13 -0
- data/doc/release_notes/3.39.0.txt +237 -0
- data/doc/schema_modification.rdoc +4 -4
- data/lib/sequel/adapters/jdbc/derby.rb +1 -0
- data/lib/sequel/adapters/mock.rb +5 -0
- data/lib/sequel/adapters/mysql.rb +8 -1
- data/lib/sequel/adapters/mysql2.rb +10 -3
- data/lib/sequel/adapters/postgres.rb +72 -8
- data/lib/sequel/adapters/shared/db2.rb +1 -0
- data/lib/sequel/adapters/shared/mssql.rb +57 -0
- data/lib/sequel/adapters/shared/mysql.rb +95 -19
- data/lib/sequel/adapters/shared/oracle.rb +14 -0
- data/lib/sequel/adapters/shared/postgres.rb +63 -24
- data/lib/sequel/adapters/shared/sqlite.rb +6 -9
- data/lib/sequel/connection_pool/sharded_threaded.rb +8 -3
- data/lib/sequel/connection_pool/threaded.rb +9 -4
- data/lib/sequel/database/query.rb +60 -48
- data/lib/sequel/database/schema_generator.rb +13 -6
- data/lib/sequel/database/schema_methods.rb +65 -12
- data/lib/sequel/dataset/actions.rb +22 -4
- data/lib/sequel/dataset/features.rb +5 -0
- data/lib/sequel/dataset/graph.rb +2 -3
- data/lib/sequel/dataset/misc.rb +2 -2
- data/lib/sequel/dataset/query.rb +0 -2
- data/lib/sequel/dataset/sql.rb +33 -12
- data/lib/sequel/extensions/constraint_validations.rb +451 -0
- data/lib/sequel/extensions/eval_inspect.rb +17 -2
- data/lib/sequel/extensions/pg_array_ops.rb +15 -5
- data/lib/sequel/extensions/pg_interval.rb +2 -2
- data/lib/sequel/extensions/pg_row_ops.rb +18 -0
- data/lib/sequel/extensions/schema_dumper.rb +3 -11
- data/lib/sequel/model/associations.rb +3 -2
- data/lib/sequel/model/base.rb +57 -13
- data/lib/sequel/model/exceptions.rb +20 -2
- data/lib/sequel/plugins/constraint_validations.rb +198 -0
- data/lib/sequel/plugins/defaults_setter.rb +15 -1
- data/lib/sequel/plugins/dirty.rb +2 -2
- data/lib/sequel/plugins/identity_map.rb +12 -8
- data/lib/sequel/plugins/subclasses.rb +19 -1
- data/lib/sequel/plugins/tree.rb +3 -3
- data/lib/sequel/plugins/validation_helpers.rb +24 -4
- data/lib/sequel/sql.rb +64 -24
- data/lib/sequel/timezones.rb +10 -2
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/mssql_spec.rb +26 -25
- data/spec/adapters/mysql_spec.rb +57 -23
- data/spec/adapters/oracle_spec.rb +34 -49
- data/spec/adapters/postgres_spec.rb +226 -128
- data/spec/adapters/sqlite_spec.rb +50 -49
- data/spec/core/connection_pool_spec.rb +22 -0
- data/spec/core/database_spec.rb +53 -47
- data/spec/core/dataset_spec.rb +36 -32
- data/spec/core/expression_filters_spec.rb +14 -2
- data/spec/core/mock_adapter_spec.rb +4 -0
- data/spec/core/object_graph_spec.rb +0 -13
- data/spec/core/schema_spec.rb +64 -5
- data/spec/core_extensions_spec.rb +1 -0
- data/spec/extensions/constraint_validations_plugin_spec.rb +196 -0
- data/spec/extensions/constraint_validations_spec.rb +316 -0
- data/spec/extensions/defaults_setter_spec.rb +24 -0
- data/spec/extensions/eval_inspect_spec.rb +9 -0
- data/spec/extensions/identity_map_spec.rb +11 -2
- data/spec/extensions/pg_array_ops_spec.rb +9 -0
- data/spec/extensions/pg_row_ops_spec.rb +11 -1
- data/spec/extensions/pg_row_plugin_spec.rb +4 -0
- data/spec/extensions/schema_dumper_spec.rb +8 -5
- data/spec/extensions/subclasses_spec.rb +14 -0
- data/spec/extensions/validation_helpers_spec.rb +15 -2
- data/spec/integration/dataset_test.rb +75 -1
- data/spec/integration/plugin_test.rb +146 -0
- data/spec/integration/schema_test.rb +34 -0
- data/spec/model/dataset_methods_spec.rb +38 -0
- data/spec/model/hooks_spec.rb +6 -0
- data/spec/model/validations_spec.rb +27 -2
- 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("%
|
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(
|
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
|
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 =
|
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.
|
data/lib/sequel/model/base.rb
CHANGED
@@ -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.
|
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(
|
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(:
|
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(:
|
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(:
|
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(:
|
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(:
|
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(:
|
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(:
|
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(:
|
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(:
|
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(:
|
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
|
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
|
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
|