Problem/Motivation

Connection::query can overload the $query argument to be either a string or an already prepared statement. This is a bit confusing, since the logic of potentially calling query to get a statement and then query again with the statement as input is weird. Normally in PHP world you would prepare a statement, execute it and iterate to fetch the results - then execute it again with different $args if you want to reuse.

Also the current status will prevent in the future to typehint the $query argument, which is now possible in PHP 7+.

Proposed resolution

- Deprecate passing a StatementInterface as $query argument to Connection::query.
- When a database operation is started with a Connection::prepareStatement, follow suit with StatementInterface::execute and StatementInterface::fetch (in case of a SELECT), or other statement inspection methods for INSERT / DELETE / UPDATE.
- Keep Connection::query as a convenience helper that prepares the statement and executes it in one go.

Remaining tasks

User interface changes

None

API changes

None

Data model changes

None

Release notes snippet

Deprecate passing a Drupal\Core\Database\StatementInterface object to Drupal\Core\Database\Connection::query().

CommentFileSizeAuthor
#63 3137883-63.patch8 KBmondrake
#63 interdiff_56-63.txt2.01 KBmondrake
#60 interdiff-58.txt514 bytesPooja Ganjage
#58 3137883-58.patch8.02 KBPooja Ganjage
#56 3137883-56.patch8.3 KBPooja Ganjage
#53 interdiff_47_53.txt571 bytesanmolgoyal74
#53 3137883-53.patch7.99 KBanmolgoyal74
#48 interdiff-47.txt961 bytesPooja Ganjage
#47 3137883-47.patch8.1 KBPooja Ganjage
#46 3137883-46.patch8.17 KBpaulocs
#46 interdiff-39-46.txt1.08 KBpaulocs
#41 interdiff-39.txt3.48 KBPooja Ganjage
#39 3137883-39.patch8.11 KBPooja Ganjage
#36 3137883-36.patch8.12 KBandypost
#36 interdiff.txt2.62 KBandypost
#23 3137883-23.patch8.07 KBmondrake
#23 interdiff_17-23.txt2.33 KBmondrake
#17 3137883-17.patch8.09 KBmondrake
#17 interdiff_16-17.txt1.9 KBmondrake
#15 3137883-16.patch6.83 KBmondrake
#15 interdiff_13-16.txt4.84 KBmondrake
#13 3137883-13.patch7.33 KBmondrake
#10 interdiff_8-10.txt2.61 KBmondrake
#10 3137883-10.patch5.83 KBmondrake
#9 3137883-9.patch5.82 KBmondrake
#9 interdiff_8-9.txt2.6 KBmondrake
#8 3137883-8.patch4.88 KBmondrake
#8 interdiff_7-8.txt1.17 KBmondrake
#7 interdiff_6-7.txt2.11 KBmondrake
#7 3137883-7.patch4.61 KBmondrake
#6 3137883-6.patch2.5 KBmondrake

Issue fork drupal-3137883

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mondrake created an issue. See original summary.

tdnshah’s picture

Assigned: Unassigned » tdnshah
tdnshah’s picture

Assigned: tdnshah » Unassigned
mondrake’s picture

Status: Active » Postponed
mondrake’s picture

mondrake’s picture

Not bad, #6 tells us there are already no calls to query with a statement argument in core code - apart from the ones within the pgsql driver itself.

Let's see if this fixes pgsql, by using the appropriate statement methods instead of query.

mondrake’s picture

mondrake’s picture

Status: Needs review » Needs work

The last submitted patch, 10: 3137883-10.patch, failed testing. View results

mondrake’s picture

Assigned: Unassigned » mondrake

On this.

daffie’s picture

