Problem/Motivation

   * @return int|null
   *   The number of rows matched by the update query. This includes rows that
   *   actually didn't have to be updated because the values didn't change

Looks like this function never returns null. So the return type should be just int or more precise non-negative-int.

Proposed resolution

Update the return type.

Issue fork drupal-3567868

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:

Comments

chi created an issue. See original summary.

farzana.farook made their first commit to this issue’s fork.

mondrake’s picture

IMHO we should not rely on the int value returned here. Only return TRUE or fail with an exception. The row count returned by different drivers is not consistent. See similar point for the UPSERT operation in #2547493-33: Add support for unique / primary key constraints composed of multiple fields for Upsert queries.

farzana.farook’s picture

Status: Active » Needs review

I’ve updated the return type and opened an MR for this documentation fix.

smustgrave’s picture

Status: Needs review » Needs work

Believe your local may have been on 11.3 not 11.x, you may need to rebase your local branch and push that. MR is showing 1000s of changes

chi’s picture

Re: #4 Not knowing how many records have been updated will make the method useless in some cases.

mondrake’s picture

Retracted, see #11

#7 IMHO in a concurrent db that information returned here should be avoided as much as possible, given that rows affected (= the rows that the UPDATE conditions would return), and rows changed (= the affected rows actually changed) numbers may be impacted by concurrent operations.

If knowing the precise number is absolutely necessary, then locking should be used, in pseudo:

START TRANSACTION
SELECT ... WHERE {update conditions} ... FOR UPDATE
count the result, that gives a reliable number
UPDATE ... WHERE {update conditions}
COMMIT
chi’s picture

I am thinking of DatabaseQueue which is currently use optimistic update.

$update = $this->connection->update(static::TABLE_NAME)
  ->fields([
    'expire' => \Drupal::time()->getCurrentTime() + $lease_time,
  ])
  ->condition('item_id', $item->item_id)
  ->condition('expire', 0);
// If there are affected rows, this update succeeded.
if ($update->execute()) {
  $item->data = unserialize($item->data);
  return $item;
}
mondrake’s picture

Sorry I made a mistake in #8, will retract and rewrite it below.

Anyway: affected rows returns in Drupal the rows matching the condition, and not necessarily the changed ones. So I am not sure what #9 is actually doing.

mondrake’s picture

#7 IMHO in a concurrent db that information returned here should be avoided as much as possible, given that rows matched (= the rows that the UPDATE conditions would return), and rows changed (= the matched rows actually changed) numbers may be impacted by concurrent operations.

Also as per #3248575: Documentation conflict re: affected rows vs. matched rows, Drupal MySQL driver sets

      // Make sure MySQL returns all matched rows on update queries including
      // rows that actually didn't have to be updated because the values didn't
      // change. This matches common behavior among other database systems.
      // @phpstan-ignore class.notFound
      (\PHP_VERSION_ID < 80400 ? \PDO::MYSQL_ATTR_FOUND_ROWS : Mysql::ATTR_FOUND_ROWS) => TRUE,

which makes me suspect PgSQL and SQLite would have the same default behaviour, then.

If knowing the precise number is absolutely necessary, then locking should be used, in pseudo:

START TRANSACTION
SELECT ... WHERE {update conditions} ... FOR UPDATE
count the result, that gives a reliable number
UPDATE ... WHERE {update conditions}
COMMIT

where {update conditions} should be set in a way to ensure matched rows=changed rows.

chi’s picture

affected rows returns in Drupal the rows matching the condition, and not necessarily the changed ones

