Need help with hair_trigger?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

572 Stars 76 Forks MIT License 168 Commits 19 Opened issues


Happy database triggers for ActiveRecord

Services available


Need anything else?

Contributors list


HairTrigger lets you create and manage database triggers in a concise, db-agnostic, Rails-y way. You declare triggers right in your models in Ruby, and a simple rake task does all the dirty work for you.


HairTrigger works with Rails 5.0 onwards. Add the following line to your Gemfile:

gem 'hairtrigger'

Then run

bundle install

For older versions of Rails check the last 0.2 release



Declare triggers in your models and use a rake task to auto-generate the appropriate migration. For example:

class AccountUser < ActiveRecord::Base
  trigger.after(:insert) do
    "UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;"

trigger.after(:update).of(:name) do "INSERT INTO user_changes(id, name) VALUES(,;" end end

and then:

rake db:generate_trigger_migration

This will create a db-agnostic migration for the trigger that mirrors the model declaration. The end result in MySQL will be something like this:

CREATE TRIGGER account_users_after_insert_row_tr AFTER INSERT ON account_users
    UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;

CREATE TRIGGER account_users_after_update_on_name_row_tr AFTER UPDATE ON account_users FOR EACH ROW BEGIN IF <> OR ( IS NULL) <> ( IS NULL) THEN INSERT INTO user_changes(id, name) VALUES(,; END IF; END;

Note that these auto-generated

statements in the migration contain the
:generated => true
option, indicating that they were created from the model definition. This is important, as the rake task will also generate appropriate drop/create statements for any model triggers that get removed or updated. It does this by diffing the current model trigger declarations and any auto-generated triggers in schema.rb (and subsequent migrations).

Chainable Methods

Triggers are built by chaining several methods together, ending in a block that specifies the SQL to be run when the trigger fires. Supported methods include:


Optional, inferred from other calls.


Ignored in models, required in migrations.


Defaults to

, PostgreSQL allows


Shorthand for



Shorthand for



Optional, SQL snippet limiting when the trigger will fire. Supports delayed interpolation of variables.


Only fire the update trigger if at least one of the columns is specified in the statement. Platforms that support it use a native

clause, others will have an inferred
IF ...
statement in the trigger body. Note the former will fire even if the column's value hasn't changed; the latter will not.


Permissions/role to check when calling trigger. PostgreSQL supports

(default) and
, MySQL supports
(default) and arbitrary users (syntax:


Required (but may be satisified by

). Possible values are


Required (but may be satisified by

). Possible values are
. MySQL/SQLite only support one action per trigger, and don't support

nowrap(flag = true)

PostgreSQL-specific option to prevent the trigger action from being wrapped in a

. This is useful for executing existing triggers/functions directly, but is not compatible with the
setting nor can it be used with pre-9.0 PostgreSQL when supplying a


trigger.after(:update).nowrap { "tsvector_update_trigger(...)" }


PostgreSQL-specific option for declaring variables for use in the trigger function. Declarations should be separated by semicolons, e.g.

trigger.after(:insert).declare("user_type text; status text") do


Noop, useful for trigger groups (see below).

Trigger Groups

Trigger groups allow you to use a slightly more concise notation if you have several triggers that fire on a given model. This is also important for MySQL, since it does not support multiple triggers on a table for the same action and timing. For example:

trigger.after(:update) do |t|
  t.all do # every row
    # some sql
  t.of("foo") do
    # some more sql
  t.where(" != AND != 'lol'") do
    # some other sql

For MySQL, this will just create a single trigger with conditional logic (since it doesn't support multiple triggers). PostgreSQL and SQLite will have distinct triggers. This same notation is also used within trigger migrations. MySQL does not currently support nested trigger groups.

Because of these differences in how the triggers are created, take care when setting the

for triggers or groups. In other words, PostgreSQL/SQLite will use the
s specified on the individual triggers; MySQL will use the
specified on the group.

Database-specific trigger bodies

Although HairTrigger aims to be totally db-agnostic, at times you do need a little more control over the body of the trigger. You can tailor it for specific databases by returning a hash rather than a string. Make sure to set a

value if you aren't explicitly specifying all of them.

For example, MySQL generally performs poorly with subselects in

statements, and it has its own proprietary syntax for multi-table
s. So you might do something like the following:
trigger.after(:insert) do
  {:default => < <

Manual Migrations

You can also manage triggers manually in your migrations via

. They are a little more verbose than model triggers, and they can be more work since you need to figure out the up/down create/drop logic when you change things. A sample trigger:
create_trigger(:compatibility => 1).on(:users).after(:insert) do
  "UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;"


may drop an existing trigger of the same name, you need to actually implement
methods in your migration (rather than
) so that it does the right thing when rolling back.

Manual triggers and :compatibility

As bugs are fixed and features are implemented in HairTrigger, it's possible that the generated trigger SQL will change (this has only happened once so far). If you upgrade to a newer version of HairTrigger, it needs a way of knowing which previous version generated the original trigger. You only need to worry about this for manual trigger migrations, as the model ones automatically take care of this. For your manual triggers you can either:

  • pass
    :compatibility => x
    to your
    statement, where x is whatever HairTrigger::Builder.compatibility is (1 for this version).
  • set
    HairTrigger::Builder.base_compatibility = x
    in an initializer, where x is whatever HairTrigger::Builder.compatibility is. This is like doing the first option on every
    . Note that once the compatibility changes, you'll need to set
    on new triggers (unless you just redo all your triggers and bump the

If you upgrade to a newer version of HairTrigger and see that the SQL compatibility has changed, you'll need to set the appropriate compatibility on any new triggers that you create.

rake db:schema:dump

HairTrigger hooks into

rake db:schema:dump
(and rake tasks that call it) to make it trigger-aware. A newly generated schema.rb will contain:
  • create_trigger
    statements for any database triggers that exactly match a
    statement in an applied migration or in the previous schema.rb file. this includes both generated and manual
  • adapter-specific
    execute('CREATE TRIGGER..')
    statements for any unmatched database triggers.

As long as you don't delete old migrations and schema.rb prior to running

rake db:schema:dump
, the result should be what you expect (and portable). If you have deleted all trigger migrations, you can regenerate a new baseline for model triggers via
rake db:generate_trigger_migration


To stay on top of things, it's strongly recommended that you add a test or spec to ensure your migrations/schema.rb match your models. This is as simple as:

assert HairTrigger::migrations_current?

This way you'll know if there are any outstanding migrations you need to create.

Warnings and Errors

There are a couple classes of errors: declaration errors and generation errors/warnings.

Declaration errors happen if your trigger declaration is obviously wrong, and will cause a runtime error in your model or migration class. An example would be

, since
is not a valid event.

Generation errors happen if you try something that your adapter doesn't support. An example would be something like
for MySQL. These errors only happen when the trigger is actually generated, e.g. when you attempt to run the migration.

Generation warnings are similar but they don't stop the trigger from being generated. If you do something adapter-specific supported by your database, you will still get a warning (to $stderr) that your trigger is not portable. You can silence warnings via

HairTrigger::Builder.show_warnings = false

You can validate your triggers beforehand using the

method. It will throw the appropriate errors/warnings so that you know what to fix, e.g.

HairTrigger does not validate your SQL, so be sure to test it in all databases you want to support.


When running a trigger migration, you might notice some PostgreSQL NOTICEs like so:

NOTICE:  trigger "foo_bar_baz" for table "quux" does not exist, skipping
NOTICE:  function foo_bar_baz() does not exist, skipping

This happens because HairTrigger will attempt to drop the existing trigger/function if it already exists. These notices are safe to ignore. Note that this behavior may change in a future release, meaning you'll first need to explicitly drop the existing trigger if you wish to redefine it.


  • As is the case with
    or any direct SQL you do, be careful to reload updated objects from the database. For example, the following code will display the wrong count since we aren't reloading the account:
  a = Account.find(123)
  a.account_users.create(:name => 'bob')
  puts "count is now #{a.user_count}"
  • For repeated chained calls, the last one wins, there is currently no merging.
  • If you want your code to be portable, the trigger actions should be limited to
    , and conditional logic should be handled through the
    option/method. Otherwise you'll likely run into trouble due to differences in syntax and supported features.
  • Manual
    statements have some gotchas. See the section "Manual triggers and :compatibility"


Contributions welcome! I don't write much Ruby these days 😢 (and haven't used this gem in years 😬) but am happy to take contributions. If I'm slow to respond, don't hesitate to @ me repeatedly, sometimes those github notifications slip through the cracks. 😆.

If you want to add a feature/bugfix, you can rely on Github Actions to run the tests, but do also run them locally (especially if you are changing supported railses/etc). HairTrigger uses appraisal to manage all that w/ automagical gemfiles. So the tl;dr when testing locally is:

  1. make sure you have mysql and postgres installed (homebrew or whatever)
  2. bundle exec appraisal install
    -- get all the dependencies
  3. bundle exec appraisal rake
    -- run the specs every which way


  • Ruby 2.3.0+
  • Rails 5.0+
  • PostgreSQL 8.0+
  • MySQL 5.0.10+
  • SQLite 3.3.8+



Copyright (c) 2011-2021 Jon Jensen. See LICENSE.txt for further details.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.