GithubHelp home page GithubHelp logo

hair_trigger's Introduction

HairTrigger

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.

Installation

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

Usage

Models

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;"
  end

  trigger.after(:update).of(:name) do
    "INSERT INTO user_changes(id, name) VALUES(NEW.id, NEW.name);"
  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
FOR EACH ROW
BEGIN
    UPDATE accounts SET user_count = user_count + 1 WHERE id = NEW.account_id;
END;

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

Note that these auto-generated create_trigger 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:

name(trigger_name)

Optional, inferred from other calls.

on(table_name)

Ignored in models, required in migrations.

for_each(item)

Defaults to :row, PostgreSQL allows :statement.

before(*events)

Shorthand for timing(:before).events(*events).

after(*events)

Shorthand for timing(:after).events(*events).

where(conditions)

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

of(*columns)

Only fire the update trigger if at least one of the columns is specified in the statement. Platforms that support it use a native OF 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.

security(user)

Permissions/role to check when calling trigger. PostgreSQL supports :invoker (default) and :definer, MySQL supports :definer (default) and arbitrary users (syntax: 'user'@'host').

timing(timing)

Required (but may be satisfied by before/after). Possible values are :before/:after.

events(*events)

Required (but may be satisfied by before/after). Possible values are :insert/:update/:delete/:truncate. MySQL/SQLite only support one action per trigger, and don't support :truncate.

nowrap(flag = true)

PostgreSQL-specific option to prevent the trigger action from being wrapped in a CREATE FUNCTION. This is useful for executing existing triggers/functions directly, but is not compatible with the security setting nor can it be used with pre-9.0 PostgreSQL when supplying a where condition.

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

declare

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
  <<-SQL
    IF (NEW.account_id = 1 OR NEW.email LIKE '%company.com') THEN
      user_type := 'employee';
    ELSIF ...
  SQL
end

all

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
  end
  t.of("foo") do
    # some more sql
  end
  t.where("OLD.bar != NEW.bar AND NEW.bar != 'lol'") do
    # some other sql
  end
end

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 name for triggers or groups. In other words, PostgreSQL/SQLite will use the names specified on the individual triggers; MySQL will use the name 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 :default value if you aren't explicitly specifying all of them.

For example, MySQL generally performs poorly with subselects in UPDATE statements, and it has its own proprietary syntax for multi-table UPDATEs. So you might do something like the following:

trigger.after(:insert) do
  {:default => <<-DEFAULT_SQL, :mysql => <<-MYSQL}

  UPDATE users SET item_count = item_count + 1
  WHERE id IN (SELECT user_id FROM buckets WHERE id = NEW.bucket_id)
  DEFAULT_SQL

  UPDATE users, buckets SET item_count = item_count + 1
  WHERE users.id = user_id AND buckets.id = NEW.bucket_id
  MYSQL
end

Manual Migrations

You can also manage triggers manually in your migrations via create_trigger and drop_trigger. 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;"
end

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

The drop_trigger currently only supports the drop_trigger(name, table, options = {}) format. You will need to determine what the resulting trigger name is (e.g. SHOW TRIGGERS query) and use that name in the drop_triggers call. Your down migration method might contain something like:

drop_trigger(:users_after_insert_row_tr, :transactions)

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 create_trigger 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 create_trigger. Note that once the compatibility changes, you'll need to set :compatibility on new triggers (unless you just redo all your triggers and bump the base_compatibility).

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 create_trigger statement in an applied migration or in the previous schema.rb file. this includes both generated and manual create_trigger calls.
  • 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.

Filtering

It is possible to filter which triggers are dumped by setting any of these configuration values:

HairTrigger::SchemaDumper::Configuration.ignore_triggers = 'exact_trigger_name'
HairTrigger::SchemaDumper::Configuration.ignore_tables = [/partial_/, 'exact_table_name']
HairTrigger::SchemaDumper::Configuration.allow_triggers = [/partial_/, 'exact_trigger_name']
HairTrigger::SchemaDumper::Configuration.allow_tables = 'exact_table_name'

Each option can accept a single String or Regexp, or a mixed array of both.

Testing

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 trigger.after(:never), since :never is not a valid event.

Generation errors happen if you try something that your adapter doesn't support. An example would be something like trigger.security(:invoker) 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 Builder#validate! method. It will throw the appropriate errors/warnings so that you know what to fix, e.g.

User.triggers.each(&:validate!)

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

PostgreSQL NOTICEs

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.

Gotchas

  • As is the case with ActiveRecord::Base.update_all 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 INSERT/UPDATE/DELETE/SELECT, and conditional logic should be handled through the :where option/method. Otherwise you'll likely run into trouble due to differences in syntax and supported features.

  • Manual create_trigger statements have some gotchas. See the section "Manual triggers and :compatibility"

Contributing

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

Compatibility

  • Ruby 3.0+
  • Rails 6.1+
  • PostgreSQL 8.0+
  • MySQL 5.0.10+
  • SQLite 3.3.8+

Copyright

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

hair_trigger's People

Contributors

aovertus avatar barthez avatar ccutrer avatar codekitchen avatar davqasd avatar dgynn avatar dhnaranjo avatar ebihara99999 avatar erikaxel avatar iagopiimenta avatar jenseng avatar luit avatar maestromac avatar miltzi avatar mothra avatar njakobsen avatar ojab avatar olleolleolle avatar ollym avatar petergoldstein avatar pezra avatar saicheg avatar sandeep-patle1508 avatar shawnpyle avatar wonda-tea-coffee avatar zealot128 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hair_trigger's Issues

