7

Here's my sql request:

$sql
    = 'CREATE TEMPORARY TABLE tmp '
    . 'SELECT * FROM '.$table.' '
    . 'WHERE id=:id; '
    . 'ALTER TABLE tmp drop ID; '
    . 'INSERT INTO '.$table.' '
    . 'SELECT 0,tmp.* FROM tmp; '
    . 'SET @last=LAST_INSERT_ID(); '
    . 'DROP TABLE tmp;'
    . 'SELECT @last; ';
    $stmt = $this->bd->execQuery($sql, array(':id'=>101));
    echo "1 -> = "; var_export($stmt); echo "\n";
    $stmt = $stmt->fetch(PDO::FETCH_OBJ);
    echo "2 -> = "; var_export($stmt); echo "\n";

The dump talk by itself: the query works (I've checked).

sql =
'CREATE TEMPORARY TABLE tmp SELECT * FROM categorie WHERE id=:id; ALTER TABLE tmp drop ID; INSERT INTO categorie SELECT 0,tmp.* FROM tmp; SET @last=LAST_INSERT_ID(); DROP TABLE tmp;SELECT @last; '
params = array (
  ':id' => 101,
)
1 -> = PDOStatement::__set_state(array(
   'queryString' => 'CREATE TEMPORARY TABLE tmp SELECT * FROM categorie WHERE id=:id; ALTER TABLE tmp drop ID; INSERT INTO categorie SELECT 0,tmp.* FROM tmp; SET @last=LAST_INSERT_ID(); DROP TABLE tmp;SELECT @last; ',
))
2 -> = false

If I do it "by hand" on the console line it works too (sorry for the looong line of code):

mysql> CREATE TEMPORARY TABLE tmp SELECT * FROM categorie WHERE id=101; ALTER TABLE tmp drop ID; INSERT INTO categorie SELECT 0,tmp.* FROM tmp; SET @last=LAST_INSERT_ID(); DROP TABLE tmp;SELECT @last; 
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

Query OK, 1 row affected (0.07 sec)
Records: 1  Duplicates: 0  Warnings: 0

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+-------+
| @last |
+-------+
|   141 |
+-------+
1 row in set (0.00 sec)

mysql> 

Here's my code that is being executed.

public function execQuery($sql, $tab=array())
{
    $stmt = self::$_pdo->prepare($sql);
    if ($stmt===false) {
        throw new Exception(
            'Erreur prepare '.$sql.
            ' = '.var_export(self::$_pdo->errorInfo(), true)
        );
    }
    foreach ($tab as $key=>$valeur) {
        $stmt->bindValue($key, $valeur);
    }
    if ($stmt->execute()===false) {
        throw new Exception(
            "Erreur execution de la requete :\n\"".$sql."\"\n".
            "Paramètres de la requete :\n\"".var_export($tab, true)."\"\n".
            "Details de l'erreur : \n".var_export(self::$_pdo->errorInfo(), true)
        );
    }
    return $stmt;
}

How can I do to get the last inserted value in one shot (= make what I did work)?

Olivier Pons
  • 15,363
  • 26
  • 117
  • 213
  • 3
    I believe the php mysql interfaces only allow one query per function call - presumably it has to do with fetching the results correctly. – Sam Dufel Apr 28 '12 at 17:31
  • 2
    Instead of checking for false, set `$pdo->setAttribute(ATTR_ERRMODE, ERRMODE_EXCEPTION)` to have PDO throw `PDOException`s in case of any error. This will guarantee you get a specific error and know what you did wrong. – Madara's Ghost Apr 28 '12 at 17:34
  • Doesn't answer the question, but you could always put your SQL in a stored procedure... – eggyal Apr 28 '12 at 17:55
  • @eggyal Is it possible to send a parameter to a stored procedure (here it's the table name, and there are more parameters (i've removed here to have a shorter code sample))? – Olivier Pons Apr 28 '12 at 20:41
  • Sure, procedures can take parameters. If it's a table name, however, you'd need to build your desired SQL command as a prepared statement. – eggyal Apr 28 '12 at 20:56
  • That's what I do in my code: `$_pdo->prepare($sql);` ... or am I missing something? – Olivier Pons Apr 28 '12 at 21:15
  • Sorry you're right i didn't put the full code for `execQuery()`. – Olivier Pons Apr 28 '12 at 22:35
  • @Truth Thank you very much I'll modify my code. – Olivier Pons Apr 29 '12 at 20:30
  • @OlivierPons: That's not quite what I meant - see my answer below. – eggyal Apr 29 '12 at 22:23
  • Why are you doing all of this anyway? Seems like an expensive alternative to `SELECT COUNT(*) + 1 FROM $table WHERE id=?` – Ja͢ck Apr 30 '12 at 08:33
  • 1
    @Jack This is the most dangerous suggestion ever in a webserver environment. (Suppose there's a lot of traffic and an insert between the operations. I'll work on the bad ID (which would be in that case id+1 **+(x in-between operations)** )). I'd suggest you to learn more about client-server databases principles. – Olivier Pons Apr 30 '12 at 08:44
  • @OlivierPons You could get better answers if you mention the background of what you're trying to achieve. – Ja͢ck Apr 30 '12 at 08:58
  • @Truth You saved my day, `self::$_pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);` gives very detailed problem and that's what I was missing so many times! Thanks for the hint! – Olivier Pons Apr 30 '12 at 12:33
  • As of PHP version 5.3 you can now perform multiple queries. See [PDO Multiple Queries](http://stackoverflow.com/questions/11271595/pdo-multiple-queries). – Jeff Vdovjak Feb 09 '13 at 08:49

1 Answers1

1

As mentioned in my comment above, whilst it doesn't answer your question of how you issue multiple SQL commands in one query from PHP, one workaround would be to put your SQL in a stored procedure using a prepared statement:

DELIMITER ;;

CREATE PROCEDURE copyRecord(TableName VARCHAR(20), id INT) BEGIN
  -- prevent SQL injection
  SET TableName = CONCAT('`', REPLACE(TableName, '`', '``'), '`');
  SET @id = id;

  SET @sql = CONCAT('
    CREATE TEMPORARY TABLE tmp SELECT * FROM ', TableName, ' WHERE id = ?
  ');
  PREPARE stmt FROM @sql;
  EXECUTE stmt USING @id;
  DEALLOCATE PREPARE stmt;

  ALTER TABLE tmp drop ID;

  SET @sql = CONCAT('
    INSERT INTO ', TableName, ' SELECT 0,tmp.* FROM tmp
  ');
  PREPARE stmt FROM @sql;
  EXECUTE stmt;
  DEALLOCATE PREPARE stmt;

  DROP TABLE tmp;

  SET @sql = NULL;
  SET @id  = NULL;
  SELECT LAST_INSERT_ID();
END;;

DELIMITER ;

From PHP you would then simply invoke the SQL command CALL copyRecord('categorie', 101).

eggyal
  • 122,705
  • 18
  • 212
  • 237
  • Could it be possible to do something like `CREATE TEMPORARY TABLE tmp SELECT * FROM ? WHERE id=?;`? – Olivier Pons Apr 30 '12 at 08:15
  • @OlivierPons: Sadly not; one can only provide values as parameters - not identifiers. – eggyal Apr 30 '12 at 08:16
  • `mysql --default-character-set=utf8 -u root -p -h localhost dev < tt ERROR 1064 (42000) at line 3: 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 'sql TEXT;` / mysql Ver 14.14 Distrib 5.1.62, for debian-linux-gnu (x86_64) using readline 6.1 – Olivier Pons Apr 30 '12 at 08:50
  • Same: `ERROR 1064 (42000) at line 3: 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 '\`sql\`;` – Olivier Pons Apr 30 '12 at 09:49
  • The problems when I execute "`PREPARE stmt FROM \`sql\`;`". If I comment the PREPARE EXECUTE DEALLOCATE it works. Only PREPARE uncommented gives me: `ERROR 1064 (42000) at line 3: 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 '\`sql\`; END' at line 7` – Olivier Pons Apr 30 '12 at 09:51
  • @OlivierPons: My bad: prepared statements can only be constructed from string literals and *user* variables, not *local* variables (because the statement might remain in scope after the procedure exits, whereas the local variable won't) - see http://dev.mysql.com/doc/refman/5.5/en/local-variable-scope.html. Updated version above. – eggyal Apr 30 '12 at 09:59
  • You're right: `EXECUTE stmt` is ok but `EXECUTE stmt USING id` is not ok. So I concatenate id like `'\` WHERE id=', id, '` (and it should be safe -correct me if i'm wrong- because id is declared as an INT) – Olivier Pons Apr 30 '12 at 10:21
  • It worked but the result is **not** the new id but "true"... **`(>.<)"`** – Olivier Pons Apr 30 '12 at 10:28
  • Yes, as an `INT` it should be safe... but if you wanted to use parameters, you'd have to go via another user variable - see http://dev.mysql.com/doc/refman/5.1/en/execute.html. Suggest you move `SELECT @last;` outside of the prepared statement so that it is the final command in the procedure (to be honest, you ought to be able to simply call `SELECT LAST_INSERT_ID()` there). I will update my answer. – eggyal Apr 30 '12 at 10:30
  • It worked (including your latest suggestion `SELECT LAST_INSERT_ID()`) but only when I exploded the multiple queries into multiple SET/PREPARE/EXECUTE/DEALLOCATE (= 5 times SET/PREPARE/EXECUTE/DEALLOCATE). – Olivier Pons Apr 30 '12 at 10:57
  • Aaaah. I never knew that - but you're right, the [manual](http://dev.mysql.com/doc/refman/5.5/en/sql-syntax-prepared-statements.html) says "SQL syntax for prepared statements does not support multi-statements (that is, multiple statements within a single string separated by “`;`” characters)." - I will update my answer. – eggyal Apr 30 '12 at 11:04
  • Actually, you shouldn't need to perform the `SELECT LAST_INSERT_ID()` at all since it should be available to you in PHP through PDO's [`lastInsertId()`](http://php.net/manual/en/pdo.lastinsertid.php) function. – eggyal Apr 30 '12 at 11:09