module Sequel::ConstraintValidations
Constants
- DEFAULT_CONSTRAINT_VALIDATIONS_TABLE
The default table name used for the validation metadata.
- OPERATORS
- REVERSE_OPERATOR_MAP
Attributes
The name of the table storing the validation metadata. If modifying this from the default, this should be changed directly after loading the extension into the database
Public Class Methods
Set the default validation metadata table name if it has not already been set.
# File lib/sequel/extensions/constraint_validations.rb 150 def self.extended(db) 151 db.constraint_validations_table ||= DEFAULT_CONSTRAINT_VALIDATIONS_TABLE 152 end
Public Instance Methods
Modify the default alter_table generator to include the constraint validation methods.
# File lib/sequel/extensions/constraint_validations.rb 321 def alter_table_generator(&block) 322 super do 323 extend AlterTableGeneratorMethods 324 @validations = [] 325 instance_exec(&block) if block 326 end 327 end
Create the table storing the validation metadata for all of the constraints created by this extension.
# File lib/sequel/extensions/constraint_validations.rb 249 def create_constraint_validations_table 250 create_table(constraint_validations_table) do 251 String :table, :null=>false 252 String :constraint_name 253 String :validation_type, :null=>false 254 String :column, :null=>false 255 String :argument 256 String :message 257 TrueClass :allow_nil 258 end 259 end
Modify the default create_table generator to include the constraint validation methods.
# File lib/sequel/extensions/constraint_validations.rb 263 def create_table_generator(&block) 264 super do 265 extend CreateTableGeneratorMethods 266 @validations = [] 267 instance_exec(&block) if block 268 end 269 end
Delete validation metadata for specific constraints. At least one of the following options should be specified:
- :table
-
The table containing the constraint
- :column
-
The column affected by the constraint
- :constraint
-
The name of the related constraint
The main reason for this method is when dropping tables or columns. If you have previously defined a constraint validation on the table or column, you should delete the related metadata when dropping the table or column. For a table, this isn't a big issue, as it will just result in some wasted space, but for columns, if you don't drop the related metadata, it could make it impossible to save rows, since a validation for a nonexistent column will be created.
# File lib/sequel/extensions/constraint_validations.rb 302 def drop_constraint_validations_for(opts=OPTS) 303 ds = from(constraint_validations_table) 304 if table = opts[:table] 305 ds = ds.where(:table=>constraint_validations_literal_table(table)) 306 end 307 if column = opts[:column] 308 ds = ds.where(:column=>column.to_s) 309 end 310 if constraint = opts[:constraint] 311 ds = ds.where(:constraint_name=>constraint.to_s) 312 end 313 unless table || column || constraint 314 raise Error, "must specify :table, :column, or :constraint when dropping constraint validations" 315 end 316 ds.delete 317 end
Drop the constraint validations table.
# File lib/sequel/extensions/constraint_validations.rb 282 def drop_constraint_validations_table 283 drop_table(constraint_validations_table) 284 end
Drop all constraint validations for a table if dropping the table.
# File lib/sequel/extensions/constraint_validations.rb 272 def drop_table(*names) 273 names.each do |name| 274 if !name.is_a?(Hash) && table_exists?(constraint_validations_table) 275 drop_constraint_validations_for(:table=>name) 276 end 277 end 278 super 279 end
Private Instance Methods
After running all of the table alteration statements, if there were any constraint validations, run table alteration statements to create related constraints. This is purposely run after the other statements, as the presence validation in alter table requires introspecting the modified model schema.
# File lib/sequel/extensions/constraint_validations.rb 337 def apply_alter_table_generator(name, generator) 338 super 339 unless generator.validations.empty? 340 gen = alter_table_generator 341 process_generator_validations(name, gen, generator.validations) 342 apply_alter_table(name, gen.operations) 343 end 344 end
# File lib/sequel/extensions/constraint_validations.rb 372 def constraint_validation_expression(cols, allow_nil) 373 exprs = cols.map do |c| 374 expr = yield c 375 if allow_nil 376 Sequel.|({c=>nil}, expr) 377 else 378 Sequel.&(Sequel.~(c=>nil), expr) 379 end 380 end 381 Sequel.&(*exprs) 382 end
Return an unquoted literal form of the table name. This allows the code to handle schema qualified tables, without quoting all table names.
# File lib/sequel/extensions/constraint_validations.rb 359 def constraint_validations_literal_table(table) 360 dataset.with_quote_identifiers(false).literal(table) 361 end
Before creating the table, add constraints for all of the generators validations to the generator.
# File lib/sequel/extensions/constraint_validations.rb 365 def create_table_from_generator(name, generator, options) 366 unless generator.validations.empty? 367 process_generator_validations(name, generator, generator.validations) 368 end 369 super 370 end
Introspect the generator to determine if column created is a string or not.
# File lib/sequel/extensions/constraint_validations.rb 485 def generator_string_column?(generator, table, c) 486 if generator.is_a?(Sequel::Schema::AlterTableGenerator) 487 # This is the alter table case, which runs after the 488 # table has been altered, so just check the database 489 # schema for the column. 490 schema(table).each do |col, sch| 491 if col == c 492 return sch[:type] == :string 493 end 494 end 495 false 496 else 497 # This is the create table case, check the metadata 498 # for the column to be created to see if it is a string. 499 generator.columns.each do |col| 500 if col[:name] == c 501 return [String, :text, :varchar].include?(col[:type]) 502 end 503 end 504 false 505 end 506 end
For the given table, generator, and validations, add constraints to the generator for each of the validations, as well as adding validation metadata to the constraint validations table.
# File lib/sequel/extensions/constraint_validations.rb 387 def process_generator_validations(table, generator, validations) 388 drop_rows = [] 389 rows = validations.map do |val| 390 columns, arg, constraint, validation_type, message, allow_nil = val.values_at(:columns, :arg, :name, :type, :message, :allow_nil) 391 392 case validation_type 393 when :presence 394 strings, non_strings = columns.partition{|c| generator_string_column?(generator, table, c)} 395 if !non_strings.empty? && !allow_nil 396 non_strings_expr = Sequel.&(*non_strings.map{|c| Sequel.~(c=>nil)}) 397 end 398 399 unless strings.empty? 400 strings_expr = constraint_validation_expression(strings, allow_nil){|c| Sequel.~(Sequel.trim(c) => blank_string_value)} 401 end 402 403 expr = if non_strings_expr && strings_expr 404 Sequel.&(strings_expr, non_strings_expr) 405 else 406 strings_expr || non_strings_expr 407 end 408 409 if expr 410 generator.constraint(constraint, expr) 411 end 412 when :exact_length 413 generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| {Sequel.char_length(c) => arg}}) 414 when :min_length 415 generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| Sequel.char_length(c) >= arg}) 416 when :max_length 417 generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| Sequel.char_length(c) <= arg}) 418 when *REVERSE_OPERATOR_MAP.keys 419 generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| Sequel.identifier(c).public_send(REVERSE_OPERATOR_MAP[validation_type], arg)}) 420 when :length_range 421 op = arg.exclude_end? ? :< : :<= 422 generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| (Sequel.char_length(c) >= arg.begin) & Sequel.char_length(c).public_send(op, arg.end)}) 423 arg = "#{arg.begin}..#{'.' if arg.exclude_end?}#{arg.end}" 424 when :format 425 generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| {c => arg}}) 426 if arg.casefold? 427 validation_type = :iformat 428 end 429 arg = arg.source 430 when :includes 431 generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| {c => arg}}) 432 if arg.is_a?(Range) 433 if arg.begin.is_a?(Integer) && arg.end.is_a?(Integer) 434 validation_type = :includes_int_range 435 arg = "#{arg.begin}..#{'.' if arg.exclude_end?}#{arg.end}" 436 else 437 raise Error, "validates includes with a range only supports integers currently, cannot handle: #{arg.inspect}" 438 end 439 elsif arg.is_a?(Array) 440 if arg.all?{|x| x.is_a?(Integer)} 441 validation_type = :includes_int_array 442 elsif arg.all?{|x| x.is_a?(String)} 443 validation_type = :includes_str_array 444 else 445 raise Error, "validates includes with an array only supports strings and integers currently, cannot handle: #{arg.inspect}" 446 end 447 arg = arg.join(',') 448 else 449 raise Error, "validates includes only supports arrays and ranges currently, cannot handle: #{arg.inspect}" 450 end 451 when :like, :ilike 452 generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| Sequel.public_send(validation_type, c, arg)}) 453 when :unique 454 generator.unique(columns, :name=>constraint) 455 columns = [columns.join(',')] 456 when :drop 457 if generator.is_a?(Sequel::Schema::AlterTableGenerator) 458 unless constraint 459 raise Error, 'cannot drop a constraint validation without a constraint name' 460 end 461 generator.drop_constraint(constraint) 462 drop_rows << [constraint_validations_literal_table(table), constraint.to_s] 463 columns = [] 464 else 465 raise Error, 'cannot drop a constraint validation in a create_table generator' 466 end 467 else 468 raise Error, "invalid or missing validation type: #{val.inspect}" 469 end 470 471 columns.map do |column| 472 {: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} 473 end 474 end 475 476 ds = from(constraint_validations_table) 477 unless drop_rows.empty? 478 ds.where([:table, :constraint_name]=>drop_rows).delete 479 end 480 ds.multi_insert(rows.flatten) 481 end