4

I'm trying to see if there is a way to check the referential integrity of a single row after it has been inserted into an InnoDB table while foreign_key_checks is set to 0.

So given two tables:

CREATE TABLE `book` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `author_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `book_cc846901` (`author_id`),
  CONSTRAINT `author_id_refs_id_7fdd0933` FOREIGN KEY (`author_id`) REFERENCES `person` (`id`)
) ENGINE=InnoDB;


CREATE TABLE `person` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

We run the following statements against them:

SET foreign_key_checks=0
INSERT INTO `book` (`id`, `name`, `author_id`) VALUES (1, 'Cryptonomicon', 3)
INSERT INTO `person` (`id`, `name`) VALUES (4, 'Neal Stephenson')
SET foreign_key_checks=1

So we've put some bad data in here -- author_id refers to nonexistent person.id 3. I'm attempting to find a way to trigger a check of some kind on that row which will throw an error if there is a reference problem like this one.

The approach I initially was trying to take was to update the row with the exact same data. I tried this:

UPDATE `book` SET `name` = 'Cryptonomicon', `author_id` = 3 WHERE `book`.`id` = 1 

I expected this would trigger an error -- however, it did not. Since the data did not change, apparently no integrity check is made? Setting author_id to 2 does fail, for example.

My question then: Is there an alternative that would allow me to inspect a given row and trigger an integrity error if there is a problem? I've googled around but I have found nothing. I'd like to avoid checking the whole table but that might be the only route.

Any insights or other approaches that are escaping me would be appreciated.

(If you're curious why I'm trying to do all this, it's because I'm trying to help resolve an open issue in the Django framework where fixtures with forward references cannot be loaded. The proposed solution is to disable foreign key checks when fixtures are loaded and re-enabled them after the load is complete. I'm trying to figure out how at that point to go back and check the referential integrity of the rows that were added while foreign key checks were off and to raise an error if there is a problem.)

jsdalton
  • 6,555
  • 4
  • 40
  • 39
  • Have you looked here: http://stackoverflow.com/questions/3727905/check-for-referential-integrity-break In short, deferred constraint checking is not available in MySQL AFAIK. – Mike Jun 24 '11 at 15:58
  • @Mike I have seen that. I do understand that there is not a single command that will execute a check, but I'm hoping to find some alternative that will trigger it (e.g. the "UPDATE" hack I showed above -- I'm not even quite sure why that one doesn't work). – jsdalton Jun 24 '11 at 16:08
  • I've not come across anything. Being able to turn off foreign key constraints without then being able to check integrity once they are turned back on, is one of those things that occasionally starts a holy war on some forums! – Mike Jun 24 '11 at 17:53

2 Answers2

8

I came up with a solution, which I actually got to thanks to the answer in this question: Force InnoDB to recheck foreign keys on a table/tables?

I had seen that question before, but I finally spent about 20 minutes unpacking all the abstractions and trying to understand what it is doing in concrete terms. It's really not all that complicated, actually. Phrasing the core of the solution in terms of the example I gave in this question, you've basically got two SELECT statements:

SELECT *
FROM information_schema.KEY_COLUMN_USAGE
WHERE
    `CONSTRAINT_SCHEMA` LIKE `test`
    AND `TABLE_NAME` = `book`

That will return all of the key columns in the book table. We can iterate over these results and check their references as follows -- in this example using "author_id":

SELECT * FROM `book` as REFERRING
LEFT JOIN `person` as REFERRED
ON (REFERRING.`author_id` = REFERRED.`id`)
WHERE REFERRING.`author_id` IS NOT NULL
AND REFERRED.`id` IS NULL

Basically, that statement will return any rows where there is a value in author_id but no corresponding value for id in the related table. These are the "bad rows".

I didn't need to trigger the error directly in MySQL -- for my purposes I can raise an error in application code -- so this was as far as I needed to go. But this ended up being a fairly straightforward and fast solution, so I'm glad I found it!

Community
  • 1
  • 1
jsdalton
  • 6,555
  • 4
  • 40
  • 39
  • ...but what use is the select from the information schema, when you already know the columns with constraints? – Mike Jun 24 '11 at 21:29
  • 1
    @Mike Yeah, sorry, that part was relevant to me but not necessarily part of how I phrased the question. Basically, I need to apply this template to any table and data set, so I first need some way of establishing which columns have foreign keys, and from there to determine whether any of the foreign keys are bad. Really the second SQL statement is the core of the solution though, you're right. – jsdalton Jun 24 '11 at 21:56
1

The UPDATE trick didn't work because MySQL ignored the update, as nothing had changed:

UPDATE `book` SET
  `name` = 'Cryptonomicon',
  `author_id` = 3
WHERE `book`.`id` = 1;
Query OK, 0 rows affected (0.03 sec)
Rows matched: 1  Changed: 0  Warnings: 0

You can therefore update first to a different value, and then back to the original value, which will then throw the desired error. I've wrapped the whole thing in a transaction so that it can be rolled back without affecting the database:

START TRANSACTION;

SET foreign_key_checks=0;

INSERT INTO `book` (`id`, `name`, `author_id`)
VALUES (1, 'Cryptonomicon', 3);

SET foreign_key_checks=1;

INSERT INTO `person` (`name`) VALUES ('NULL');

UPDATE `book` SET
  `author_id` = LAST_INSERT_ID()
WHERE `book`.`id` = 1;

UPDATE `book` SET
  `author_id` = 3
WHERE `book`.`id` = 1;

ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`test`.`book`, CONSTRAINT `author_id_refs_id_7fdd0933` FOREIGN KEY (`author_id`) REFERENCES `person` (`id`))

ROLLBACK;

It seems like a messy and potentially dangerous solution, but I don't know enough about the original problem... I also don't know if you actually want to keep the original value in the table. If you do, you'll have to disable the foreign key constraints again in order to reset it!

Mike
  • 21,301
  • 2
  • 42
  • 65
  • thanks. See the answer I just posted -- this is how I finally solved it. I had considered your approach as well, but I too decided it was a bit to hackish to work. – jsdalton Jun 24 '11 at 20:58