jenseng / hair_trigger

Happy database triggers for ActiveRecord

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

MySQL trigger syntax schema

michaelirey opened this issue · comments

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."

I noticed even when I do:

trigger.security("'root'@'%'").before(:insert)

The quotes are striped out, I we end up with:

  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

looks like it's due to how mysql returns the Definer when you call SHOW TRIGGERS ... even though you have to quote the user and host when creating a trigger, they are never quoted when you query the triggers from the db, e.g. you just get something like hairtrigger@localhost

so it should be pretty easy to make hairtrigger handle that correctly

that said, it is a little odd that your schema.rb is generating raw CREATE TRIGGER calls (instead of create_trigger). how were these triggers added to the db? model triggers with the rake task? manual create_trigger calls? or execute("CREATE TRIGGER...")?

They were added as model triggers with the rake task, as described in the documentation.

Let me know if you would like a code sample.

Come to think of it I did get this message in the schema.rb as well: " WARNING: generating adapter-specific definition for #{name} due to a mismatch. either there's a bug in hairtrigger or you've messed up your migrations and/or db :-/"

that might be a symptom of the definer mismatch. so yeah, if you could send a sample of the model triggers you're using, that would help track it down and hopefully we can solve both problems at once

Here is the migration that was generated:

# This migration was auto-generated via `rake db:generate_trigger_migration'.
# While you can edit this file, any changes you make to the definitions here
# will be undone by the next auto-generated trigger migration.

class CreateTriggersEventUsersInsertAndEventUsersUpdate < ActiveRecord::Migration
  def up
    create_trigger("event_users_before_insert_row_tr", :generated => true, :compatibility => 1).
        on("event_users").
        before(:insert) do
      "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

    create_trigger("event_users_before_update_row_tr", :generated => true, :compatibility => 1).
        on("event_users").
        before(:update) do
      "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
  end

  def down
    drop_trigger("event_users_before_insert_row_tr", "event_users", :generated => true)

    drop_trigger("event_users_before_update_row_tr", "event_users", :generated => true)
  end
end

And here is a module included in our active record models:

module AutoCreatedUpdated

  extend ActiveSupport::Concern

  included do 

    @trigger_sql = "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)"

    trigger.before(:insert) { @trigger_sql } 
    trigger.before(:update) { @trigger_sql } 

  end

end

mysql version 5.1.70

Which created this in the schema.rb

# no candidate create_trigger statement could be found, creating an adapter-specific one
  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

  # no candidate create_trigger statement could be found, creating an adapter-specific one
  execute(<<-TRIGGERSQL)
CREATE DEFINER = root@% TRIGGER event_users_before_update_row_tr BEFORE UPDATE 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

When we added this module to multiple models at the same time we got this:

  # WARNING: generating adapter-specific definition for event_notes_before_insert_row_tr due to a mismatch.
  # either there's a bug in hairtrigger or you've messed up your migrations and/or db :-/
  execute(<<-TRIGGERSQL)
CREATE DEFINER = root@% TRIGGER event_notes_before_insert_row_tr BEFORE INSERT ON event_notes
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

  # WARNING: generating adapter-specific definition for event_notes_before_update_row_tr due to a mismatch.
  # either there's a bug in hairtrigger or you've messed up your migrations and/or db :-/
  execute(<<-TRIGGERSQL)
CREATE DEFINER = root@% TRIGGER event_notes_before_update_row_tr BEFORE UPDATE ON event_notes
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