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.
- 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
|