Well, I sat and played with some old[er] code that did work (thank goodness for incremental backups!) and compared it with the change that didn't work and eventually worked out what the problem was.
The $st->execute($row); doesn't like it if $row contains a column that doesn't match the prepared statement. It can't cope with using just the columns it needs from $row.
I had explicitly set a value in $row empty (and thus the value existed) which didn't occur in the prepared statement. Then, trying to cut down the prepared statement simply made things worse because lots of values in $row were set that weren't in the prepared statement.
As always I suspect that explaining the problem made me stand back from it a little and work out what was wrong - so thanks for listening anyway (if you bothered!). :-)