`drop_trigger` is not working

We're using the drop_trigger as the following:

drop_trigger("trigger_name", "table_name")

But it does not execute the DROP TRIGGER SQL command at all.

Invalid PG version check in builder

Running current master branch with PG version 9.6.3, I get the following error during db:migrate:

HairTrigger::Builder::GenerationError: of can only be used in conjunction with nowrap on postgres 9.1 and greater
hair_trigger-52e88e4f623d/lib/hair_trigger/builder.rb:430:in `generate_trigger_postgresql'
hair_trigger-52e88e4f623d/lib/hair_trigger/builder.rb:230:in `generate'
hair_trigger-52e88e4f623d/lib/hair_trigger/builder.rb:347:in `maybe_execute'
hair_trigger-52e88e4f623d/lib/hair_trigger/builder.rb:155:in `set_nowrap'
hair_trigger-52e88e4f623d/lib/hair_trigger/builder.rb:145:in `nowrap'
20170807113627_create_triggers_events_insert_or_events_update.rb:20:in `up'
bin/rails:6:in `require'

db_version < 91000 should be db_version < 90100

Problems with rails 5.1

I updated an app to rails 5.1 (ruby 2.3.4) using postgres 9.6.1 on OSX Sierra
Afterwards all db related tasks result in this error
Without the hairtrigger gem the db interactions work

Bundler::GemRequireError: There was an error while trying to load the gem 'hairtrigger'.
Gem Load Error is: No connection pool with 'primary' found.
Backtrace for gem load error is:
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/connection_adapters/abstract/connection_pool.rb:930:in `retrieve_connection'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/connection_handling.rb:116:in `retrieve_connection'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/connection_handling.rb:88:in `connection'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/migration.rb:832:in `connection'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/migration.rb:839:in `block in method_missing'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/migration.rb:818:in `block in say_with_time'
/Users/const/.rvm/rubies/ruby-2.3.4/lib/ruby/2.3.0/benchmark.rb:293:in `measure'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/migration.rb:818:in `say_with_time'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/migration.rb:838:in `method_missing'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/migration.rb:600:in `method_missing'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/hairtrigger-0.2.19/lib/hair_trigger/migrator.rb:20:in `block in included'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/hairtrigger-0.2.19/lib/hair_trigger/migrator.rb:19:in `instance_eval'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/hairtrigger-0.2.19/lib/hair_trigger/migrator.rb:19:in `included'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/hairtrigger-0.2.19/lib/hair_trigger.rb:224:in `include'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/hairtrigger-0.2.19/lib/hair_trigger.rb:224:in `<top (required)>'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activesupport-5.1.1/lib/active_support/dependencies.rb:292:in `require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activesupport-5.1.1/lib/active_support/dependencies.rb:292:in `block in require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activesupport-5.1.1/lib/active_support/dependencies.rb:258:in `load_dependency'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activesupport-5.1.1/lib/active_support/dependencies.rb:292:in `require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/hairtrigger-0.2.19/lib/hairtrigger.rb:1:in `<top (required)>'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:91:in `require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:91:in `block (2 levels) in require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:86:in `each'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:86:in `block in require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:75:in `each'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:75:in `require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler.rb:107:in `require'
/Users/const/develop/defectradar/defectradarserver/config/application.rb:7:in `<top (required)>'
/Users/const/develop/defectradar/defectradarserver/Rakefile:5:in `require'
/Users/const/develop/defectradar/defectradarserver/Rakefile:5:in `<top (required)>'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/rake-12.0.0/lib/rake/rake_module.rb:28:in `load'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/rake-12.0.0/lib/rake/rake_module.rb:28:in `load_rakefile'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/rake-12.0.0/lib/rake/application.rb:687:in `raw_load_rakefile'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/rake-12.0.0/lib/rake/application.rb:96:in `block in load_rakefile'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/rake-12.0.0/lib/rake/application.rb:178:in `standard_exception_handling'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/rake-12.0.0/lib/rake/application.rb:95:in `load_rakefile'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/rake-12.0.0/lib/rake/application.rb:79:in `block in run'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/rake-12.0.0/lib/rake/application.rb:178:in `standard_exception_handling'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/rake-12.0.0/lib/rake/application.rb:77:in `run'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/rake-12.0.0/exe/rake:27:in `<top (required)>'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/bin/rake:22:in `load'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/bin/rake:22:in `<main>'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/bin/ruby_executable_hooks:15:in `eval'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/bin/ruby_executable_hooks:15:in `<main>'
Bundler Error Backtrace:
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:94:in `rescue in block (2 levels) in require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:90:in `block (2 levels) in require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:86:in `each'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:86:in `block in require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:75:in `each'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:75:in `require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler.rb:107:in `require'
/Users/const/develop/defectradar/defectradarserver/config/application.rb:7:in `<top (required)>'
/Users/const/develop/defectradar/defectradarserver/Rakefile:5:in `require'
/Users/const/develop/defectradar/defectradarserver/Rakefile:5:in `<top (required)>'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/rake-12.0.0/exe/rake:27:in `<top (required)>'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/bin/ruby_executable_hooks:15:in `eval'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/bin/ruby_executable_hooks:15:in `<main>'
ActiveRecord::ConnectionNotEstablished: No connection pool with 'primary' found.
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/connection_adapters/abstract/connection_pool.rb:930:in `retrieve_connection'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/connection_handling.rb:116:in `retrieve_connection'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/connection_handling.rb:88:in `connection'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/migration.rb:832:in `connection'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/migration.rb:839:in `block in method_missing'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/migration.rb:818:in `block in say_with_time'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/migration.rb:818:in `say_with_time'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/migration.rb:838:in `method_missing'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activerecord-5.1.1/lib/active_record/migration.rb:600:in `method_missing'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/hairtrigger-0.2.19/lib/hair_trigger/migrator.rb:20:in `block in included'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/hairtrigger-0.2.19/lib/hair_trigger/migrator.rb:19:in `instance_eval'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/hairtrigger-0.2.19/lib/hair_trigger/migrator.rb:19:in `included'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/hairtrigger-0.2.19/lib/hair_trigger.rb:224:in `include'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/hairtrigger-0.2.19/lib/hair_trigger.rb:224:in `<top (required)>'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activesupport-5.1.1/lib/active_support/dependencies.rb:292:in `require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activesupport-5.1.1/lib/active_support/dependencies.rb:292:in `block in require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activesupport-5.1.1/lib/active_support/dependencies.rb:258:in `load_dependency'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/activesupport-5.1.1/lib/active_support/dependencies.rb:292:in `require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/hairtrigger-0.2.19/lib/hairtrigger.rb:1:in `<top (required)>'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:91:in `require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:91:in `block (2 levels) in require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:86:in `each'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:86:in `block in require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:75:in `each'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler/runtime.rb:75:in `require'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/bundler-1.14.6/lib/bundler.rb:107:in `require'
/Users/const/develop/defectradar/defectradarserver/config/application.rb:7:in `<top (required)>'
/Users/const/develop/defectradar/defectradarserver/Rakefile:5:in `require'
/Users/const/develop/defectradar/defectradarserver/Rakefile:5:in `<top (required)>'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/gems/rake-12.0.0/exe/rake:27:in `<top (required)>'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/bin/ruby_executable_hooks:15:in `eval'
/Users/const/.rvm/gems/ruby-2.3.4@defectradar/bin/ruby_executable_hooks:15:in `<main>'
(See full trace by running task with --trace)

HairTrigger::migrations_current? returning a wrong result

  1. Make one of your models to have a simple trigger
  2. Generate the migration and run it

HairTrigger::migrations_current? will return false despite you have done step 2.

I can see the migration name from prepared_actions is not present in the trigger definitions. Reason of this is that the method in charge of its initialization, #prepared_name, is never called.

Current tests pass because #generate_migration will iterate through trigger definitions while calling the name-initialization method.

Incorrect trigger error when using of with multiple events

I'm trying to port a trigger to hair_trigger. Unfortunately, it complains throwing the error:

/usr/local/bundle/gems/hairtrigger-0.2.20/lib/hair_trigger/builder.rb:330:in `maybe_execute': of may only be specified on update triggers (HairTrigger::Builder::DeclarationError)