->fields(['expire' => \Drupal::time()->getCurrentTime() + $lease_time]

plus

 ->condition('expire', 0);

Together will always change these matching rows.

chi’s picture

As I understood the \Drupal\Core\Queue\DatabaseQueue::claimItem relies on the UPDATE return value to figure out whether the item was successfully claimed.

mondrake’s picture

Postgres seems to return matched rows since 9.0 => https://www.php.net/manual/en/function.pg-affected-rows.php

mondrake’s picture

#13 OK, but then a boolean is enough in this case, right? As long as it's not 0 we don't really need to know the exact number.

mondrake’s picture

SQLite viceversa seems to return the changed rows and not the matched ones => https://www.php.net/manual/en/sqlite3.changes.php

chi’s picture

numbers may be impacted by concurrent operations

Could you please direct me to the documentation for that? I couldn't find it in the PostgreSQL docs.

mondrake’s picture

MR!14392 is a proposal to return a bool.

mondrake’s picture

Status: Needs work » Needs review
chi’s picture

@modrake, how would casting the return value to boolean make the result safer? If we cannot trust the number returned from UPDATE statement then we cannot trust its derivative (boolean).

Still wonder why you consider the return value unsafe. I suppose we just need to ensure behavior consistency across drivers ("matching" vs "changed"). And apparently "matching" is default behavior in all supported drivers.

The relevant Postgres documentation.

The count is the number of rows updated, including matched rows whose values did not change.

mondrake’s picture

I spot checked usage of update across core's code base, and AFAICS the value returned by ::execute is

  • not assigned to a variable (most common case), or
  • explicitly cast to boolean, or
  • when checked in an if, checked in a boolean way i.e. that its value is non-zero

#21 yes casting to boolean is not safer per se, but in practice it does not look like the actual rowcount is used for its int value, apart in the update tests themselves.

I stand by #4. In fact IMHO for all of UPDATE, UPSERT and DELETE operations, what we should assume is that if the execution is not failing, then all the records identified by the conditions are eventually affected by the operation itself; trying to interpret the various row-affected or info internals seems to me not reliable (e.g. see the UPSERT related issue, or think someone in MySql set Mysql::ATTR_FOUND_ROWS to false: then if the row is found but not changed, update will return 0 or FALSE which is not what Drupal would expect, and in fact the row would be set to the values indicated by UPDATE).

chi’s picture

I spot checked usage of update across core's code base

I suppose contrib and custom projects may widely use it. So it would a BC break.

casting to boolean is not safer per se

If casting is not safe why not just return nothing? Make the method void and advise using SELECT FOR UPDATE statement as workaround.

mondrake’s picture

If casting is not safe why not just return nothing? Make the method void and advise using SELECT FOR UPDATE statement as workaround.

Fully agree. That should be the end game. BC is in the way, of course.

mondrake changed the visibility of the branch 3567868-bool to hidden.

mondrake’s picture

mondrake’s picture

Status: Needs review » Needs work

NW for #6

andypost’s picture

Version: 11.3.x-dev » main

farzana.farook changed the visibility of the branch 3567868- to hidden.

farzana.farook changed the visibility of the branch 3567868-bool to active.

farzana.farook changed the visibility of the branch 3567868-bool to hidden.

farzana.farook’s picture

.

quietone changed the visibility of the branch 3567868- to active.

quietone’s picture

Version: 11.3.x-dev » main

Yes, this should be on main and so should the MR.

farzana.farook’s picture

Status: Needs work » Needs review
smustgrave’s picture

Status: Needs review » Needs work

Appears to have a phpstan error. Most likely the baseline needs to be updated. Believe the artifact can be retrieved directly from the MR

farzana.farook’s picture

Status: Needs work » Needs review

Updated the phpstan baseline using the artifacts from the MR.

smustgrave’s picture

@mondrake or @chi thoughts on what's in the MR?

chi’s picture

Status: Needs review » Needs work

There is another documentation bug that caused new ignores in PhpStan configuration.

\Drupal\Core\Database\ExceptionHandler::handleExecutionException should have return type never, not void.

sivaji_ganesh_jojodae made their first commit to this issue’s fork.

mradcliffe’s picture

Issue tags: -Novice

I am removing the Novice tag from this issue because I am not sure what the next task is for a new contributor.

I’m using this documentation as a source: https://www.drupal.org/community/contributor-guide/task/triage-novice-is...

sivaji_ganesh_jojodae’s picture

Status: Needs work » Needs review

Updated the return type for ExceptionHandler::handleExecutionException() as per #39.

Removed outdated ignore rules from phpstan-baseline.php that are no longer triggered, fixing ignore.unmatched errors in the pipeline.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.