Let's try to port some ActiveRecord validations to Postgres using constraints!

class User < ActiveRecord::Base validates_presence_of :email end ALTER TABLE users ALTER COLUMN email SET NOT NULL; ALTER TABLE users ADD CONSTRAINT email_presence CHECK (char_length(email) > 0);

class User < ActiveRecord::Base validates_uniqueness_of :email end ALTER TABLE users ADD CONSTRAINT email_uniqueness UNIQUE (email);

class User < ActiveRecord::Base validates_uniqueness_of :email, case_sensitive: false end ALTER TABLE users ALTER COLUMN email TYPE citext; ALTER TABLE users ADD CONSTRAINT email_uniqueness UNIQUE (email);

class User < ActiveRecord::Base validates_uniqueness_of :email, scope: :account_id end ALTER TABLE users ADD CONSTRAINT email_uniqueness UNIQUE (email, account_id);

class User < ActiveRecord::Base validates_numericality_of :age, greater_than_or_equal_to: 18 end ALTER TABLE users ADD CONSTRAINT age_numericality check (age >= 18);

class User < ActiveRecord::Base validates_numericality_of :age, equal_to: 50 end ALTER TABLE users ADD CONSTRAINT age_numericality check (age = 50);

class User < ActiveRecord::Base validates_numericality_of :age, odd: true end ALTER TABLE users ADD CONSTRAINT age_numericality check (age % 2 != 0);

class User < ActiveRecord::Base validates_numericality_of :age, even: true end ALTER TABLE users ADD CONSTRAINT age_numericality check (age % 2 = 0);

class User < ActiveRecord::Base validates_inclusion_of :age, in: [1, 2, 3] end ALTER TABLE users ADD CONSTRAINT age_inclusion check (age IN (1, 2, 3));

class User < ActiveRecord::Base validates_inclusion_of :age, in: 18..25 end ALTER TABLE users ADD CONSTRAINT age_inclusion check (age IN generate_sequence(18, 25));

class User < ActiveRecord::Base validates_inclusion_of :age, in: (1..100).step(2) end ALTER TABLE users ADD CONSTRAINT age_inclusion check (age IN generate_sequence(1, 100, 2));

class User < ActiveRecord::Base validates_exclusion_of :age, in: [1, 2, 3] end ALTER TABLE users ADD CONSTRAINT age_exclusion check (age NOT IN (1, 2, 3));

class User < ActiveRecord::Base validates_exclusion_of :age, in: 18..25 end ALTER TABLE users ADD CONSTRAINT age_exclusion check (age NOT IN generate_sequence(18, 25));

class User < ActiveRecord::Base validates_exclusion_of :age, in: (1..100).step(2) end ALTER TABLE users ADD CONSTRAINT age_exclusion check (age NOT IN generate_sequence(1, 100, 2));

class User < ActiveRecord::Base validates_length_of :password, minimum: 6, maximum: 32 end ALTER TABLE users ADD CONSTRAINT password_length CHECK (char_length(password) BETWEEN 6 AND 32);

class User < ActiveRecord::Base validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i end ALTER TABLE users ADD CONSTRAINT email_format CHECK (email ~* '\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z');

class User < ActiveRecord::Base validates_format_of :email, with: /.+@.+/, allow_nil: true end ALTER TABLE users ADD CONSTRAINT email_format CHECK (email IS NULL OR email ~* '.+@.+');

class User < ActiveRecord::Base validates_format_of :email, with: /.+@.+/, allow_blank: true end ALTER TABLE users ADD CONSTRAINT email_format CHECK (email = '' OR email ~* '.+@.+');

class User < ActiveRecord::Base validates_format_of :email, with: /.+@.+/, allow_blank: true, allow_nil: true end ALTER TABLE users ADD CONSTRAINT email_format CHECK (email IN (NULL, '') OR email ~* '.+@.+');

custom validations

class User < ActiveRecord::Base validate :validate_email_domain def domain email.to_s.split('@').last end private def validate_email_domain if banned_domains.include?(domain) errors.add(:email, 'domain is invalid') end end def banned_domains %w(gmail.com live.com) end end CREATE OR REPLACE FUNCTION validate_email_domain() RETURNS trigger AS $$ DECLARE banned text[]; domain text; BEGIN banned := ARRAY['gmail.com', 'live.com']; FOREACH domain IN ARRAY banned LOOP IF NEW.email ~* (domain || '$') THEN RAISE EXCEPTION 'Invalid email domain %', domain; END IF; END LOOP; RETURN NEW; END; $$ language plpgsql; CREATE TRIGGER email_domain_validation BEFORE INSERT OR UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE validate_email_domain();

Caveats

You'll need convert these constraint exceptions into user friendly error messages within your applications. The exceptions contain the name of the failed constraint like email_domain_validation so a common way to accomplish this is to map the name to a hash containing messages like Your email domain #{domain.inspect} has been banned .

Postgres returns the first raised exception that is encountered. This means that you can't get a list of ALL failing validations when calling save , only one at a time as each error is fixed.

If you enjoyed this, check out how to port ActiveRecord counter cache and soft delete behavior to Postgres as well!