Here is the trigger in raw SQL:

CREATE FUNCTION toggle_current_role() RETURNS trigger as $toggle_current_role$
  BEGIN
    -- If row is current, do nothing.
    IF (TG_OP = 'UPDATE' AND OLD.current = true) THEN
      RETURN NEW;
    END IF;

    -- Ensure user roles are not marked as current
    UPDATE roles SET current = null WHERE current = true AND user_id = NEW.user_id;

    RETURN NEW;
  END;
$toggle_current_role$ LANGUAGE plpgsql;

CREATE TRIGGER toggle_current_role BEFORE INSERT OR UPDATE OF current ON roles
  FOR EACH ROW WHEN (NEW.current = true) EXECUTE PROCEDURE toggle_current_role();

In my model, I've written the trigger as follows:

trigger.before(:insert, :update).of(:current).where('NEW.current = true') do
  <<-SQL
    -- If row is current, do nothing.
    IF (TG_OP = 'UPDATE' AND OLD.current = true) THEN
      RETURN NEW;
    END IF;

    -- Ensure user roles are not marked as current
    UPDATE roles SET current = null WHERE current = true AND user_id = NEW.user_id;

    RETURN NEW;
  SQL
end

This trigger is composed of two separate events. One INSERT and one UPDATE OF. hair_trigger does not distinguish this and complains since there are multiple events present.

undefined method `abstract_class?' for Object:Class

Rail 4.2.0, Ruby 2.2.0, Postgres 9.4.1
receiving the error message
undefined method `abstract_class?' for Object:Class
when i
rake db:generate_trigger_migration

any help appreciated.

 class Production < ActiveRecord::Base
   trigger.before(:update) do
     "INSERT INTO production_histories(production_id) VALUES(id);"
  end   
end

