I’m working on some Ruby code that needs to create primary keys on existing MySQL database tables. It needs to detect and fix duplicates to allow index creation to succeed, with the following algorithm:
# 1. Query
ALTER TABLE foo ADD PRIMARY KEY (id)
# 2. Handle exception:
Duplicate entry '3761290' for key 'PRIMARY' (Mysql::Error)
# 3. Query:
SELECT COUNT(1) FROM TABLE foo WHERE id = 3761290
# 4. (Assuming 5 rows were returned from the previous query) Query:
DELETE FROM TABLE foo WHERE id = 3761290 LIMIT 4 OFFSET 1
# 5. retry ALTER TABLE query
The test looks something like this:
def test_create_primary_key
table = 'foo'
db = flexmock
db.should_receive(:prepare).
with("ALTER TABLE #{table} ADD PRIMARY KEY (id)").
twice.
and_raise(Mysql::Error, "Duplicate entry '3761290' for key 'PRIMARY'")
db.should_receive(:prepare).
with("SELECT COUNT(1) FROM #{table} WHERE id = ?").
once.
and_return(MockStatement.new [ [5] ])
db.should_receive(:prepare).
with("DELETE FROM #{table} WHERE id = ? LIMIT 4 OFFSET 1").
once.
and_return(MockStatement.new [ [5] ])
indexer = Indexer.new :database_handle => db
indexer.create_indexes table
end
The problem is that the code will run in an infinite loop (unless it has a max retries condition, which it may well do), since it will continue to get an exception from the FlexMock’d db.
Ideally, the mock should be able to raise an exception the first time, then return a valid statement handle the second time. The block form of #with might work here, but I’d like to do it in a clean way if at all possible.
Any ideas?
I forgot that since Ruby (generally) conforms to The Principle of Least Astonishment, one should just try what makes sense and see what happens:
Does what it says on the tin. 🙂