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
@@ -21,6 +21,11 @@ module Sequel
|
|
21
21
|
# The default options for join table columns.
|
22
22
|
DEFAULT_JOIN_TABLE_COLUMN_OPTIONS = {:null=>false}
|
23
23
|
|
24
|
+
# The alter table operations that are combinable.
|
25
|
+
COMBINABLE_ALTER_TABLE_OPS = [:add_column, :drop_column, :rename_column,
|
26
|
+
:set_column_type, :set_column_default, :set_column_null,
|
27
|
+
:add_constraint, :drop_constraint]
|
28
|
+
|
24
29
|
# Adds a column to the specified table. This method expects a column name,
|
25
30
|
# a datatype and optionally a hash with additional constraints and options:
|
26
31
|
#
|
@@ -70,7 +75,7 @@ module Sequel
|
|
70
75
|
def alter_table(name, generator=nil, &block)
|
71
76
|
generator ||= alter_table_generator(&block)
|
72
77
|
remove_cached_schema(name)
|
73
|
-
|
78
|
+
apply_alter_table_generator(name, generator)
|
74
79
|
nil
|
75
80
|
end
|
76
81
|
|
@@ -333,17 +338,21 @@ module Sequel
|
|
333
338
|
|
334
339
|
# Apply the changes in the given alter table ops to the table given by name.
|
335
340
|
def apply_alter_table(name, ops)
|
336
|
-
alter_table_sql_list(name, ops).
|
341
|
+
alter_table_sql_list(name, ops).each{|sql| execute_ddl(sql)}
|
337
342
|
end
|
338
343
|
|
344
|
+
# Apply the operations in the given generator to the table given by name.
|
345
|
+
def apply_alter_table_generator(name, generator)
|
346
|
+
apply_alter_table(name, generator.operations)
|
347
|
+
end
|
348
|
+
|
339
349
|
# The class used for alter_table generators.
|
340
350
|
def alter_table_generator_class
|
341
351
|
Schema::AlterTableGenerator
|
342
352
|
end
|
343
353
|
|
344
|
-
#
|
345
|
-
|
346
|
-
def alter_table_sql(table, op)
|
354
|
+
# SQL fragment for given alter table operation.
|
355
|
+
def alter_table_op_sql(table, op)
|
347
356
|
quoted_name = quote_identifier(op[:name]) if op[:name]
|
348
357
|
alter_table_op = case op[:op]
|
349
358
|
when :add_column
|
@@ -358,24 +367,56 @@ module Sequel
|
|
358
367
|
"ALTER COLUMN #{quoted_name} SET DEFAULT #{literal(op[:default])}"
|
359
368
|
when :set_column_null
|
360
369
|
"ALTER COLUMN #{quoted_name} #{op[:null] ? 'DROP' : 'SET'} NOT NULL"
|
361
|
-
when :add_index
|
362
|
-
return index_definition_sql(table, op)
|
363
|
-
when :drop_index
|
364
|
-
return drop_index_sql(table, op)
|
365
370
|
when :add_constraint
|
366
371
|
"ADD #{constraint_definition_sql(op)}"
|
367
372
|
when :drop_constraint
|
368
373
|
"DROP CONSTRAINT #{quoted_name}#{' CASCADE' if op[:cascade]}"
|
369
374
|
else
|
370
|
-
raise Error, "Unsupported ALTER TABLE operation"
|
375
|
+
raise Error, "Unsupported ALTER TABLE operation: #{op[:op]}"
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
# The SQL to execute to modify the DDL for the given table name. op
|
380
|
+
# should be one of the operations returned by the AlterTableGenerator.
|
381
|
+
def alter_table_sql(table, op)
|
382
|
+
case op[:op]
|
383
|
+
when :add_index
|
384
|
+
index_definition_sql(table, op)
|
385
|
+
when :drop_index
|
386
|
+
drop_index_sql(table, op)
|
387
|
+
else
|
388
|
+
"ALTER TABLE #{quote_schema_table(table)} #{alter_table_op_sql(table, op)}"
|
371
389
|
end
|
372
|
-
"ALTER TABLE #{quote_schema_table(table)} #{alter_table_op}"
|
373
390
|
end
|
374
391
|
|
375
392
|
# Array of SQL DDL modification statements for the given table,
|
376
393
|
# corresponding to the DDL changes specified by the operations.
|
377
394
|
def alter_table_sql_list(table, operations)
|
378
|
-
|
395
|
+
if supports_combining_alter_table_ops?
|
396
|
+
grouped_ops = []
|
397
|
+
last_combinable = false
|
398
|
+
operations.each do |op|
|
399
|
+
if combinable_alter_table_op?(op)
|
400
|
+
if sql = alter_table_op_sql(table, op)
|
401
|
+
grouped_ops << [] unless last_combinable
|
402
|
+
grouped_ops.last << sql
|
403
|
+
last_combinable = true
|
404
|
+
end
|
405
|
+
elsif sql = alter_table_sql(table, op)
|
406
|
+
grouped_ops << sql
|
407
|
+
last_combinable = false
|
408
|
+
end
|
409
|
+
end
|
410
|
+
grouped_ops.map do |gop|
|
411
|
+
if gop.is_a?(Array)
|
412
|
+
"ALTER TABLE #{quote_schema_table(table)} #{gop.join(', ')}"
|
413
|
+
else
|
414
|
+
gop
|
415
|
+
end
|
416
|
+
end
|
417
|
+
else
|
418
|
+
operations.map{|op| alter_table_sql(table, op)}.flatten.compact
|
419
|
+
end
|
379
420
|
end
|
380
421
|
|
381
422
|
# The SQL string specify the autoincrement property, generally used by
|
@@ -458,6 +499,12 @@ module Sequel
|
|
458
499
|
"FOREIGN KEY #{literal(constraint[:columns])}#{column_references_sql(constraint)}"
|
459
500
|
end
|
460
501
|
|
502
|
+
# Whether the given alter table operation is combinable.
|
503
|
+
def combinable_alter_table_op?(op)
|
504
|
+
# Use a dynamic lookup for easier overriding in adapters
|
505
|
+
COMBINABLE_ALTER_TABLE_OPS.include?(op[:op])
|
506
|
+
end
|
507
|
+
|
461
508
|
# SQL DDL fragment specifying a constraint on a table.
|
462
509
|
def constraint_definition_sql(constraint)
|
463
510
|
sql = constraint[:name] ? "CONSTRAINT #{quote_identifier(constraint[:name])} " : ""
|
@@ -651,6 +698,12 @@ module Sequel
|
|
651
698
|
@schema_utility_dataset ||= dataset
|
652
699
|
end
|
653
700
|
|
701
|
+
# Whether the database supports combining multiple alter table
|
702
|
+
# operations into a single query, false by default.
|
703
|
+
def supports_combining_alter_table_ops?
|
704
|
+
false
|
705
|
+
end
|
706
|
+
|
654
707
|
# SQL DDL fragment for temporary table
|
655
708
|
def temporary_table_sql
|
656
709
|
self.class.const_get(:TEMPORARY)
|
@@ -93,12 +93,30 @@ module Sequel
|
|
93
93
|
columns
|
94
94
|
end
|
95
95
|
|
96
|
-
# Returns the number of records in the dataset.
|
96
|
+
# Returns the number of records in the dataset. If an argument is provided,
|
97
|
+
# it is used as the argument to count. If a block is provided, it is
|
98
|
+
# treated as a virtual row, and the result is used as the argument to
|
99
|
+
# count.
|
97
100
|
#
|
98
101
|
# DB[:table].count # SELECT COUNT(*) AS count FROM table LIMIT 1
|
99
102
|
# # => 3
|
100
|
-
|
101
|
-
|
103
|
+
# DB[:table].count(:column) # SELECT COUNT(column) AS count FROM table LIMIT 1
|
104
|
+
# # => 2
|
105
|
+
# DB[:table].count{foo(column)} # SELECT COUNT(foo(column)) AS count FROM table LIMIT 1
|
106
|
+
# # => 1
|
107
|
+
def count(arg=(no_arg=true), &block)
|
108
|
+
if no_arg
|
109
|
+
if block
|
110
|
+
arg = Sequel.virtual_row(&block)
|
111
|
+
aggregate_dataset.get{COUNT(arg).as(count)}
|
112
|
+
else
|
113
|
+
aggregate_dataset.get{COUNT(:*){}.as(count)}.to_i
|
114
|
+
end
|
115
|
+
elsif block
|
116
|
+
raise Error, 'cannot provide both argument and block to Dataset#count'
|
117
|
+
else
|
118
|
+
aggregate_dataset.get{COUNT(arg).as(count)}
|
119
|
+
end
|
102
120
|
end
|
103
121
|
|
104
122
|
# Deletes the records in the dataset. The returned value should be
|
@@ -311,7 +329,7 @@ module Sequel
|
|
311
329
|
# # INSERT INTO table (x) VALUES (1)
|
312
330
|
# # INSERT INTO table (x) VALUES (2)
|
313
331
|
#
|
314
|
-
# DB[:table].insert_multiple([{:x=>1}, {:x=>2}]){|row| row[:y] = row[:x] * 2}
|
332
|
+
# DB[:table].insert_multiple([{:x=>1}, {:x=>2}]){|row| row[:y] = row[:x] * 2; row }
|
315
333
|
# # => [6, 7]
|
316
334
|
# # INSERT INTO table (x, y) VALUES (1, 2)
|
317
335
|
# # INSERT INTO table (x, y) VALUES (2, 4)
|
@@ -114,6 +114,11 @@ module Sequel
|
|
114
114
|
supports_distinct_on?
|
115
115
|
end
|
116
116
|
|
117
|
+
# Whether the dataset supports pattern matching by regular expressions.
|
118
|
+
def supports_regexp?
|
119
|
+
false
|
120
|
+
end
|
121
|
+
|
117
122
|
# Whether the RETURNING clause is supported for the given type of query.
|
118
123
|
# +type+ can be :insert, :update, or :delete.
|
119
124
|
def supports_returning?(type)
|
data/lib/sequel/dataset/graph.rb
CHANGED
@@ -73,9 +73,8 @@ module Sequel
|
|
73
73
|
# alias the table. You will get an error if the the alias (or table) name is
|
74
74
|
# used more than once.
|
75
75
|
def graph(dataset, join_conditions = nil, options = {}, &block)
|
76
|
-
# Allow the use of a
|
76
|
+
# Allow the use of a dataset or symbol as the first argument
|
77
77
|
# Find the table name/dataset based on the argument
|
78
|
-
dataset = dataset.dataset if dataset.respond_to?(:dataset)
|
79
78
|
table_alias = options[:table_alias]
|
80
79
|
case dataset
|
81
80
|
when Symbol
|
@@ -91,7 +90,7 @@ module Sequel
|
|
91
90
|
table_alias ||= dataset_alias((@opts[:num_dataset_sources] || 0)+1)
|
92
91
|
end
|
93
92
|
else
|
94
|
-
raise Error, "The dataset argument should be a symbol
|
93
|
+
raise Error, "The dataset argument should be a symbol or dataset"
|
95
94
|
end
|
96
95
|
|
97
96
|
# Raise Sequel::Error with explanation that the table alias has been used
|
data/lib/sequel/dataset/misc.rb
CHANGED
@@ -31,9 +31,9 @@ module Sequel
|
|
31
31
|
end
|
32
32
|
|
33
33
|
# Define a hash value such that datasets with the same DB, opts, and SQL
|
34
|
-
# will be
|
34
|
+
# will be considered equal.
|
35
35
|
def ==(o)
|
36
|
-
o.is_a?(self.class) && db == o.db
|
36
|
+
o.is_a?(self.class) && db == o.db && opts == o.opts && sql == o.sql
|
37
37
|
end
|
38
38
|
|
39
39
|
# Alias for ==
|
data/lib/sequel/dataset/query.rb
CHANGED
@@ -459,7 +459,6 @@ module Sequel
|
|
459
459
|
# * type - The type of join to do (e.g. :inner)
|
460
460
|
# * table - Depends on type:
|
461
461
|
# * Dataset - a subselect is performed with an alias of tN for some value of N
|
462
|
-
# * Model (or anything responding to :table_name) - table.table_name
|
463
462
|
# * String, Symbol: table
|
464
463
|
# * expr - specifies conditions, depends on type:
|
465
464
|
# * Hash, Array of two element arrays - Assumes key (1st arg) is column of joined table (unless already
|
@@ -536,7 +535,6 @@ module Sequel
|
|
536
535
|
end
|
537
536
|
table_name = table_alias
|
538
537
|
else
|
539
|
-
table = table.table_name if table.respond_to?(:table_name)
|
540
538
|
table, implicit_table_alias = split_alias(table)
|
541
539
|
table_alias ||= implicit_table_alias
|
542
540
|
table_name = table_alias || table
|
data/lib/sequel/dataset/sql.rb
CHANGED
@@ -49,10 +49,6 @@ module Sequel
|
|
49
49
|
end
|
50
50
|
when Dataset, Array, LiteralString
|
51
51
|
values = vals
|
52
|
-
else
|
53
|
-
if vals.respond_to?(:values) && (v = vals.values).is_a?(Hash)
|
54
|
-
return insert_sql(v)
|
55
|
-
end
|
56
52
|
end
|
57
53
|
when 2
|
58
54
|
if (v0 = values.at(0)).is_a?(Array) && ((v1 = values.at(1)).is_a?(Array) || v1.is_a?(Dataset) || v1.is_a?(LiteralString))
|
@@ -182,6 +178,9 @@ module Sequel
|
|
182
178
|
clauses.map{|clause| :"#{type}_#{clause}_sql"}.freeze
|
183
179
|
end
|
184
180
|
|
181
|
+
# Map of emulated function names to native function names.
|
182
|
+
EMULATED_FUNCTION_MAP = {}
|
183
|
+
|
185
184
|
WILDCARD = LiteralString.new('*').freeze
|
186
185
|
ALL = ' ALL'.freeze
|
187
186
|
AND_SEPARATOR = " AND ".freeze
|
@@ -272,6 +271,7 @@ module Sequel
|
|
272
271
|
TIMESTAMP_FORMAT = "'%Y-%m-%d %H:%M:%S%N%z'".freeze
|
273
272
|
STANDARD_TIMESTAMP_FORMAT = "TIMESTAMP #{TIMESTAMP_FORMAT}".freeze
|
274
273
|
TWO_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::TWO_ARITY_OPERATORS
|
274
|
+
REGEXP_OPERATORS = ::Sequel::SQL::ComplexExpression::REGEXP_OPERATORS
|
275
275
|
UNDERSCORE = '_'.freeze
|
276
276
|
UPDATE = 'UPDATE'.freeze
|
277
277
|
UPDATE_CLAUSE_METHODS = clause_methods(:update, %w'update table set where')
|
@@ -456,6 +456,9 @@ module Sequel
|
|
456
456
|
sql << PAREN_CLOSE
|
457
457
|
end
|
458
458
|
when *TWO_ARITY_OPERATORS
|
459
|
+
if REGEXP_OPERATORS.include?(op) && !supports_regexp?
|
460
|
+
raise InvalidOperation, "Pattern matching via regular expressions is not supported on #{db.database_type}"
|
461
|
+
end
|
459
462
|
sql << PAREN_OPEN
|
460
463
|
literal_append(sql, args.at(0))
|
461
464
|
sql << SPACE << op.to_s << SPACE
|
@@ -493,15 +496,18 @@ module Sequel
|
|
493
496
|
sql << constant.to_s
|
494
497
|
end
|
495
498
|
|
496
|
-
# SQL fragment specifying an SQL function call
|
499
|
+
# SQL fragment specifying an emulated SQL function call.
|
500
|
+
# By default, assumes just the function name may need to
|
501
|
+
# be emulated, adapters should set an EMULATED_FUNCTION_MAP
|
502
|
+
# hash mapping emulated functions to native functions in
|
503
|
+
# their dataset class to setup the emulation.
|
504
|
+
def emulated_function_sql_append(sql, f)
|
505
|
+
_function_sql_append(sql, native_function_name(f.f), f.args)
|
506
|
+
end
|
507
|
+
|
508
|
+
# SQL fragment specifying an SQL function call without emulation.
|
497
509
|
def function_sql_append(sql, f)
|
498
|
-
sql
|
499
|
-
args = f.args
|
500
|
-
if args.empty?
|
501
|
-
sql << FUNCTION_EMPTY
|
502
|
-
else
|
503
|
-
literal_append(sql, args)
|
504
|
-
end
|
510
|
+
_function_sql_append(sql, f.f, f.args)
|
505
511
|
end
|
506
512
|
|
507
513
|
# SQL fragment specifying a JOIN clause without ON or USING.
|
@@ -718,6 +724,16 @@ module Sequel
|
|
718
724
|
|
719
725
|
private
|
720
726
|
|
727
|
+
# Backbone of function_sql_append and emulated_function_sql_append.
|
728
|
+
def _function_sql_append(sql, name, args)
|
729
|
+
sql << name.to_s
|
730
|
+
if args.empty?
|
731
|
+
sql << FUNCTION_EMPTY
|
732
|
+
else
|
733
|
+
literal_append(sql, args)
|
734
|
+
end
|
735
|
+
end
|
736
|
+
|
721
737
|
# Formats the truncate statement. Assumes the table given has already been
|
722
738
|
# literalized.
|
723
739
|
def _truncate_sql(table)
|
@@ -1127,6 +1143,11 @@ module Sequel
|
|
1127
1143
|
BOOL_TRUE
|
1128
1144
|
end
|
1129
1145
|
|
1146
|
+
# Get the native function name given the emulated function name.
|
1147
|
+
def native_function_name(emulated_function)
|
1148
|
+
self.class.const_get(:EMULATED_FUNCTION_MAP).fetch(emulated_function, emulated_function)
|
1149
|
+
end
|
1150
|
+
|
1130
1151
|
# Returns a qualified column name (including a table name) if the column
|
1131
1152
|
# name isn't already qualified.
|
1132
1153
|
def qualified_column_name(column, table)
|
@@ -0,0 +1,451 @@
|
|
1
|
+
# The constraint_validations extension is designed to easily create database
|
2
|
+
# constraints inside create_table and alter_table blocks. It also adds
|
3
|
+
# relevant metadata about the constraints to a separate table, which the
|
4
|
+
# constraint_validations model plugin uses to setup automatic validations.
|
5
|
+
#
|
6
|
+
# To use this extension, you first need to load it into the database:
|
7
|
+
#
|
8
|
+
# DB.extension(:constraint_validations)
|
9
|
+
#
|
10
|
+
# Note that you should only need to do this when modifying the constraint
|
11
|
+
# validations (i.e. when migrating). You should probably not load this
|
12
|
+
# extension in general application code.
|
13
|
+
#
|
14
|
+
# You also need to make sure to add the metadata table for the automatic
|
15
|
+
# validations. By default, this table is called sequel_constraint_validations.
|
16
|
+
#
|
17
|
+
# DB.create_constraint_validations_table
|
18
|
+
#
|
19
|
+
# This table should only be created once. For new applications, you
|
20
|
+
# generally want to create it first, before creating any other application
|
21
|
+
# tables.
|
22
|
+
#
|
23
|
+
# Because migrations instance_eval the up and down blocks on a database,
|
24
|
+
# using this extension in a migration can be done via:
|
25
|
+
#
|
26
|
+
# Sequel.migration do
|
27
|
+
# up do
|
28
|
+
# extension(:constraint_validations)
|
29
|
+
# # ...
|
30
|
+
# end
|
31
|
+
# down do
|
32
|
+
# extension(:constraint_validations)
|
33
|
+
# # ...
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# However, note that you cannot use change migrations with this extension,
|
38
|
+
# you need to use separate up/down migrations.
|
39
|
+
#
|
40
|
+
# The API for creating the constraints with automatic validations is
|
41
|
+
# similar to the validation_helpers model plugin API. However,
|
42
|
+
# instead of having separate validates_* methods, it just adds a validate
|
43
|
+
# method that accepts a block to the schema generators. Like the
|
44
|
+
# create_table and alter_table blocks, this block is instance_evaled and
|
45
|
+
# offers its own DSL. Example:
|
46
|
+
#
|
47
|
+
# DB.create_table(:table) do
|
48
|
+
# Integer :id
|
49
|
+
# String :name
|
50
|
+
#
|
51
|
+
# validate do
|
52
|
+
# presence :id
|
53
|
+
# min_length 5, :name
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# instance_eval is used in this case because create_table and alter_table
|
58
|
+
# already use instance_eval, so losing access to the surrounding receiver
|
59
|
+
# is not an issue.
|
60
|
+
#
|
61
|
+
# Here's a breakdown of the constraints created for each constraint validation
|
62
|
+
# method:
|
63
|
+
#
|
64
|
+
# All constraints except unique unless :allow_nil is true :: CHECK column IS NOT NULL
|
65
|
+
# presence (String column) :: CHECK trim(column) != ''
|
66
|
+
# exact_length 5 :: CHECK char_length(column) = 5
|
67
|
+
# min_length 5 :: CHECK char_length(column) >= 5
|
68
|
+
# max_length 5 :: CHECK char_length(column) <= 5
|
69
|
+
# length_range 3..5 :: CHECK char_length(column) >= 3 AND char_length(column) <= 5
|
70
|
+
# length_range 3...5 :: CHECK char_length(column) >= 3 AND char_length(column) < 5
|
71
|
+
# format /foo\\d+/ :: CHECK column ~ 'foo\\d+'
|
72
|
+
# format /foo\\d+/i :: CHECK column ~* 'foo\\d+'
|
73
|
+
# like 'foo%' :: CHECK column LIKE 'foo%'
|
74
|
+
# ilike 'foo%' :: CHECK column ILIKE 'foo%'
|
75
|
+
# includes ['a', 'b'] :: CHECK column IN ('a', 'b')
|
76
|
+
# includes [1, 2] :: CHECK column IN (1, 2)
|
77
|
+
# includes 3..5 :: CHECK column >= 3 AND column <= 5
|
78
|
+
# includes 3...5 :: CHECK column >= 3 AND column < 5
|
79
|
+
# unique :: UNIQUE (column)
|
80
|
+
#
|
81
|
+
# There are some additional API differences:
|
82
|
+
#
|
83
|
+
# * Only the :message and :allow_nil options are respected. The :allow_blank
|
84
|
+
# and :allow_missing options are not respected.
|
85
|
+
# * A new option, :name, is respected, for providing the name of the constraint. It is highly
|
86
|
+
# recommended that you provide a name for all constraint validations, as
|
87
|
+
# otherwise, it is difficult to drop the constraints later.
|
88
|
+
# * The includes validation only supports an array of strings, and array of
|
89
|
+
# integers, and a range of integers.
|
90
|
+
# * There are like and ilike validations, which are similar to the format
|
91
|
+
# validation but use a case sensitive or case insensitive LIKE pattern. LIKE
|
92
|
+
# patters are very simple, so many regexp patterns cannot be expressed by
|
93
|
+
# them, but only a couple databases (PostgreSQL and MySQL) support regexp
|
94
|
+
# patterns.
|
95
|
+
# * When using the unique validation, column names cannot have embedded commas.
|
96
|
+
# For similar reasons, when using an includes validation with an array of
|
97
|
+
# strings, none of the strings in the array can have embedded commas.
|
98
|
+
# * The unique validation does not support an arbitrary number of columns.
|
99
|
+
# For a single column, just the symbol should be used, and for an array
|
100
|
+
# of columns, an array of symbols should be used. There is no support
|
101
|
+
# for creating two separate unique validations for separate columns in
|
102
|
+
# a single call.
|
103
|
+
# * A drop method can be called with a constraint name in a alter_table
|
104
|
+
# validate block to drop an existing constraint and the related
|
105
|
+
# validation metadata.
|
106
|
+
# * While it is allowed to create a presence constraint with :allow_nil
|
107
|
+
# set to true, doing so does not create a constraint unless the column
|
108
|
+
# has String type.
|
109
|
+
#
|
110
|
+
# Note that this extension has the following issues on certain databases:
|
111
|
+
#
|
112
|
+
# * MySQL does not support check constraints (they are parsed but ignored),
|
113
|
+
# so using this extension does not actually set up constraints on MySQL,
|
114
|
+
# except for the unique constraint. It can still be used on MySQL to
|
115
|
+
# add the validation metadata so that the plugin can setup automatic
|
116
|
+
# validations.
|
117
|
+
# * On SQLite, adding constraints to a table is not supported, so it must
|
118
|
+
# be emulated by dropping the table and recreating it with the constraints.
|
119
|
+
# If you want to use this plugin on SQLite with an alter_table block,
|
120
|
+
# you should drop all constraint validation metadata using
|
121
|
+
# <tt>drop_constraint_validations_for(:table=>'table')</tt>, and then
|
122
|
+
# readd all constraints you want to use inside the alter table block,
|
123
|
+
# making no other changes inside the alter_table block.
|
124
|
+
|
125
|
+
module Sequel
|
126
|
+
module ConstraintValidations
|
127
|
+
# The default table name used for the validation metadata.
|
128
|
+
DEFAULT_CONSTRAINT_VALIDATIONS_TABLE = :sequel_constraint_validations
|
129
|
+
|
130
|
+
# Set the default validation metadata table name if it has not already
|
131
|
+
# been set.
|
132
|
+
def self.extended(db)
|
133
|
+
db.constraint_validations_table ||= DEFAULT_CONSTRAINT_VALIDATIONS_TABLE
|
134
|
+
end
|
135
|
+
|
136
|
+
# This is the DSL class used for the validate block inside create_table and
|
137
|
+
# alter_table.
|
138
|
+
class Generator
|
139
|
+
# Store the schema generator that encloses this validates block.
|
140
|
+
def initialize(generator)
|
141
|
+
@generator = generator
|
142
|
+
end
|
143
|
+
|
144
|
+
# Create constraint validation methods that don't take an argument
|
145
|
+
%w'presence unique'.each do |v|
|
146
|
+
class_eval(<<-END, __FILE__, __LINE__+1)
|
147
|
+
def #{v}(columns, opts={})
|
148
|
+
@generator.validation({:type=>:#{v}, :columns=>Array(columns)}.merge(opts))
|
149
|
+
end
|
150
|
+
END
|
151
|
+
end
|
152
|
+
|
153
|
+
# Create constraint validation methods that take an argument
|
154
|
+
%w'exact_length min_length max_length length_range format like ilike includes'.each do |v|
|
155
|
+
class_eval(<<-END, __FILE__, __LINE__+1)
|
156
|
+
def #{v}(arg, columns, opts={})
|
157
|
+
@generator.validation({:type=>:#{v}, :columns=>Array(columns), :arg=>arg}.merge(opts))
|
158
|
+
end
|
159
|
+
END
|
160
|
+
end
|
161
|
+
|
162
|
+
# Given the name of a constraint, drop that constraint from the database,
|
163
|
+
# and remove the related validation metadata.
|
164
|
+
def drop(constraint)
|
165
|
+
@generator.validation({:type=>:drop, :name=>constraint})
|
166
|
+
end
|
167
|
+
|
168
|
+
# Alias of instance_eval for a nicer API.
|
169
|
+
def process(&block)
|
170
|
+
instance_eval(&block)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Additional methods for the create_table generator to support constraint validations.
|
175
|
+
module CreateTableGeneratorMethods
|
176
|
+
# An array of stored validation metadata, used later by the database to create
|
177
|
+
# constraints.
|
178
|
+
attr_reader :validations
|
179
|
+
|
180
|
+
# Add a validation metadata hash to the stored array.
|
181
|
+
def validation(opts)
|
182
|
+
@validations << opts
|
183
|
+
end
|
184
|
+
|
185
|
+
# Call into the validate DSL for creating constraint validations.
|
186
|
+
def validate(&block)
|
187
|
+
Generator.new(self).process(&block)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Additional methods for the alter_table generator to support constraint validations,
|
192
|
+
# used to give it a more similar API to the create_table generator.
|
193
|
+
module AlterTableGeneratorMethods
|
194
|
+
include CreateTableGeneratorMethods
|
195
|
+
|
196
|
+
# Alias of add_constraint for similarity to create_table generator.
|
197
|
+
def constraint(*args)
|
198
|
+
add_constraint(*args)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Alias of add_unique_constraint for similarity to create_table generator.
|
202
|
+
def unique(*args)
|
203
|
+
add_unique_constraint(*args)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# The name of the table storing the validation metadata. If modifying this
|
208
|
+
# from the default, this should be changed directly after loading the
|
209
|
+
# extension into the database
|
210
|
+
attr_accessor :constraint_validations_table
|
211
|
+
|
212
|
+
# Create the table storing the validation metadata for all of the
|
213
|
+
# constraints created by this extension.
|
214
|
+
def create_constraint_validations_table
|
215
|
+
create_table(constraint_validations_table) do
|
216
|
+
String :table, :null=>false
|
217
|
+
String :constraint_name
|
218
|
+
String :validation_type, :null=>false
|
219
|
+
String :column, :null=>false
|
220
|
+
String :argument
|
221
|
+
String :message
|
222
|
+
TrueClass :allow_nil
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Drop the constraint validations table.
|
227
|
+
def drop_constraint_validations_table
|
228
|
+
drop_table(constraint_validations_table)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Delete validation metadata for specific constraints. At least
|
232
|
+
# one of the following options should be specified:
|
233
|
+
#
|
234
|
+
# :table :: The table containing the constraint
|
235
|
+
# :column :: The column affected by the constraint
|
236
|
+
# :constraint :: The name of the related constraint
|
237
|
+
#
|
238
|
+
# The main reason for this method is when dropping tables
|
239
|
+
# or columns. If you have previously defined a constraint
|
240
|
+
# validation on the table or column, you should delete the
|
241
|
+
# related metadata when dropping the table or column.
|
242
|
+
# For a table, this isn't a big issue, as it will just result
|
243
|
+
# in some wasted space, but for columns, if you don't drop
|
244
|
+
# the related metadata, it could make it impossible to save
|
245
|
+
# rows, since a validation for a nonexistent column will be
|
246
|
+
# created.
|
247
|
+
def drop_constraint_validations_for(opts={})
|
248
|
+
ds = from(constraint_validations_table)
|
249
|
+
if table = opts[:table]
|
250
|
+
ds = ds.where(:table=>constraint_validations_literal_table(table))
|
251
|
+
end
|
252
|
+
if column = opts[:column]
|
253
|
+
ds = ds.where(:column=>column.to_s)
|
254
|
+
end
|
255
|
+
if constraint = opts[:constraint]
|
256
|
+
ds = ds.where(:constraint_name=>constraint.to_s)
|
257
|
+
end
|
258
|
+
unless table || column || constraint
|
259
|
+
raise Error, "must specify :table, :column, or :constraint when dropping constraint validations"
|
260
|
+
end
|
261
|
+
ds.delete
|
262
|
+
end
|
263
|
+
|
264
|
+
private
|
265
|
+
|
266
|
+
# Modify the default create_table generator to include
|
267
|
+
# the constraint validation methods.
|
268
|
+
def alter_table_generator(&block)
|
269
|
+
super do
|
270
|
+
extend AlterTableGeneratorMethods
|
271
|
+
@validations = []
|
272
|
+
instance_eval(&block) if block
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
# After running all of the table alteration statements,
|
277
|
+
# if there were any constraint validations, run table alteration
|
278
|
+
# statements to create related constraints. This is purposely
|
279
|
+
# run after the other statements, as the presence validation
|
280
|
+
# in alter table requires introspecting the modified model
|
281
|
+
# schema.
|
282
|
+
def apply_alter_table_generator(name, generator)
|
283
|
+
super
|
284
|
+
unless generator.validations.empty?
|
285
|
+
gen = alter_table_generator
|
286
|
+
process_generator_validations(name, gen, generator.validations)
|
287
|
+
apply_alter_table(name, gen.operations)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# The value of a blank string. An empty string by default, but nil
|
292
|
+
# on Oracle as Oracle treats the empty string as NULL.
|
293
|
+
def blank_string_value
|
294
|
+
if database_type == :oracle
|
295
|
+
nil
|
296
|
+
else
|
297
|
+
''
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Return an unquoted literal form of the table name.
|
302
|
+
# This allows the code to handle schema qualified tables,
|
303
|
+
# without quoting all table names.
|
304
|
+
def constraint_validations_literal_table(table)
|
305
|
+
ds = dataset
|
306
|
+
ds.quote_identifiers = false
|
307
|
+
ds.literal(table)
|
308
|
+
end
|
309
|
+
|
310
|
+
# Before creating the table, add constraints for all of the
|
311
|
+
# generators validations to the generator.
|
312
|
+
def create_table_from_generator(name, generator, options)
|
313
|
+
unless generator.validations.empty?
|
314
|
+
process_generator_validations(name, generator, generator.validations)
|
315
|
+
end
|
316
|
+
super
|
317
|
+
end
|
318
|
+
|
319
|
+
# Modify the default create_table generator to include
|
320
|
+
# the constraint validation methods.
|
321
|
+
def create_table_generator(&block)
|
322
|
+
super do
|
323
|
+
extend CreateTableGeneratorMethods
|
324
|
+
@validations = []
|
325
|
+
instance_eval(&block) if block
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
# For the given table, generator, and validations, add constraints
|
330
|
+
# to the generator for each of the validations, as well as adding
|
331
|
+
# validation metadata to the constraint validations table.
|
332
|
+
def process_generator_validations(table, generator, validations)
|
333
|
+
drop_rows = []
|
334
|
+
rows = validations.map do |val|
|
335
|
+
columns, arg, constraint, validation_type, message, allow_nil = val.values_at(:columns, :arg, :name, :type, :message, :allow_nil)
|
336
|
+
|
337
|
+
case validation_type
|
338
|
+
when :presence
|
339
|
+
string_check = columns.select{|c| generator_string_column?(generator, table, c)}.map{|c| [Sequel.trim(c), blank_string_value]}
|
340
|
+
generator_add_constraint_from_validation(generator, val, (Sequel.negate(string_check) unless string_check.empty?))
|
341
|
+
when :exact_length
|
342
|
+
generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| {Sequel.char_length(c) => arg}}))
|
343
|
+
when :min_length
|
344
|
+
generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| Sequel.char_length(c) >= arg}))
|
345
|
+
when :max_length
|
346
|
+
generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| Sequel.char_length(c) <= arg}))
|
347
|
+
when :length_range
|
348
|
+
op = arg.exclude_end? ? :< : :<=
|
349
|
+
generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| (Sequel.char_length(c) >= arg.begin) & Sequel.char_length(c).send(op, arg.end)}))
|
350
|
+
arg = "#{arg.begin}..#{'.' if arg.exclude_end?}#{arg.end}"
|
351
|
+
when :format
|
352
|
+
generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| {c => arg}}))
|
353
|
+
if arg.casefold?
|
354
|
+
validation_type = :iformat
|
355
|
+
end
|
356
|
+
arg = arg.source
|
357
|
+
when :includes
|
358
|
+
generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| {c => arg}}))
|
359
|
+
if arg.is_a?(Range)
|
360
|
+
if (b = arg.begin).is_a?(Integer) && (e = arg.end).is_a?(Integer)
|
361
|
+
validation_type = :includes_int_range
|
362
|
+
arg = "#{arg.begin}..#{'.' if arg.exclude_end?}#{arg.end}"
|
363
|
+
else
|
364
|
+
raise Error, "validates includes with a range only supports integers currently, cannot handle: #{arg.inspect}"
|
365
|
+
end
|
366
|
+
elsif arg.is_a?(Array)
|
367
|
+
if arg.all?{|x| x.is_a?(Integer)}
|
368
|
+
validation_type = :includes_int_array
|
369
|
+
elsif arg.all?{|x| x.is_a?(String)}
|
370
|
+
validation_type = :includes_str_array
|
371
|
+
else
|
372
|
+
raise Error, "validates includes with an array only supports strings and integers currently, cannot handle: #{arg.inspect}"
|
373
|
+
end
|
374
|
+
arg = arg.join(',')
|
375
|
+
else
|
376
|
+
raise Error, "validates includes only supports arrays and ranges currently, cannot handle: #{arg.inspect}"
|
377
|
+
end
|
378
|
+
when :like, :ilike
|
379
|
+
generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| Sequel.send(validation_type, c, arg)}))
|
380
|
+
when :unique
|
381
|
+
generator.unique(columns, :name=>constraint)
|
382
|
+
columns = [columns.join(',')]
|
383
|
+
when :drop
|
384
|
+
if generator.is_a?(Sequel::Schema::AlterTableGenerator)
|
385
|
+
unless constraint
|
386
|
+
raise Error, 'cannot drop a constraint validation without a constraint name'
|
387
|
+
end
|
388
|
+
generator.drop_constraint(constraint)
|
389
|
+
drop_rows << [constraint_validations_literal_table(table), constraint.to_s]
|
390
|
+
columns = []
|
391
|
+
else
|
392
|
+
raise Error, 'cannot drop a constraint validation in a create_table generator'
|
393
|
+
end
|
394
|
+
else
|
395
|
+
raise Error, "invalid or missing validation type: #{val.inspect}"
|
396
|
+
end
|
397
|
+
|
398
|
+
columns.map do |column|
|
399
|
+
{:table=>constraint_validations_literal_table(table), :constraint_name=>(constraint.to_s if constraint), :validation_type=>validation_type.to_s, :column=>column.to_s, :argument=>(arg.to_s if arg), :message=>(message.to_s if message), :allow_nil=>allow_nil}
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
ds = from(:sequel_constraint_validations)
|
404
|
+
ds.multi_insert(rows.flatten)
|
405
|
+
unless drop_rows.empty?
|
406
|
+
ds.where([:table, :constraint_name]=>drop_rows).delete
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
# Add the constraint to the generator, including a NOT NULL constraint
|
411
|
+
# for all columns unless the :allow_nil option is given.
|
412
|
+
def generator_add_constraint_from_validation(generator, val, cons)
|
413
|
+
unless val[:allow_nil]
|
414
|
+
nil_cons = Sequel.negate(val[:columns].map{|c| [c, nil]})
|
415
|
+
cons = cons ? Sequel.&(nil_cons, cons) : nil_cons
|
416
|
+
end
|
417
|
+
|
418
|
+
if cons
|
419
|
+
generator.constraint(val[:name], cons)
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
|
424
|
+
# Introspect the generator to determine if column
|
425
|
+
# created is a string or not.
|
426
|
+
def generator_string_column?(generator, table, c)
|
427
|
+
if generator.is_a?(Sequel::Schema::AlterTableGenerator)
|
428
|
+
# This is the alter table case, which runs after the
|
429
|
+
# table has been altered, so just check the database
|
430
|
+
# schema for the column.
|
431
|
+
schema(table).each do |col, sch|
|
432
|
+
if col == c
|
433
|
+
return sch[:type] == :string
|
434
|
+
end
|
435
|
+
end
|
436
|
+
false
|
437
|
+
else
|
438
|
+
# This is the create table case, check the metadata
|
439
|
+
# for the column to be created to see if it is a string.
|
440
|
+
generator.columns.each do |col|
|
441
|
+
if col[:name] == c
|
442
|
+
return [String, :text, :varchar].include?(col[:type])
|
443
|
+
end
|
444
|
+
end
|
445
|
+
false
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
Database.register_extension(:constraint_validations, ConstraintValidations)
|
451
|
+
end
|