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, line 150 def self.extended(db) db.constraint_validations_table ||= DEFAULT_CONSTRAINT_VALIDATIONS_TABLE end
Public Instance Methods
Modify the default alter_table generator to include the constraint validation methods.
# File lib/sequel/extensions/constraint_validations.rb, line 321 def alter_table_generator(&block) super do extend AlterTableGeneratorMethods @validations = [] instance_exec(&block) if block end end
Create the table storing the validation metadata for all of the constraints created by this extension.
# File lib/sequel/extensions/constraint_validations.rb, line 249 def create_constraint_validations_table create_table(constraint_validations_table) do String :table, :null=>false String :constraint_name String :validation_type, :null=>false String :column, :null=>false String :argument String :message TrueClass :allow_nil end end
Modify the default create_table generator to include the constraint validation methods.
# File lib/sequel/extensions/constraint_validations.rb, line 263 def create_table_generator(&block) super do extend CreateTableGeneratorMethods @validations = [] instance_exec(&block) if block end 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, line 302 def drop_constraint_validations_for(opts=OPTS) ds = from(constraint_validations_table) if table = opts[:table] ds = ds.where(:table=>constraint_validations_literal_table(table)) end if column = opts[:column] ds = ds.where(:column=>column.to_s) end if constraint = opts[:constraint] ds = ds.where(:constraint_name=>constraint.to_s) end unless table || column || constraint raise Error, "must specify :table, :column, or :constraint when dropping constraint validations" end ds.delete end
Drop the constraint validations table.
# File lib/sequel/extensions/constraint_validations.rb, line 282 def drop_constraint_validations_table drop_table(constraint_validations_table) end
Drop all constraint validations for a table if dropping the table.
# File lib/sequel/extensions/constraint_validations.rb, line 272 def drop_table(*names) names.each do |name| if !name.is_a?(Hash) && table_exists?(constraint_validations_table) drop_constraint_validations_for(:table=>name) end end super 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, line 337 def apply_alter_table_generator(name, generator) super unless generator.validations.empty? gen = alter_table_generator process_generator_validations(name, gen, generator.validations) apply_alter_table(name, gen.operations) end end
# File lib/sequel/extensions/constraint_validations.rb, line 372 def constraint_validation_expression(cols, allow_nil) exprs = cols.map do |c| expr = yield c if allow_nil Sequel.|({c=>nil}, expr) else Sequel.&(Sequel.~(c=>nil), expr) end end Sequel.&(*exprs) 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, line 359 def constraint_validations_literal_table(table) dataset.with_quote_identifiers(false).literal(table) end
Before creating the table, add constraints for all of the generators validations to the generator.
# File lib/sequel/extensions/constraint_validations.rb, line 365 def create_table_from_generator(name, generator, options) unless generator.validations.empty? process_generator_validations(name, generator, generator.validations) end super end
Introspect the generator to determine if column created is a string or not.
# File lib/sequel/extensions/constraint_validations.rb, line 485 def generator_string_column?(generator, table, c) if generator.is_a?(Sequel::Schema::AlterTableGenerator) # This is the alter table case, which runs after the # table has been altered, so just check the database # schema for the column. schema(table).each do |col, sch| if col == c return sch[:type] == :string end end false else # This is the create table case, check the metadata # for the column to be created to see if it is a string. generator.columns.each do |col| if col[:name] == c return [String, :text, :varchar].include?(col[:type]) end end false end 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, line 387 def process_generator_validations(table, generator, validations) drop_rows = [] rows = validations.map do |val| columns, arg, constraint, validation_type, message, allow_nil = val.values_at(:columns, :arg, :name, :type, :message, :allow_nil) case validation_type when :presence strings, non_strings = columns.partition{|c| generator_string_column?(generator, table, c)} if !non_strings.empty? && !allow_nil non_strings_expr = Sequel.&(*non_strings.map{|c| Sequel.~(c=>nil)}) end unless strings.empty? strings_expr = constraint_validation_expression(strings, allow_nil){|c| Sequel.~(Sequel.trim(c) => blank_string_value)} end expr = if non_strings_expr && strings_expr Sequel.&(strings_expr, non_strings_expr) else strings_expr || non_strings_expr end if expr generator.constraint(constraint, expr) end when :exact_length generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| {Sequel.char_length(c) => arg}}) when :min_length generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| Sequel.char_length(c) >= arg}) when :max_length generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| Sequel.char_length(c) <= arg}) when *REVERSE_OPERATOR_MAP.keys generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| Sequel.identifier(c).public_send(REVERSE_OPERATOR_MAP[validation_type], arg)}) when :length_range op = arg.exclude_end? ? :< : :<= 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)}) arg = "#{arg.begin}..#{'.' if arg.exclude_end?}#{arg.end}" when :format generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| {c => arg}}) if arg.casefold? validation_type = :iformat end arg = arg.source when :includes generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| {c => arg}}) if arg.is_a?(Range) if arg.begin.is_a?(Integer) && arg.end.is_a?(Integer) validation_type = :includes_int_range arg = "#{arg.begin}..#{'.' if arg.exclude_end?}#{arg.end}" else raise Error, "validates includes with a range only supports integers currently, cannot handle: #{arg.inspect}" end elsif arg.is_a?(Array) if arg.all?{|x| x.is_a?(Integer)} validation_type = :includes_int_array elsif arg.all?{|x| x.is_a?(String)} validation_type = :includes_str_array else raise Error, "validates includes with an array only supports strings and integers currently, cannot handle: #{arg.inspect}" end arg = arg.join(',') else raise Error, "validates includes only supports arrays and ranges currently, cannot handle: #{arg.inspect}" end when :like, :ilike generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| Sequel.public_send(validation_type, c, arg)}) when :unique generator.unique(columns, :name=>constraint) columns = [columns.join(',')] when :drop if generator.is_a?(Sequel::Schema::AlterTableGenerator) unless constraint raise Error, 'cannot drop a constraint validation without a constraint name' end generator.drop_constraint(constraint) drop_rows << [constraint_validations_literal_table(table), constraint.to_s] columns = [] else raise Error, 'cannot drop a constraint validation in a create_table generator' end else raise Error, "invalid or missing validation type: #{val.inspect}" end columns.map do |column| {: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} end end ds = from(constraint_validations_table) unless drop_rows.empty? ds.where([:table, :constraint_name]=>drop_rows).delete end ds.multi_insert(rows.flatten) end