undefined method `abstract_class?' for Object:Class

receiving the preceding message when i rake db:generate_trigger_migration. any help appreciated.

 class Production < ActiveRecord::Base
   trigger.before(:update) do
     "INSERT INTO production_histories(production_id) VALUES(id);"
  end   
end

variable declarations support for postgres

in mysql you can declare variables in your trigger body. in postgres you need to do it in the declaration section (before the begin/end). so currently you can't use any non-predefined variables in postgres triggers managed by hairtrigger

there should be a chainable method (declare perhaps?) that lets you specify any such declarations

Rails 5.0.1: "alias_method_chain is deprecated"

I see the following warning when starting my Rails 5.0.1 app:

DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super. (called from <top (required)> at .../config/application.rb:13)
.../hairtrigger-0.2.19/lib/hair_trigger/migrator.rb:20:in `block in included'
  .../hairtrigger-0.2.19/lib/hair_trigger/migrator.rb:19:in `instance_eval'
  .../hairtrigger-0.2.19/lib/hair_trigger/migrator.rb:19:in `included'
  .../hairtrigger-0.2.19/lib/hair_trigger.rb:224:in `include'
  .../hairtrigger-0.2.19/lib/hair_trigger.rb:224:in `<top (required)>'

MySQL trigger syntax schema

Everything was working great until..

rake db:reset

rake aborted!
Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '% TRIGGER

Was able to hunt the issue down and it turns out this was being generated in the schema.rb file:

  execute(<<-TRIGGERSQL)
CREATE DEFINER = root@% TRIGGER event_users_before_insert_row_tr BEFORE INSERT ON event_users
FOR EACH ROW
BEGIN
    SET NEW.created_at = IFNULL(NEW.created_at, NOW()), NEW.created_at = IF(NEW.created_at = '0000-00-00 00:00:00', NOW(), NEW.created_at), NEW.updated_at = IFNULL(NEW.updated_at, NOW()), NEW.updated_at = IF(NEW.updated_at = '0000-00-00 00:00:00', NOW(), NEW.updated_at);
END
  TRIGGERSQL

The fix is:

  execute(<<-TRIGGERSQL)
CREATE DEFINER = 'root'@'%' TRIGGER event_users_before_insert_row_tr BEFORE INSERT ON event_users
FOR EACH ROW
BEGIN
    SET NEW.created_at = IFNULL(NEW.created_at, NOW()), NEW.created_at = IF(NEW.created_at = '0000-00-00 00:00:00', NOW(), NEW.created_at), NEW.updated_at = IFNULL(NEW.updated_at, NOW()), NEW.updated_at = IF(NEW.updated_at = '0000-00-00 00:00:00', NOW(), NEW.updated_at);
END
  TRIGGERSQL

The only difference is putting quotes around the username and host. I checked the mysql documentation and found this:

http://dev.mysql.com/doc/refman/5.1/en/create-trigger.html

"The DEFINER clause specifies the MySQL account to be used when checking access privileges at trigger activation time. If a user value is given, it should be a MySQL account specified as 'user_name'@'host_name' (the same format used in the GRANT statement), CURRENT_USER, or CURRENT_USER(). The default DEFINER value is the user who executes the CREATE TRIGGER statement. This is the same as specifying DEFINER = CURRENT_USER explicitly."

Declare method doesn't work correctly

Hi, I have problems to use declare syntax.

When I use

class Users < ActiveRecord::Base
    trigger.after(:insert, :delete).
    declare('row RECORD; tbl_name TEXT; tags_cache_sql TEXT;') do
      'SOME SQL'
   end
end

and run rake db:generate_trigger_migration
then in my migration I get trigger.after(:insert, :delete).declare(nil).

I have tried to manually modify the migration and it migrates correctly (I have checked in database) but after the rake db:migrateis finished in my schema.rb I have again trigger.after(:insert, :delete).declare(nil).

I use rails 4.1 and postgresql 9.3.4

Ability to add MySQL DECLARE statements

Hi there - thanks for the great gem.

I'm currently unable to use MySQL DECLARE statements to declare local variables in conjunction with the conditional of from the gem, since they appear after the opening IF THEN that is generated. MySQL requires any declared variables to be immediately after the BEFORE statement.

Is this something you'd consider adding support for? I'm unsure if MySQL's declare is the same as Postgres's. If so, I'd be happy to open a PR with MySQL support. If not, would you be open to adding an option for code to run immediately after the opening BEGIN?

Add docs about expected NOTICE output from database

I spent a few minutes getting scared about messages like these being emitted from a migration with a create_trigger:

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

I ended up working my way down to the fact that hair_trigger will always attempt to drop an existing trigger+function before making it. It would be nice to mention in the docs that these are expected messages to see in normal operation, so that dolts like me know that everything is ๐Ÿ‘Œ. ๐Ÿ˜€

support for `change` migrations

Spawned from #26

Right now you can't do create_trigger in a change migration (only up/down). This is because create_trigger will drop a trigger of the same name if it already exists before creating the new one. This makes reversing the migration problematic, since the correct behavior depends on the state of the schema before that migration. So create_trigger per se contains insufficient information to tell you how to reverse it.

It's probably worth changing that behavior and introducing something like force: true for use in schema.rb (like how rails auto-drops tables before creating them), and defaulting it to false for migrations. That would make the behavior of create_trigger more predictable and would allow it to be reversible (so long as force: false)

One problem to solve is how to make the upgrade process painless, since there are probably migrations in the wild that depend on the current create_trigger auto-drop behavior.

We could probably also support drop_trigger ร  la drop_table (i.e. full definition with a block), otherwise there's insufficient information to reverse it.

How to just execute procedure written in C?

