On Triggers, Stored Procedures, and Call Stacks

If you’re a frequent reader, you might have noticed that I’m on a roll contributing MySQL patches by now… there are many more to come. This is part of the reason that I founded Proven Scaling — to be able to spend my time solving interesting problems and making MySQL better. So what about triggers, stored procedures, and call stacks?

I’ve written a patch that implements three new functions in MySQL 5.01:

  • CALLER(level) — Returns the SQL statement in the call stack at level, where 0 is the level containing the call to CALLER() itself (which is nearly useless), and 1 and above are any stored procedure or trigger calls that got us here.
  • CALLER_DEPTH() — Returns the current depth of the call stack, not counting 0. CALLER(CALLER_DEPTH()) will always return the top-most level (i.e. the user-generated command).
  • CALLER_WS(separator, level) — Returns the entire call stack starting at level, with each level separated by separator.

What are these good for? Quite a bit! For a few examples:

  • Debugging — From any place in your stored procedure code, you can now find out how exactly you got there, no matter how complex the code is. In the future, this could be augmented by allowing you to see the values of the parameters at each step as well.
  • Auditing — This is much more interesting for most people, and while the current implementation isn’t perfect2, it’s getting a lot closer to what people need for auditing purposes. Using triggers and CALLER(), USER(), and NOW() you can get most of the auditing information you may need.

Now, for some examples of the output from these new functions:

Just a test of CALLER() and CALLER_DEPTH():

mysql> select caller(0);
+------------------+
| caller(0)        |
+------------------+
| select caller(0) | 
+------------------+
1 row in set (0.00 sec)

mysql> select caller_depth();
+----------------+
| caller_depth() |
+----------------+
|              0 | 
+----------------+
1 row in set (0.00 sec)

In order to get anything interesting, we need to generate some depth of the call stack:

delimiter //
create procedure stack() begin select caller_ws(' <called by> ', 1); end //
create procedure ts1() begin call stack(); end //
create procedure ts2() begin call ts1(); end //
create procedure ts3() begin call ts2(); end //

Now we can try it out for real:

mysql> call ts2()//
+------------------------------------------------------------+
| caller_ws(' <called by> ', 1)                              |
+------------------------------------------------------------+
| call stack() <called by> call ts1() <called by> call ts2() | 
+------------------------------------------------------------+
1 row in set (0.00 sec)

As an example of what you can do inside stored procedures:

DROP PROCEDURE IF EXISTS test.dump_stack //
CREATE DEFINER='root'@'localhost' PROCEDURE test.dump_stack ()
BEGIN
  DECLARE i INT DEFAULT 1;
  DECLARE caller_depth INT DEFAULT 0;
  SET caller_depth = CALLER_DEPTH();

  CREATE TEMPORARY TABLE test_stack_t (
    depth INT NOT NULL,
    query TEXT, 
    PRIMARY KEY (depth)
  );
  WHILE i <= caller_depth
  DO
    INSERT INTO test_stack_t (depth, query) VALUES (i, caller(i+1));
    SET i = i + 1;
  END WHILE;
  SELECT depth, query FROM test_stack_t ORDER BY depth DESC;
  DROP TEMPORARY TABLE test_stack_t;
END
//

We can then redefine stack() to call dump_stack() instead, so that we can use the same tsX() procedures above:

drop procedure if exists stack //
create procedure stack() begin call dump_stack(); end //

And the same test:

mysql> call ts2()//
+-------+--------------+
| depth | query        |
+-------+--------------+
|     3 | call ts2()   | 
|     2 | call ts1()   | 
|     1 | call stack() | 
+-------+--------------+
3 rows in set (0.00 sec)

With a little additional magic, we can use CALLER() for auditing2:

DROP TABLE IF EXISTS test.audit //
CREATE TABLE test.audit (
  id INT NOT NULL auto_increment,
  ts DATETIME,
  user CHAR(64), 
  call_stack TEXT, 
  PRIMARY KEY (id), 
  INDEX (ts), 
  INDEX (user)
)
//

DROP PROCEDURE IF EXISTS test.audit //
CREATE DEFINER='root'@'localhost' PROCEDURE test.audit ()
BEGIN
  INSERT INTO test.audit (ts, user, call_stack)
  VALUES (now(), user(), caller_ws(' <called by> ', 2));
END
//

DROP TABLE IF EXISTS test.test //
CREATE TABLE test.test (
  c CHAR(50)
)
//

CREATE DEFINER='root'@'localhost' TRIGGER test_test_b_i
BEFORE INSERT ON test.test 
FOR EACH ROW CALL test.audit()
//

CREATE DEFINER='root'@'localhost' PROCEDURE addtest(in in_c char(50))
BEGIN
  INSERT INTO test.test (c) VALUES (in_c);
END
//

And, let’s try it out:

mysql> INSERT INTO test.test (c) VALUES ('jeremy') //
Query OK, 1 row affected (0.01 sec)

mysql> SELECT * FROM test.audit //
+----+---------------------+---------------+---------------------------------------------+
| id | ts                  | user          | call_stack                                  |
+----+---------------------+---------------+---------------------------------------------+
|  4 | 2006-10-01 18:07:39 | root@dhcp-100 | INSERT INTO test.test (c) VALUES ('jeremy') |
+----+---------------------+---------------+---------------------------------------------+
1 row in set (0.00 sec)

mysql> CALL addtest('monty') //
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM test.audit //
+----+---------------------+---------------+---------------------------------------------------------------------------+
| id | ts                  | user          | call_stack                                                                |
+----+---------------------+---------------+---------------------------------------------------------------------------+
|  4 | 2006-10-01 18:07:39 | root@dhcp-100 | INSERT INTO test.test (c) VALUES ('jeremy')                               |
|  5 | 2006-10-01 18:11:26 | root@dhcp-100 | INSERT INTO test.test (c) VALUES (in_c) <called by> CALL addtest('monty') |
+----+---------------------+---------------+---------------------------------------------------------------------------+
2 rows in set (0.01 sec)

Let me know what you think!

1 Specifically mysql-5.0.25-nightly-20060823.

2 Currently, triggers are missing three important features to make them really useful for auditing: a way to find the call stack (which this patch addresses), the ability to have multiple triggers per type per table (currently MySQL only allows one trigger per table per type [before update, after insert, etc.]), and per-statement triggers (currently MySQL only has per-row triggers).

4 thoughts on “On Triggers, Stored Procedures, and Call Stacks

  1. That is really nice. It solves a big part of the audit problem. You mentioned that MySQL needs statement level triggers.

    something like maybe:
    CREATE TRIGGER [:trigger_name:] [BEFORE|AFTER] [DML|DDL|(any sql command like alter table or select)] ON [DATABASE [ALL|[:database_name:]|CURRENT] | TABLE [:table_name:] ]

    CREATE TRIGGER [:trigger_name:] ON CONNECTION [FOR USER user_id]

    You could audit all (or a subset of) DDL at a high level then add fine-grained tuning at the table level.

    Am I making crazy talk, or do those sound, well, sound?

  2. Proven Scaling Blog » Making MySQL more usable: Stored Procedure call stack on error

  3. Hi, I wonder to know, how I can add those three functions in MySQL. I’ve work on Postgres and I don’t really have experience with MySQL. I understand the use of the functions (and what you said in the post), but I don’t know how to add the caller.patch. Thanks a lot for your help.
    Diego

What do you think?

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s