module Sequel::ConstraintValidations

Constants

DEFAULT_CONSTRAINT_VALIDATIONS_TABLE

The default table name used for the validation metadata.

OPERATORS
REVERSE_OPERATOR_MAP

Attributes

constraint_validations_table[RW]

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

extended(db) click to toggle source

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

alter_table_generator(&block) click to toggle source

Modify the default alter_table generator to include the constraint validation methods.

Calls superclass method
# 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_constraint_validations_table() click to toggle source

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
create_table_generator(&block) click to toggle source

Modify the default create_table generator to include the constraint validation methods.

Calls superclass method
# 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
drop_constraint_validations_for(opts=OPTS) click to toggle source

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_constraint_validations_table() click to toggle source

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_table(*names) click to toggle source

Drop all constraint validations for a table if dropping the table.

Calls superclass method
# 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

apply_alter_table_generator(name, generator) click to toggle source

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.

Calls superclass method
# 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
blank_string_value() click to toggle source

The value of a blank string. An empty string by default, but nil on Oracle as Oracle treats the empty string as NULL.

# File lib/sequel/extensions/constraint_validations.rb, line 348
def blank_string_value
  if database_type == :oracle
    nil
  else
    ''
  end
end
constraint_validation_expression(cols, allow_nil) { |c| ... } click to toggle source
# 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
constraint_validations_literal_table(table) click to toggle source

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
create_table_from_generator(name, generator, options) click to toggle source

Before creating the table, add constraints for all of the generators validations to the generator.

Calls superclass method
# 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
generator_string_column?(generator, table, c) click to toggle source

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
process_generator_validations(table, generator, validations) click to toggle source

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