I'm struggling with keeping triggers enabled while testing rails app. I want to mimic simple behavior outlined on https://github.com/arkhipov/temporal_tables with rails. So I decided to give a try to this gem.

According to that repo, I have a model something like

class Employee < ApplicationRecord
  trigger.before(:insert, :update, :delete) do
    "EXECUTE PROCEDURE versioning('sys_period', 'employees_history', true);"
  end
end

that gave a migration

class CreateTriggerEmployeesInsertUpdateDelete < ActiveRecord::Migration
  def up
    create_trigger("employees_before_insert_update_delete_row_tr", :generated => true, :compatibility => 1).
        on("employees").
        before(:insert, :update, :delete) do
      "EXECUTE PROCEDURE versioning('sys_period', 'employees_history', true);"
    end
  end

  def down
    drop_trigger("employees_before_insert_update_delete_row_tr", "employees", :generated => true)
  end
end

But when I try to migrate, I'm getting

  class CreateTriggerEmployeesInsertUpdateDelete < ActiveRecord::Migration[4.2] (called from migrate at C:0)
== 20161005213647 CreateTriggerEmployeesInsertUpdateDelete: migrating =========
-- create_trigger("employees_before_insert_update_delete_row_tr", {:generated=>true, :compatibility=>1})
   -> 0.0050s
WARNING: sqlite and mysql triggers may not be shared by multiple actions
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