Status: Needs review » Needs work
  1. Why do we add the trigger_error() to Drupal\Core\Database\Driver\mysql\Connection::query() and Drupal\Core\Database\Driver\pgsql\Connection::query()? Is the one in Drupal\Core\Database\Connection::query() not enough?
  2. AFAIK we only have one simple test for getting the last inserted ID. We are doing some strange stuff with the last inserted ID in Drupal\Core\Database\Driver\pgsql\Insert::execute(). Can we add some testing for this?
  3. +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
    @@ -101,15 +103,30 @@ public function execute() {
    +        $exception = new IntegrityConstraintViolationException($message, $e->getCode(), $e);
    ...
    +        $exception = new DatabaseExceptionWrapper($message, 0, $e);
    
    +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php
    @@ -75,9 +75,10 @@ public function execute() {
    +      $stmt->allowRowCount = TRUE;
    

    Can we add some testing for the exceptions being thrown here.

  4. We also need deprecation message testing.
mondrake’s picture

Thanks @daffie.

Fixed #14.1 and .4.

#14.2 what's strange? Since we are not calling query anymore, we need to explicitly manage any exception thrown by the call to StatementInterface::execute. The existing insert tests cover that already.

#14.3 outstanding.

mondrake’s picture

mondrake’s picture

mondrake’s picture

#17 adds testing for the new Connection::lastInsertId method, per #14.2.

Re #14.3, InvalidDataTest::testInsertDuplicateData, covers the case already.

Switching to NR.

daffie’s picture

Issue summary: View changes
Status: Needs review » Needs work

Updated the IS and created an CR.

  1. +++ b/core/lib/Drupal/Core/Database/Connection.php
    @@ -745,6 +745,7 @@ public function query($query, array $args = [], $options = []) {
    +        @trigger_error('Passing a StatementInterface object as a $query argument to ' . __METHOD__ . ' is deprecated in drupal:9.TODO.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/TODO', E_USER_DEPRECATED);
    

    Fix the TODO parts.

  2. +++ b/core/lib/Drupal/Core/Database/Connection.php
    @@ -1015,6 +1016,32 @@ public function insert($table, array $options = []) {
    +   * If the PDO driver does not support this capability, \PDO::lastInsertId()
    +   * triggers an IM001 SQLSTATE.
    ...
    +  public function lastInsertId(?string $name = NULL): string {
    

    Should we not add a @throws annotation to the docblock?

  3. +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
    @@ -101,15 +103,30 @@ public function execute() {
    +    catch (\PDOException $e) {
    +      $this->connection->rollbackSavepoint();
    +      $message = $e->getMessage() . ": " . $stmt->getQueryString();
    +      // Match all SQLSTATE 23xxx errors.
    +      if (substr($e->getCode(), -6, -3) == '23') {
    +        $exception = new IntegrityConstraintViolationException($message, $e->getCode(), $e);
    +      }
    +      else {
    +        $exception = new DatabaseExceptionWrapper($message, 0, $e);
    +      }
    +      throw $exception;
    +    }
    

    Is this part PostgreSQL specific or does this belong in the method Connection::lastInsertId()? Just asking.

mondrake’s picture

#19:

2. Well this is just a proxy to the method on the PDO connection. Whatever that one will throw, this method will just pass it through. In other words, whateevr is thrown, it is not thrown here. We have a @see \PDO::lastInsertId already, IMO that's enough.

3. It is pgsql specific, in the sense that for this driver we prepare a statement (already, in HEAD), and then we execute it and have to manage any exceptions that previoulsy would have been captured by query and managed by handleQueryException. Other drivers just run query which is self contained.
See this in PGSql (currently in HEAD, without the patch):

/**
 * PostgreSQL implementation of \Drupal\Core\Database\Query\Insert.
 */
class Insert extends QueryInsert {

  public function execute() {
    ...

    $stmt = $this->connection->prepareStatement((string) $this, $this->queryOptions);

    ...

    $this->connection->addSavepoint();
    try {
      // Only use the returned last_insert_id if it is not already set.
      if (!empty($last_insert_id)) {
        $this->connection->query($stmt, [], $options);
      }
      else {
        $last_insert_id = $this->connection->query($stmt, [], $options);
      }
      $this->connection->releaseSavepoint();
    }
    catch (\Exception $e) {
      $this->connection->rollbackSavepoint();
      throw $e;
    }

and in MySql

/**
 * MySQL implementation of \Drupal\Core\Database\Query\Insert.
 */
class Insert extends QueryInsert {

  public function execute() {
    ...

    $last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions);

IMHO, Connection::query is overdoing. Since we have db drivers, each operation should prepare its own statement, execute it, and handle its own exceptions.

daffie’s picture

@mondrake: Thank you for your explanation. If you could do #19.1, then it is RTBC for me.

mondrake’s picture

Assigned: Unassigned » mondrake

on this

mondrake’s picture

Done #19.1.

daffie’s picture

Status: Needs review » Reviewed & tested by the community

All looks good to me.
All my remarks are addressed.
For me it is RTBC.

mondrake’s picture

Issue summary: View changes
catch’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
@@ -101,15 +103,30 @@ public function execute() {
       $this->connection->releaseSavepoint();
     }
+    catch (\PDOException $e) {
+      $this->connection->rollbackSavepoint();
+      $message = $e->getMessage() . ": " . $stmt->getQueryString();
+      // Match all SQLSTATE 23xxx errors.
+      if (substr($e->getCode(), -6, -3) == '23') {
+        $exception = new IntegrityConstraintViolationException($message, $e->getCode(), $e);
+      }
+      else {
+        $exception = new DatabaseExceptionWrapper($message, 0, $e);
+      }
+      throw $exception;
+    }

Is this actually necessary in order to deprecate passing a statement, or is it a bug found while working on the patch?

mondrake’s picture

Status: Needs review » Reviewed & tested by the community

It's not a bug, and yes it's needed to deprecate.

That's because the post execute processing is no longer done in Connection::query here:

+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
@@ -101,15 +103,30 @@ public function execute() {
+      $stmt->execute(NULL, $options);
       // Only use the returned last_insert_id if it is not already set.
-      if (!empty($last_insert_id)) {
-        $this->connection->query($stmt, [], $options);

+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/NativeUpsert.php
@@ -80,7 +80,7 @@ public function execute() {
-      $this->connection->query($stmt, [], $options);
+      $stmt->execute(NULL, $options);

+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Update.php
@@ -75,9 +75,10 @@ public function execute() {
-      $result = $this->connection->query($stmt, [], $options);
+      $stmt->execute(NULL, $options);

Normally query will deal with processing the results, but now in these classes we now no longer call query, rather prepare the statement and execute it. So the same code that is currently in Connection:

      switch ($options['return']) {
        case Database::RETURN_STATEMENT:
          return $stmt;

        case Database::RETURN_AFFECTED:
          $stmt->allowRowCount = TRUE;
          return $stmt->rowCount();

        case Database::RETURN_INSERT_ID:
          $sequence_name = isset($options['sequence_name']) ? $options['sequence_name'] : NULL;
          return $this->connection->lastInsertId($sequence_name);

        case Database::RETURN_NULL:
          return NULL;

        default:
          throw new \PDOException('Invalid return directive: ' . $options['return']);
      }

needs to be replicated into the dedicated classes.

mondrake’s picture

Also, see #20.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review

Is this change actually worth it? In PHP 8.0 we'll get the ability to do string|StatementInterface as a typehint.

+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -1015,6 +1016,32 @@ public function insert($table, array $options = []) {
+  public function lastInsertId(?string $name = NULL): string {

Adding this has it's own risks and adds new API in order to deprecate other API - which seems odd.

andypost’s picture

+++ b/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php
@@ -134,6 +134,18 @@ public function testTransactionsOptionDeprecation() {
+   * @expectedDeprecation Passing a StatementInterface object as a $query argument to Drupal\Core\Database\Connection::query is deprecated in drupal:9.1.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439

needs re-roll for https://www.drupal.org/node/3176667

andypost’s picture

Status: Needs review » Needs work

NW for #30

New API is a way to get rid of getClientConnection() (btw why not getDriverConnection)

Sequences API mostly unused by both core and contrib so there's at least #2665216: Deprecate Drupal\Core\Database\Connection::nextId() and the {sequences} table and schema

mondrake’s picture

Looking outside of Drupal, doctrine/dbal has a lastInsertId method on its Connection class, https://github.com/doctrine/dbal/blob/2224d36702f0d7313bdecd2b01169767b7...

andypost’s picture

And even https://www.php.net/manual/ru/sqlite3.lastinsertrowid.php non-PDO drivers implement it

andypost’s picture

+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -1015,6 +1016,32 @@ public function insert($table, array $options = []) {
+   * @return string
+   *   The value returned by the wrapped PDO connection.
...
+   * @see \PDO::lastInsertId
...
+  public function lastInsertId(?string $name = NULL): string {
+    return $this->connection->lastInsertId($name);

Better to mark it @internal as return value may flux depending on driver

PHP 8 defines it as @return string|false
https://github.com/php/php-src/blob/php-8.0.0RC2/ext/pdo/pdo_dbh.stub.ph...

andypost’s picture

Also looking at implementation it may throw https://github.com/php/php-src/blob/php-8.0.0RC2/ext/pdo/pdo_dbh.c#L955-... but returns FALSE

andypost’s picture

Fix for #30 and re-roll

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

daffie’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Database/Connection.php
    @@ -1046,6 +1047,34 @@ public function insert($table, array $options = []) {
    +   * If the PDO driver does not support this capability, \PDO::lastInsertId()
    +   * triggers an IM001 SQLSTATE.
    

    Should we not move this text to a @throws in the docblock of this method?

  2. +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
    @@ -103,15 +105,30 @@ public function execute() {
    +        $exception = new DatabaseExceptionWrapper($message, 0, $e);
    

    Why do we not add the $e->getCode() DatabaseExceptionWrapper call?

  3. +++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
    @@ -103,15 +105,30 @@ public function execute() {
    +      if (substr($e->getCode(), -6, -3) == '23') {
    +        $exception = new IntegrityConstraintViolationException($message, $e->getCode(), $e);
    +      }
    +      else {
    +        $exception = new DatabaseExceptionWrapper($message, 0, $e);
    +      }
    +      throw $exception;
    

    Can we change the construction to (for better readability):

         if (substr($e->getCode(), -6, -3) == '23') {
           throw new IntegrityConstraintViolationException($message, $e->getCode(), $e);
         }
         else {
           throw new DatabaseExceptionWrapper($message, 0, $e);
         }
    
  4. +++ b/core/lib/Drupal/Core/Database/Connection.php
    @@ -776,6 +776,7 @@ public function query($query, array $args = [], $options = []) {
    +        @trigger_error('Passing a StatementInterface object as a $query argument to ' . __METHOD__ . ' is deprecated in drupal:9.1.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439', E_USER_DEPRECATED);
    
    +++ b/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php
    @@ -134,6 +134,18 @@ public function testTransactionsOptionDeprecation() {
    +    $this->expectDeprecation('Passing a StatementInterface object as a $query argument to Drupal\Core\Database\Connection::query is deprecated in drupal:9.1.0 and is removed in drupal:10.0.0. Call the execute method from the StatementInterface object directly instead. See https://www.drupal.org/node/3154439');
    

    We are now working on 9.2.0 and not with 9.1.0

For #29: If we want to use the construction parameter type hint with string|StatementInterface we shall have to wait for A Drupal version that only supports PHP 8.0 or higher. Personally I do not want to wait for that. Just my opinion. :)

Pooja Ganjage’s picture

Hi,

Creating a patch as per the #38 comment suggestion.

Please review the patch.

Thanks.

ravi.shankar’s picture

@Pooja Ganjage can you please provide interdiff as well.

Pooja Ganjage’s picture

FileSize
3.48 KB

Attached interdiff for #39 patch.

Pooja Ganjage’s picture

Status: Needs work » Needs review
daffie’s picture

Status: Needs review » Needs work

@Pooja: All your changes look good. Only point #38.3 is still open. After that is done, will I give it a RTBC.

Pooja Ganjage’s picture

Hi,

@daffie, I have not understand the point of #38.3, what need to change?

Please give me suggestion for this. So, I can create a patch again.

Thanks.

paulocs’s picture

Assigned: Unassigned » paulocs
paulocs’s picture

Assigned: paulocs » Unassigned
FileSize
1.08 KB
8.17 KB

Fixing code standard.
Point #38.3 is still open.

Pooja Ganjage’s picture

Hi,

Updated patch with mentioned changes into the #38.3 point.

Please review the patch.

Thanks.

Pooja Ganjage’s picture

FileSize
961 bytes

Attached interdiff for #47 patch.

Pooja Ganjage’s picture

Status: Needs work » Needs review
daffie’s picture

Status: Needs review » Needs work

I reviewed the patch from Pooja Ganjage. Just one point to fix:

+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -1078,6 +1079,36 @@ public function insert($table, array $options = []) {
+   * @throws IM001 SQLSTATE
+   *    If the PDO driver does not support this capability, \PDO::lastInsertId()
+   *    triggers an IM001 SQLSTATE.
+   *
+   * @return string|false
+   *   The value returned by the wrapped PDO connection.

According to the Drupal coding standards the tag @returns comes before @throws. See: https://www.drupal.org/docs/develop/standards/api-documentation-and-comm.... Almost RTBC.

@paulocs: I did not review your patch, because the other patch has comment #38.3 fixed.

mondrake’s picture

+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -1078,6 +1079,36 @@ public function insert($table, array $options = []) {
+   * @throws IM001 SQLSTATE
+   *    If the PDO driver does not support this capability, \PDO::lastInsertId()
+   *    triggers an IM001 SQLSTATE.
+   *

Actually I think we can drop this altogether because if the PDO driver were not implementing this, it would not be possible to use it in Drupal.

daffie’s picture

I agree with @mondrake. Lets remove the @throws from the docblock.

anmolgoyal74’s picture

Status: Needs work » Needs review
FileSize
7.99 KB
571 bytes

Removed the @throws from the docblock.

daffie’s picture

Status: Needs review » Reviewed & tested by the community

The patch does what is in the IS.
There is deprecation testing.
The CR has the deprecation and the API addition in it.
All changes in the patch look good to me.
For me it is RTBC.

mondrake’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Database/Connection.php
    @@ -1078,6 +1079,32 @@ public function insert($table, array $options = []) {
    +   * @internal
    

    IMHO this is not really @internal, since database drivers may override it. How about using the statement recently used in the StatementWrapper class:

       * This method should normally be used only within database driver code.
    
  2. +++ b/core/tests/Drupal/KernelTests/Core/Database/ConnectionTest.php
    @@ -134,6 +134,18 @@ public function testTransactionsOptionDeprecation() {
    +  public function testStatementQueryDeprecation() {
    

    should have a void return typehint

  3. +++ b/core/tests/Drupal/KernelTests/Core/Database/StatementTest.php
    @@ -31,6 +31,7 @@ public function testRepeatedInsertStatementReuse() {
    +    $this->assertSame(5, (int) $this->connection->lastInsertId(), 'lastInsertId should return the latest value of the auto-increment id column.');
    
    @@ -38,6 +39,7 @@ public function testRepeatedInsertStatementReuse() {
    +    $this->assertSame(6, (int) $this->connection->lastInsertId(), 'lastInsertId should return the latest value of the auto-increment id column.');
    

    this is a database test, and IMHO we should be strict, therefore suggest we do not typecast:

    +++ b/core/tests/Drupal/KernelTests/Core/Database/StatementTest.php
    @@ -31,6 +31,7 @@ public function testRepeatedInsertStatementReuse() {
    +    $this->assertSame('5', $this->connection->lastInsertId(), 'lastInsertId should return the latest value of the auto-increment id column.');
    
    @@ -38,6 +39,7 @@ public function testRepeatedInsertStatementReuse() {
    +    $this->assertSame('6', $this->connection->lastInsertId(), 'lastInsertId should return the latest value of the auto-increment id column.');
    
Pooja Ganjage’s picture

Updated patch as per the #55 comment suggestion.

mondrake’s picture

+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -480,7 +480,7 @@ public function prefixTables($sql) {
-   *   This method should only be called by database API code.
+   *   This method should normally be used only within database driver code.

please revert this, it's not in the scoipe of the issue here

and do this for lastInsertId

+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -1078,6 +1079,32 @@ public function insert($table, array $options = []) {
+  /**
+   * Returns the ID of the last inserted row or sequence value.
+   *
+   * This method should normally be used only within database driver code.
+   *
+   * This is a proxy to invoke lastInsertId() from the wrapped connection.
+   * If a sequence name is not specified for the name parameter, this
+   * returns a string representing the row ID of the last
+   * row that was inserted into the database.
+   * If a sequence name is specified for the name parameter,
+   * this returns a string representing the last value retrieved
+   * from the specified sequence object.
+   *
+   * @param string|null $name
+   *   (Optional) Name of the sequence object from which the ID should be
+   *   returned.
+   *
+   * @return string|false
+   *   The value returned by the wrapped connection.
+   *
+   * @see \PDO::lastInsertId
+   */
+  public function lastInsertId(?string $name = NULL) {
+    return $this->connection->lastInsertId($name);
+  }
+

so this is a bit less PDO-centric, we have discussions open to abstract more from PDO so let's not go in the opposite direction.

Pooja Ganjage’s picture

Updated patch.

Pooja Ganjage’s picture

Status: Needs work » Needs review
Pooja Ganjage’s picture

FileSize
514 bytes

Attached interdiff for #58 patch.

daffie’s picture

Status: Needs review » Needs work

The patch does not what @mondrake has asked for in comment #57. Therefor back to needs work.

mondrake’s picture

Assigned: Unassigned » mondrake

working on this

mondrake’s picture

Assigned: mondrake » Unassigned
Status: Needs work » Needs review
FileSize
2.01 KB
8 KB

done #57

daffie’s picture

Status: Needs review » Reviewed & tested by the community

The docblock for lastInsertId looks good to me.
Back to RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
@@ -103,15 +105,29 @@ public function execute() {
     try {
+      $stmt->execute(NULL, $options);
       // Only use the returned last_insert_id if it is not already set.
-      if (!empty($last_insert_id)) {
-        $this->connection->query($stmt, [], $options);
+      // PostgreSQL requires the table name to be specified explicitly when
+      // requesting the last insert ID.
+      if ($options['return'] === Database::RETURN_INSERT_ID && isset($options['sequence_name'])) {
+        $last_insert_id = $this->connection->lastInsertId($options['sequence_name']);
       }
       else {
-        $last_insert_id = $this->connection->query($stmt, [], $options);
+        $last_insert_id = NULL;
       }
       $this->connection->releaseSavepoint();
     }

I think this needs to match the previous logic. And not overwrite the $last_insert_id when it is already set.

Like the comment is // Only use the returned last_insert_id if it is not already set. is still there but with the current logic it is not working.

Something like

    try {
      $stmt->execute(NULL, $options);
      // Only use the returned last_insert_id if it is not already set.
      // PostgreSQL requires the table name to be specified explicitly when
      // requesting the last insert ID.
      if (empty($last_insert_id) && ['return'] === Database::RETURN_INSERT_ID && isset($options['sequence_name'])) {
        $last_insert_id = $this->connection->lastInsertId($options['sequence_name']);
      }
      $this->connection->releaseSavepoint();
    }

I'm not sure about adding API to remove API. But also I'm wondering if it is really needed?

+++ b/core/lib/Drupal/Core/Database/Connection.php
@@ -836,7 +837,7 @@ public function query($query, array $args = [], $options = []) {
-          return $this->connection->lastInsertId($sequence_name);
+          return $this->lastInsertId($sequence_name);

This is the only place where the new method is used and I'm not sure it is needed. Is it really necessary to add Connection::lastInsertId() in this issue?

mondrake’s picture

Addressed #65 first suggestion, the rest I need to think and figure out. Personally I do not see the problem to have a mehod for getting last insert id, this would just put us on par with what, for instance, doctrine/dbal have.

We might think to have it on pgsql Connection only, since its Insert method needs now to access the lower level PDO connection, one way or another, to get the info that is no longer available since we are inserting by executing the statement and no longer Connection::query.

mondrake’s picture

alexpott’s picture

What we can do in postgres is do a query :) that's what return $this->connection->lastInsertId($sequence_name); does anyway - see https://github.com/php/php-src/blob/master/ext/pdo_pgsql/pgsql_driver.c#...

Looking at https://stackoverflow.com/questions/2944297/postgresql-function-for-last... - there's we can do something like SELECT currval('sequence_name');

Or maybe even better we can use the RETURNING id thing because we're constructing the query here to. And there we can avoid the extra query completely :)

mondrake’s picture

Status: Needs work » Needs review

#65 I stand by the new method; I'd rather go the other way round and convert also base, mysql and sqlite Insert classes to use the statement directly instead of ::query. That would potentially open the door to deprecate Database::RETURN_INSERT_ID and simplify ::handleQueryException since these cases

      if (substr($e->getCode(), -6, -3) == '23') {
        $exception = new IntegrityConstraintViolationException($message, $e->getCode(), $e);
      }

could be dealt on the Insert::execute methods only.

mondrake’s picture

#69

even better we can use the RETURNING id thing because we're constructing the query here to. And there we can avoid the extra query completely :)

that sounds promising and also a performance improvement.

IMHO #70 could be pursued anyway, though.

alexpott’s picture

@mondrake I think we should decouple the new API from deprecating.

The simplest thing to do here seems to be to do the SELECT currval('sequence_name'); in the postgres insert

We can then open a followup about deprecating Database::RETURN_INSERT_ID and replacing that with a new method on the connection object.

We can also open a follow-up to explore the possible performance improvements of using RETURNING id

mondrake’s picture

Assigned: Unassigned » mondrake
Status: Needs review » Needs work

OK, on that.

mondrake’s picture

Assigned: mondrake » Unassigned
Status: Needs work » Needs review
daffie’s picture

@mondrake: Maybe the problem is that there are to many quotes in the following line:

$last_insert_id = $this->query("SELECT currval('{$options['sequence_name']}')")->fetchField();
mondrake’s picture

Thanks @daffie it looks like I was just missing the connection.

daffie’s picture

Status: Needs review » Needs work

I have updated the CR.

Could we change the following code:

$last_insert_id = $this->connection->query("SELECT currval('{$options['sequence_name']}')")->fetchField();

to:

$sequence_name = $options['sequence_name'];
$last_insert_id = $this->connection->query("SELECT currval('$sequence_name')")->fetchField();

For better readability.
The rest of the patch is for me RTBC.

mondrake’s picture

Status: Needs work » Needs review

@daffie I see #77 more as a personal preference, but OK.

daffie’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

Updated the IS.

The patch looks good now.
For me it is RTBC.

@mondrake: Thank you.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

I tried leaving a code review on the merge request. Looking at the comment above we need to integrate this a bit better. Here's the a link to the comment - https://git.drupalcode.org/project/drupal/-/merge_requests/74#note_5067 and also this sets this back to needs work.

mondrake’s picture

Status: Needs work » Needs review
alexpott’s picture

Status: Needs review » Needs work

A further tidy up can be done.

mondrake’s picture

Status: Needs work » Needs review
mondrake’s picture

mondrake’s picture

Status: Needs review » Needs work

Need to add a deprecation trigger to the case a PDOStatement is passed in.

mondrake’s picture

Status: Needs work » Needs review
daffie’s picture

Status: Needs review » Needs work

Patch looks good. Just 2 minor remarks.

  1.    * @param string|\Drupal\Core\Database\StatementInterface|\PDOStatement $query
       *   The query to execute. This is a string containing an SQL query
       *   with placeholders.
       *   (deprecated) An already-prepared instance of StatementInterface may
       *   also be passed in order to allow calling code to manually bind
       *   variables to a query. If a StatementInterface is passed, the $args
       *   array will be ignored.
    

    We are not only deprecating StatementInterface input, but also PDOStatement input. Can we mentioning that in the docblock.

  2.       // We allow either a pre-bound statement object (deprecated) or a literal string.
    

    Nitpick: This line goes over the 80 character limit.

mondrake’s picture

Status: Needs work » Needs review

Thanks @daffie

mondrake’s picture

In a push earlier today I added deprecation comments not meant for this issue/MR, reverted.

daffie’s picture

Status: Needs review » Reviewed & tested by the community

The patch now looks great.
For me it is RTBC.

alexpott’s picture

I agree this patch looks good now - waiting for the test runs before committing. Noticed a couple of extra things that we need to file issues for:

  • \Drupal\Core\Database\Driver\pgsql\Upsert::execute() should return the last insert ID but atm it returns TRUE!
  • The return value docs for \Drupal\Core\Database\Query\Query::execute() leave quite a bit to be desired. What is return depends upon the query type.

I couldn't find an issue to the upsert one and I didn't look for the other one.

I assigned issue credit for patch review based on issue comments.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 6aab223 and pushed to 9.2.x. Thanks!

diff --git a/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
index 60cb01f247..c7b67f9fa0 100644
--- a/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
+++ b/core/lib/Drupal/Core/Database/Driver/pgsql/Insert.php
@@ -103,6 +103,7 @@ public function execute() {
       // requesting the last insert ID.
       if (!isset($last_insert_id)) {
         if ($this->queryOptions['return'] === Database::RETURN_INSERT_ID && $sequence_name !== NULL) {
+          // cspell:ignore currval
           $last_insert_id = $this->connection->query("SELECT currval('$sequence_name')")->fetchField();
         }
         else {

I added an ignore for currval for cspell as this failed the check.

  • alexpott committed 6aab223 on 9.2.x
    Issue #3137883 by mondrake, Pooja Ganjage, andypost, paulocs,...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

mradcliffe’s picture

Filed a follow-up issue #3210071: DatabaseExceptionWrapper has incorrect arguments in Insert as the arguments passed in this line, throw new DatabaseExceptionWrapper($message, 0, $e->getCode());, aren't correct.