PG::SyntaxError: ERROR:  syntax error at or near "("
LINE 4:     EXECUTE PROCEDURE versioning('sys_period', 'employees_hi...
                                        ^
: CREATE FUNCTION "employees_before_insert_update_delete_row_tr"()
RETURNS TRIGGER AS $$
BEGIN
    EXECUTE PROCEDURE versioning('sys_period', 'employees_history', true);
    RETURN OLD;
END;
$$ LANGUAGE plpgsql;

It tries to CREATE FUNCTION wheres I already have an existing C function from that extension. I just want to EXEC PROCEDURE for each row.... and make sure trigger is enabled while testing rails.

Rails 3 compatibility?

Bundler could not find compatible versions for gem "activerecord":
In Gemfile:
hairtrigger depends on
activerecord (< 3.0, >= 2.3.0)

rails (= 3.0.5) depends on
  activerecord (3.0.5)

Support ruby 2.1

Error reading triggers in db/migrate/20140211102642_create_trigger_foos_update.rb: unrecognized RUBY_VERSION 2.1.0
rake aborted!
undefined method `empty?' for nil:NilClass
~/.gem/ruby/2.1.0/gems/hairtrigger-0.2.4/lib/hair_trigger.rb:67:in `block in current_migrations'
~/.gem/ruby/2.1.0/gems/hairtrigger-0.2.4/lib/hair_trigger.rb:64:in `each'
~/.gem/ruby/2.1.0/gems/hairtrigger-0.2.4/lib/hair_trigger.rb:64:in `current_migrations'
~/.gem/ruby/2.1.0/gems/hairtrigger-0.2.4/lib/hair_trigger/schema_dumper.rb:15:in `triggers'
~/.gem/ruby/2.1.0/gems/hairtrigger-0.2.4/lib/hair_trigger/schema_dumper.rb:4:in `trailer_with_triggers'
~/.gem/ruby/2.1.0/gems/activerecord-4.1.0.beta1/lib/active_record/schema_dumper.rb:39:in `dump'

ruby_parser doesn't support ruby 2.1 (yet?).

I did try an ugly hack, seems to work - no errors and schema.rb is updated:

class RubyParser
  def self.for_current_ruby
    Ruby20Parser.new
  end
end

auto drop_trigger needs :generated => true

when you remove triggers from a model and then generate a trigger migration, it doesn't set :generated => true on the drop_trigger statements. additionally, MigrationReader.generate_drop_trigger ignores it. so the end result is that when you generate another migration, it tries to drop those triggers all over again.

Undefined method `migrations` in Rails v5.2

$ bundle exec rails db:schema:dump --trace
** Invoke db:schema:dump (first_time)
** Invoke db:load_config (first_time)
** Invoke environment (first_time)
** Execute environment
** Execute db:load_config
** Invoke environment
** Execute db:schema:dump
rails aborted!
NoMethodError: undefined method `migrations' for ActiveRecord::Migrator:Class
Did you mean?  migrations_paths
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/hairtrigger-0.2.20/lib/hair_trigger.rb:45:in `migrator'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/hairtrigger-0.2.20/lib/hair_trigger.rb:65:in `current_migrations'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/hairtrigger-0.2.20/lib/hair_trigger/schema_dumper.rb:23:in `triggers'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/hairtrigger-0.2.20/lib/hair_trigger/schema_dumper.rb:7:in `trailer'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.0/lib/active_record/schema_dumper.rb:39:in `dump'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.0/lib/active_record/schema_dumper.rb:22:in `dump'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.0/lib/active_record/railties/databases.rake:251:in `block (4 levels) in <main>'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.0/lib/active_record/railties/databases.rake:250:in `open'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activerecord-5.2.0/lib/active_record/railties/databases.rake:250:in `block (3 levels) in <main>'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/task.rb:271:in `block in execute'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/task.rb:271:in `each'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/task.rb:271:in `execute'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bugsnag-6.7.3/lib/bugsnag/integrations/rake.rb:18:in `execute_with_bugsnag'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/task.rb:213:in `block in invoke_with_call_chain'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/2.5.0/monitor.rb:226:in `mon_synchronize'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/task.rb:193:in `invoke_with_call_chain'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/task.rb:182:in `invoke'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/application.rb:160:in `invoke_task'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/application.rb:116:in `block (2 levels) in top_level'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/application.rb:116:in `each'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/application.rb:116:in `block in top_level'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/application.rb:125:in `run_with_threads'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/application.rb:110:in `top_level'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.0/lib/rails/commands/rake/rake_command.rb:23:in `block in perform'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/rake-12.3.1/lib/rake/application.rb:186:in `standard_exception_handling'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.0/lib/rails/commands/rake/rake_command.rb:20:in `perform'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.0/lib/rails/command.rb:48:in `invoke'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/railties-5.2.0/lib/rails/commands.rb:18:in `<main>'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bootsnap-1.3.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bootsnap-1.3.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `block in require_with_bootsnap_lfi'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bootsnap-1.3.0/lib/bootsnap/load_path_cache/loaded_features_index.rb:65:in `register'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bootsnap-1.3.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:20:in `require_with_bootsnap_lfi'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/bootsnap-1.3.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:29:in `require'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:283:in `block in require'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:249:in `load_dependency'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/activesupport-5.2.0/lib/active_support/dependencies.rb:283:in `require'
/Users/jstayton/Code/medishare/api/bin/rails:9:in `<top (required)>'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/spring-2.0.2/lib/spring/client/rails.rb:28:in `load'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/spring-2.0.2/lib/spring/client/rails.rb:28:in `call'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/spring-2.0.2/lib/spring/client/command.rb:7:in `call'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/spring-2.0.2/lib/spring/client.rb:30:in `run'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/spring-2.0.2/bin/spring:49:in `<top (required)>'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/spring-2.0.2/lib/spring/binstub.rb:31:in `load'
/Users/jstayton/.rbenv/versions/2.5.1/lib/ruby/gems/2.5.0/gems/spring-2.0.2/lib/spring/binstub.rb:31:in `<top (required)>'
/Users/jstayton/Code/medishare/api/bin/spring:15:in `require'
/Users/jstayton/Code/medishare/api/bin/spring:15:in `<top (required)>'
bin/rails:3:in `load'
bin/rails:3:in `<main>'
Tasks: TOP => db:schema:dump

warnings from hairtigger on postgres

Hello, I'm seeing the following warning in schema.db:

WARNING: generating adapter-specific definition for posts_after_update__trigger due to a mismatch.

either there's a bug in hairtrigger or you've messed up your migrations and/or db :-/

Seems related to dependency versions, because in another installation of rails I see this instead:

no candidate create_trigger statement could be found, creating an adapter-specific one

https://github.com/rubyrailhead/hairtriggertest/blob/master/db/schema.rb#L40

Complete project showing the issue here:
https://github.com/rubyrailhead/hairtriggertest
see especially https://github.com/rubyrailhead/hairtriggertest/blob/master/db/migrate/20150528170434_add_localtimestamp_to_posts.rb

JRuby support?

When installing the gem in JRuby, I get the following:

Could not find gem 'hair_trigger java' in any of the gem sources listed in your Gemfile or available on this machine.

Is JRuby support planned?

Quote table names in generated SQL

We have a table named "user". In Postgres this is a reserved word, so the following SQL generated by hair_trigger fails:

DROP TRIGGER IF EXISTS user_after_update_of_login_id_row_tr ON user;

This could be solved by escaping table names in all generated SQL (in the case of postgres, using double quotes):

DROP TRIGGER IF EXISTS user_after_update_of_login_id_row_tr ON "user";

railtie for rake tasks

installing hairtrigger requires you to create a rake task in your app to make hairtrigger's tasks available. this shouldn't be necessary in rails 3, there should just be a railtie that does this for you.

unable to create trigger with custom name

Hello,

I am unable to use the 'name' option to create a trigger with a custom name. Here is an example of the trigger in my model:

  trigger.after(:update).name('contacts_trigger_custom_name') do |t|
    t.where("OLD.foo != NEW.foo") do
      "Bar"
    end
  end

The auto-generated migration looks like this:

class CreateTriggerContactsUpdate < ActiveRecord::Migration
  def up
    create_trigger("contacts_trigger_custom_name", :generated => true, :compatibility => 1).
        on("contacts").
        after(:update).
        name("contacts_trigger_custom_name") do |t|
      t.where("OLD.foo != NEW.foo") do
        "Bar;"
      end
    end
  end

  def down
    drop_trigger("contacts_trigger_custom_name", "contacts", :generated => true)

    drop_trigger("contacts_after_update_row_when_old_foo_new_foo_tr", "contacts", :generated => true)
  end
end

When I run that migration, the trigger has the name contacts_after_update_row_when_old_foo_new_foo_tr instead of my custom name. I am trying to create several update triggers with similar conditions, and the result is that only one trigger is created with the auto-generated name. So far my workaround has been to add bogus conditions to the triggers so that they are created with different names:

t.where("1 = 1 AND OLD.foo != NEW.foo")

I am using Rails 3.2.13, hairtrigger 0.2.4, pg 0.17.0, and PostgreSQL 9.2.

Thanks.

model won't load if db doesn't exist

if you put a trigger in your model definition and you haven't yet created the db, it blows up. this is because it checks ActiveRecord::Base.connection's adapter type so we know what's supported.

it should handle non-existence of the db more gracefully, and it should also delay checking the connection/adapter as late as possible. for example, if a model trigger is only using trigger features that are supported by all adapters, we should never ever hit the connection in the course of loading the model

schema dumper fails on manual triggers with no options

spawned from #4

if you have a manual trigger migration and haven't specified an options hash to create_trigger, schema dumping fails when it tries to generate the builder from the migration (you get a "You have a nil object when you didn't expect it!" error). a workaround is to specify an options hash (can be empty, ideally it should have the compatibility level, e.g. :compatibility => 1)

Support for deferred constraint triggers.

In PG, you're able to attach a trigger to a table using CREATE CONSTRAINT TRIGGER, and declare it as DEFERRABLE INITIALLY DEFERRED. It would be nice to be able to manage this kind of trigger with HairTrigger if possible.

In addition, it would be nice to be able to attach database comments to functions using COMMENT ON FUNCTION.

Multi Schema support

I'm using hair-trigger with a multi-tenant application that uses Postgres schemas to separate each tenant's information. Doing schema_load on a newly created tenant errors out, because on the schema.rb the CREATE FUNCTION script has the public schema hardcoded, ex:

  # no candidate create_trigger statement could be found, creating an adapter-specific one
  execute(<<-TRIGGERSQL)
CREATE OR REPLACE FUNCTION public.tsv_body_trigger()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
BEGIN
    NEW.tsv_body := to_tsvector('pg_catalog.spanish', coalesce(NEW.content,''));
    RETURN NULL;
END;
$function$
  TRIGGERSQL

This causes that the SQL that creates the trigger errors out because the function is not defined in the schema:

  # no candidate create_trigger statement could be found, creating an adapter-specific one
  execute("CREATE TRIGGER tsv_body_trigger AFTER INSERT OR UPDATE ON \"pg_search_documents\" FOR EACH ROW EXECUTE PROCEDURE tsv_body_trigger()")

trigger generation broken in 1.9.x + sqlite

as of ruby 1.9.x, Array#to_s is equivalent to calling Array#inspect, e.g. ["foo"].to_s => "[\"foo\"]" HairTrigger::Builder#generate_trigger_sqlite interpolates options[:events] (an array) into a string, causing an implicit to_s call. This results in an invalid statement like CREATE TRIGGER foo_tr AFTER ["INSERT"] ON ...

we should instead interpolate options[:events].first (like we do in HairTrigger::Builder#generate_trigger_mysql)

hook into rake db:schema:dump

rake db:schema:dump needs to be made aware of the current model triggers and include the appropriate create_trigger statements.

as for triggers that are defined solely in migrations (e.g. manual create_trigger), we could probably pull those in too. triggers that just live in the db, otoh, can't be pulled into schema.rb in a db-agnostic way. it might be nice to issue a warning or something

as a workaround, you can do the following to get model trigger definitions into schema.rb:

  1. run rake db:schema:dump
  2. nuke your current trigger migrations
  3. run rake db:generate_trigger_migration
  4. copy the create_trigger statements into schema.rb

Support ruby 2.2

I guess this may be caused by the ruby_parser dependency not supporting 2.2?

Error reading triggers in db/migrate/20150415070431_create_trigger_user_update.rb: unrecognized RUBY_VERSION 2.2.0

support for field selection

Hey guys!

As i understand from your docs there is no ability to limit trigger event for some specific field. In my project i have some sort of logic where i need increase some numbers if field if updated. See example below:

increase_progress AFTER UPDATE OF progress ON issues 
FOR EACH ROW
EXECUTE PROCEDURE increase_issues_progress()

with current syntax i am able to do

trigger.name("increase_progress").after(:update) do
  "EXECUTE PROCEDURE increase_issues_progress()"
end

which will produce me

increase_progress AFTER UPDATE ON issues 
FOR EACH ROW
EXECUTE PROCEDURE increase_issues_progress()

which is not what i needed.

Any ideas how i can implement logic i need?

revisit security method for mysql

see instructure/canvas-lms#179

hair_trigger never explicitly sets SQL SECURITY for mysql, so it defaults to DEFINER. you can specify an alternate definer with the security method, but you cannot specify :invoker (as you can for postgres). we should probably support this and make it the default.

this may help for scenarios where people are using binary logging for mysql but do not have log_bin_trust_function_creators enabled. some extra investigation should be done to see what else we can do there.

see also https://forums.aws.amazon.com/thread.jspa?messageID=181915

Triggers at the beginning of schema, before tables created

When I dump a schema for a DB with some triggers on some tables, them triggers are added at the top of the schema.rb file before the table definitions. When trying to load the schema it throws errors for the missing tables.

Any way around this? Basically need to be able to specify triggers should go last in the schema file.

Support for Ruby 2.0.0

After running rake db:migrate on Ruby 2.0.0 + Rails 4.0.0.rc1 triggers are successfully created, but then task generates error:

==  CreateTriggersStudentInsertAndStudentUpdate: migrating ====================
-- create_trigger("student_after_insert_row_tr", {:generated=>true, :compatibility=>1})
   -> 0.0001s
-- create_trigger("student_after_update_row_tr", {:generated=>true, :compatibility=>1})
   -> 0.0001s
==  CreateTriggersStudentInsertAndStudentUpdate: migrated (0.6680s) ===========

DEPRECATION WARNING: instantiate this class with a list of migrations. (called from call at /Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:246)
Error reading triggers in db/migrate/20130613085347_create_triggers_student_insert_and_student_update.rb: unrecognized RUBY_VERSION 2.0.0
rake aborted!
undefined method `empty?' for nil:NilClass
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/gems/hairtrigger-0.2.3/lib/hair_trigger.rb:60:in `block in current_migrations'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/gems/hairtrigger-0.2.3/lib/hair_trigger.rb:57:in `each'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/gems/hairtrigger-0.2.3/lib/hair_trigger.rb:57:in `current_migrations'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/gems/hairtrigger-0.2.3/lib/hair_trigger/schema_dumper.rb:15:in `triggers'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/gems/hairtrigger-0.2.3/lib/hair_trigger/schema_dumper.rb:4:in `trailer_with_triggers'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/gems/activerecord-4.0.0.rc1/lib/active_record/schema_dumper.rb:29:in `dump'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/gems/activerecord-4.0.0.rc1/lib/active_record/schema_dumper.rb:21:in `dump'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/gems/activerecord-4.0.0.rc1/lib/active_record/railties/databases.rake:244:in `block (4 levels) in <top (required)>'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/gems/activerecord-4.0.0.rc1/lib/active_record/railties/databases.rake:243:in `open'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/gems/activerecord-4.0.0.rc1/lib/active_record/railties/databases.rake:243:in `block (3 levels) in <top (required)>'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:246:in `call'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:246:in `block in execute'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:241:in `each'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:241:in `execute'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:184:in `block in invoke_with_call_chain'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:177:in `invoke_with_call_chain'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:170:in `invoke'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/gems/activerecord-4.0.0.rc1/lib/active_record/railties/databases.rake:50:in `block (2 levels) in <top (required)>'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:246:in `call'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:246:in `block in execute'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:241:in `each'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:241:in `execute'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:184:in `block in invoke_with_call_chain'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:177:in `invoke_with_call_chain'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:170:in `invoke'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/gems/activerecord-4.0.0.rc1/lib/active_record/railties/databases.rake:45:in `block (2 levels) in <top (required)>'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:246:in `call'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:246:in `block in execute'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:241:in `each'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:241:in `execute'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:184:in `block in invoke_with_call_chain'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:177:in `invoke_with_call_chain'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/task.rb:170:in `invoke'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/application.rb:143:in `invoke_task'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/application.rb:101:in `block (2 levels) in top_level'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/application.rb:101:in `each'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/application.rb:101:in `block in top_level'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/application.rb:110:in `run_with_threads'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/application.rb:95:in `top_level'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/application.rb:73:in `block in run'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/application.rb:160:in `standard_exception_handling'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0@global/gems/rake-10.0.4/lib/rake/application.rb:70:in `run'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/bin/ruby_noexec_wrapper:14:in `eval'
/Users/storkvist/.rvm/gems/ruby-2.0.0-p0/bin/ruby_noexec_wrapper:14:in `<main>'
Tasks: TOP => db:schema:dump
(See full trace by running task with --trace)

rails 4.1 support

hairtrigger appears to be broken on 4.1. it looks like some stuff changed with ActiveRecord::Migrator so now the proper_table_name_with_hash_awareness override isn't getting added

Multi-line and table triggers

Hi!

I'm looking forward to implementing your gem into my project, but I've got more a doubt rather than an issue:
Is it possible to define multiline triggers? (I mean, like structuring more than one instruction inside de trigger as in the examples)
Furthermore, is it possible to implement cursors into the syntax? It's necessary for me to add some cursors there.

Thanks in advance!

Not supported on ruby 2.3?

Getting these warnings, and no triggers are defined.

Error reading triggers in db/migrate/20151029080635_create_search_index_for_personal_infos.rb: unrecognized RUBY_VERSION 2.3.0
Error reading triggers in db/migrate/20151029080635_create_search_index_for_personal_infos.rb: unrecognized RUBY_VERSION 2.3.0
Included HairTrigger::SchemaDumper in ActiveRecord::SchemaDumper
Error reading triggers in db/migrate/20151029080635_create_search_index_for_personal_infos.rb: unrecognized RUBY_VERSION 2.3.0

hair trigger should drop raw triggers before creating in schema.rb

Spawned from discussion #26 ... in schema.rb, although create_trigger will drop the existing trigger first (if it exists), that is not the case for raw execute "CREATE TRIGGER..." calls that it does. Because of this, if you have any triggers that don't correspond to a create_trigger in a migration, schema.rb cannot be used to update an existing test database (ร  la rake db:test:prepare), only an empty one.

For any adapter-specific execute "CREATE TRIGGER..." calls, HairTrigger should first do a DROP TRIGGER IF EXISTS call

How to execute sequence with prefix

Hi @jenseng

Could you explain how to create a trigger on insert matching prefix + sequence pattern?
Let's say I want to have my tickets table to have a ref column with TICK000 + id

Thanks. This is what I have so far.

trigger.after(:insert) do
  ref = "TICK000"
  "UPDATE tickets SET reference = ref + id WHERE id = NEW.id;"
end

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.