Problem/Motivation

SchemaTest::testSchema fails if a driver implements Sqlite support but is not the Drupal core driver, 'cause

      case 'sqlite':
        // For SQLite we need access to the protected
        // \Drupal\Core\Database\Driver\sqlite\Schema::introspectSchema() method
        // because we have no other way of getting the table prefixes needed for
        // running a straight PRAGMA query.
        $schema_object = Database::getConnection()->schema();
        $reflection = new \ReflectionMethod($schema_object, 'introspectSchema');
        $reflection->setAccessible(TRUE);

assumes an introspectSchema method exists, but it may not be the case.

Also, per parent issue #2877583: [Meta] Remove database specific logic from core it is desirable not to have database specific conditionals in core code.

Proposed resolution

Add a Schema::findPrimaryKeyColumns in the abstract class and its driver implementations, and call that method from the relevant test.

Remaining tasks

Review.

User interface changes

None

API changes

An additional abstract method in the abstract Schema class + driver level implementations.

Data model changes

None

CommentFileSizeAuthor
#57 2881522-57.patch17.16 KBtstoeckler
#57 2881522-41-57-interdiff.txt11.5 KBtstoeckler
#41 interdiff_36-41.txt832 bytesmondrake
#41 2881522-41.patch11.87 KBmondrake
#36 2881522-36.patch11.66 KBtstoeckler
#36 2881522-30-36-interdiff.txt1.19 KBtstoeckler
#30 2881522-28-30-interdiff.txt4.07 KBdaffie
#30 2881522-30.patch12 KBdaffie
#29 2881522-29.patch12 KBdaffie
#29 2881522-28-29-interdiff.txt4.07 KBdaffie
#28 2881522-28.patch10.16 KBtstoeckler
#28 2881522-24-28-interdiff.txt1.4 KBtstoeckler
#24 2881522-24.patch9.77 KBtstoeckler
#24 2881522-20-24-interdiff.txt2.35 KBtstoeckler
#20 2881522-20.patch9.64 KBmondrake
#20 interdiff_17-20.txt735 bytesmondrake
#18 interdiff_16-17.txt487 bytesmondrake
#18 2881522-17.patch9.2 KBmondrake
#16 2881522-16.patch9.19 KBmondrake
#16 interdiff_12-16.txt2.71 KBmondrake
#12 2881522-12.patch7.88 KBmondrake
#12 interdiff_8-12.txt821 bytesmondrake
#8 2881522-8.patch7.69 KBmondrake
#8 interdiff_2-8.txt7.67 KBmondrake
#2 2881522-2.patch6.27 KBmondrake
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

mondrake created an issue. See original summary.

mondrake’s picture

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

daffie’s picture

Status: Needs review » Needs work

I like the idea for this issue. We should do the same with unique and non-unique indexes.
The patch looks good to me.
I am missing testing for the table does not exists and testing for a table without a primary key.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

tstoeckler’s picture

Patch looks great! @mondrake can you add the tests requested by #4, then I could RTBC ;-)

I think if we want to do the same for unique keys it should be a new issue.

mondrake’s picture

Assigned: Unassigned » mondrake

I'll work on that. I also suggest to use a different method name e.g. findPrimaryKeyColumns instead of getPrimaryKeyColumns to reflect the fact that this is introspecting actual status from the db - a similar method to introspect the existing tables is named Schema::findTables.

mondrake’s picture

Title: Add a Schema::getPrimaryKeyColumns method to remove database specific logic from test » Add a Schema::findPrimaryKeyColumns method to remove database specific logic from test
Assigned: mondrake » Unassigned
Issue summary: View changes
Status: Needs work » Needs review
FileSize
7.67 KB
7.69 KB

Renamed the method and added tests.

tstoeckler’s picture

Status: Needs review » Reviewed & tested by the community

Awesome, looks great to me! Thanks.

mondrake’s picture

I thought this may need a CR so I drafted one: https://www.drupal.org/node/2974956

amateescu’s picture

The patch looks good to me as well, I just found two tiny nitpcks :)

  1. +++ b/core/lib/Drupal/Core/Database/Schema.php
    @@ -408,6 +408,18 @@ public function fieldExists($table, $column) {
    +   * @param $table
    

    Missing the 'string' type hint.

  2. +++ b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
    @@ -791,6 +793,45 @@ protected function assertFieldChange($old_spec, $new_spec, $test_data = NULL) {
    +   * Tests the Schema::findPrimaryKeyColumns method.
    

    We could change this to @covers \Drupal\Core\Database\Schema::findPrimaryKeyColumns.

mondrake’s picture

#11:

1. Correct, but out of scope of this patch. Actually most of the methods are missing the typehint in this test class.

2. Done, albeit a bit differently.

daffie’s picture

Status: Reviewed & tested by the community » Needs work

Could we add testing with more complicated scenario's. Like:

  1. A table with a primary key that consist of multiple columns.
  2. A table where the first column of the table is not be part of primary key.
  3. In combination with adding/removing/changing column(s) from an existing table. And then testing its primary key.

The patch looks good to me. Great work!

tstoeckler’s picture

Re #13:

In combination with adding/removing/changing column(s) from an existing table. And then testing its primary key.

Let's leave this out for now as this is currently broken on MariaDB and also inconsistent between MySQL/SQLite on the on hand and PostgreSQL on the other hand. See #2974722: Fix dropping of columns in a composite primary key on MariaDB >=10.2.8 and make it consistent across database drivers and the latest comments in #2938951: The primary key is not correctly re-created when updating the storage definition of an identifier field for more information. I think we should expand the former issue to be "Fix dropping of columns in a composite primary key on MariaDB and make it consistent across database drivers" and then that one can include that coverage (and be blocked on this one).

mondrake’s picture

Status: Needs work » Reviewed & tested by the community

I agree, let's have #13 in follow ups, this issue is already kind of blocker for the ones mentioned in #14.

Actually #13.1 is already covered in the testSchema method.

mondrake’s picture

OK, actually added some more tests for #13.1 and #13.2; #13.3 let's leave for follow up as suggested by @tstoeckler.

amateescu’s picture

Re #12:

1. Correct, but out of scope of this patch. Actually most of the methods are missing the typehint in this test class.

That's not quite true, it's very much in the scope of this patch :) Since it is adding new code, it should conform to the coding standards regardless of the surrounding code.

mondrake’s picture

mondrake’s picture

Status: Needs review » Needs work

It looks like additional tests are now spotting an issue in SQLite and Postgres... :(

If the sequence of columns composing the PK is different than the flow of columns on the table left-to-right, the queries do not take that into account.

I can investigate the SQLite one, but have no clue on pgsql... anybody can help?

mondrake’s picture

This should fix the SQLite one.

tstoeckler’s picture

Hehe, I kind of noticed that already in #2881522: Add a Schema::findPrimaryKeyColumns method to remove database specific logic from test but didn't realize I did. I just added a bunch of sort()s in the respective assertions and was done with it. You are right, though, that we should fix this properly ;-) Thanks! And thanks @daffie for insisting on the added tests! Will try to look at Postgres one later (unless someone beats me to it)

daffie’s picture

@tstoeckler: The honour should go to @wimleers. He is the one with his api-first initiative that we should not only test the "easy path". So we should do the same here. :)

mondrake’s picture

Status: Needs review » Needs work
tstoeckler’s picture

Alright, this should pass in all three database engines now.

For SQLite, #20 was pretty much on the right track, except you fixed the issue for normal indexes not for the primary key. So I adapted your approach for the primary key and it worked. I had to use an explicit ksort() in PHP there, as we cannot sort in SQL directly because we do not want to alter the order of the fields and the primary key information is attached to them directly. I also reverted the interdiff from #20. I think it's correct to retrieve the indexes in the specified order, but if we make that change here we should also add test coverage so I'd rather leave that for a separate issue.

For PostgreSQL, the only place I could find where the order of the primary key is stored is in the i.indkey array column in the order of the array itself. Which the array_position() function we can then extract the correct key for sorting. Since we needed that anyway, I altered the query a bit so that the return value is keyed by the position directly so that we don't need the explicit array_keys() in PHP. I removed the data_type field, but we weren't using that anyway, that was just copied from https://wiki.postgresql.org/wiki/Retrieve_primary_key_columns I guess (but due to the ordering we diverge from that query anyway...)

I just noticed that this is marked against 8.6.x, but this now blocks a number of bugs, so is there a chance we can backport this to 8.5.x?

mondrake’s picture

For SQLite, #20 was pretty much on the right track, except you fixed the issue for normal indexes not for the primary key. So I adapted your approach for the primary key and it worked. I had to use an explicit ksort() in PHP there, as we cannot sort in SQL directly because we do not want to alter the order of the fields and the primary key information is attached to them directly. I also reverted the interdiff from #20. I think it's correct to retrieve the indexes in the specified order, but if we make that change here we should also add test coverage so I'd rather leave that for a separate issue.

Excellent @tstoeckler, very clean - I was getting into complicated stuff but your solution is simple and elegant. Also fully agree on reverting #20 here, it was just my mistake. RTBC++ if it comes back green, but I cannot set it myself.

mondrake’s picture

I just noticed that this is marked against 8.6.x, but this now blocks a number of bugs, so is there a chance we can backport this to 8.5.x?

Custom/contrib db drivers would fail in that case because of the new abstract method in Schema that has to be implemented - so I think this only for a minor.

mondrake’s picture

Status: Needs review » Needs work

Still issues with Postgres

tstoeckler’s picture

Apparently array_position() is only PostgreSQL >=9.5 :-(

So I went ahead and re-implemented the ordering in PHP. Since the order is only available (as far as I can tell) in array form, which gets into PHP as a concatenated array, we have to do some annoying string futzing for that. Please feel free to suggest a succincter or more readable version if you can come up with one ;-)

daffie’s picture

In the drivers for PostgreSQL and SQLite are sorting methods added. I have added a test with a more complicated primary key to see if the one test with a multiple primary key is not a lucky pass. I also added some comments to the tests.

daffie’s picture

tstoeckler’s picture

Thanks @daffie! The test also looks more readable now. Assuming the tests come back green, maybe we can get an RTBC here? ;-)

The last submitted patch, 29: 2881522-29.patch, failed testing. View results

daffie’s picture

Status: Needs review » Reviewed & tested by the community

Thanks @daffie! The test also looks more readable now.

Taking this as given my tests addition as good enough for RTBC.

The patch looks good.
There is enough testing for the problem.
It is RTBC for me.

There is a follow-up: #2974722: Fix dropping of columns in a composite primary key on MariaDB >=10.2.8 and make it consistent across database drivers.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Schema.php
@@ -502,13 +502,15 @@ protected function introspectSchema($table) {
-          $schema['primary key'][] = $row->name;
+          $schema['primary key'][$row->pk - 1] = $row->name;

$row->pk - 1 needs a comment. Since this looks pretty odd. I think we need to document that $row->pk contains a number that reflects the primary key order. The number starts from 1 (i guess) so we subtract 1 to make it a zero indexed array. Given that we're doing

ksort($schema['primary key']);

later, I think we should do what we do for postgres and do

ksort($schema['primary key']);
$schema['primary key'] = array_values($schema['primary key']);

And then we don't have to do -1 subtracting.

It feels awkward to be adding API for tests-only but having #2974722: Fix dropping of columns in a composite primary key on MariaDB >=10.2.8 and make it consistent across database drivers following is good because that will introduce a usage in non-test code.

tstoeckler’s picture

I think we need to document that $row->pk contains a number that reflects the primary key order. The number starts from 1 (i guess) so we subtract 1 to make it a zero indexed array.

Yup, that's exactly right. Will fix according to your suggestion.

tstoeckler’s picture

amateescu’s picture

Status: Needs review » Reviewed & tested by the community

@alexpott's review has been addressed, back to RBTC :)

daffie’s picture

He, I wanted to do that! ;-)
+1 for RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review

Sorry, I've had another look at the patch and I have a BC concern.

+++ b/core/lib/Drupal/Core/Database/Schema.php
@@ -408,6 +408,18 @@ public function fieldExists($table, $column) {
+  /**
+   * Finds the primary key columns of a table, from the database.
+   *
+   * @param string $table
+   *   The name of the table.
+   *
+   * @return string[]|false
+   *   A simple array with the names of the columns composing the table's
+   *   primary key, or FALSE if the table does not exist.
+   */
+  abstract public function findPrimaryKeyColumns($table);

This is going to break contributed database drivers. I think we need to think about this a bit more carefully for BC purposes.

mondrake’s picture

One possibility on top of my head would be to change this abstract into a base method that throws an exception - then core drivers would all override it with their implementations. At that stage it would be OK also for contrib drivers that may or may not implement the override, nothing will fatal out because of the missing implementation. That's a common pattern e.g. in Doctrine DBAL.

The problem will arise when this method will start to be used in non-test core code - calls to the method should be enveloped in try/catch blocks and logic be developed to manage the case of missing implementation.

mondrake’s picture

mondrake’s picture

#41: SQLite is currently failing in HEAD, too.

daffie’s picture

Status: Needs review » Reviewed & tested by the community

I think this is the best solution to minimise the BC break. Back to RTBC.

mondrake’s picture

I do not know of ways to test the change in #41 in DrupalCI. But if anyone interested, here's the test run on TravisCI for a non-core db driver in the scenario where findPrimaryKeysColumn override implementation is missing. If you click on any of the tests with a red X, you will see the RuntimeException being thrown. https://travis-ci.org/mondrake/drudbal/builds/386647348

daffie’s picture

You could write a Unittest that mocks Connection::driver() to a not by Drupal core supported driver. Calling Schema::findPrimaryKeyColumns() should then result in your RuntimeException being thrown.

alexpott credited catch.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

Discussed with @catch in #contribute. We agreed that throwing the exception is the right way to go but...

+++ b/core/lib/Drupal/Core/Database/Schema.php
@@ -408,6 +408,23 @@ public function fieldExists($table, $column) {
+  /**
+   * Finds the primary key columns of a table, from the database.
+   *
+   * @param string $table
+   *   The name of the table.
+   *
+   * @return string[]|false
+   *   A simple array with the names of the columns composing the table's
+   *   primary key, or FALSE if the table does not exist.
+   *
+   * @throws \RuntimeException
+   *   If the driver does not override this method.
+   */
+  public function findPrimaryKeyColumns($table) {
+    throw new \RuntimeException("The '" . $this->connection->driver() . "' database driver does not implement " . __METHOD__);
+  }

We need to document that all non-test usages of this method MUST catch this exception and do the most sensible thing if this not available including continuing with old buggy behaviour because in Drupal 8 we can not guarantee this functionality is available.

Also in reviewing the code we can do the FALSE return when the table does not exist in this implementation.

tstoeckler’s picture

all non-test usages of this method MUST catch this exception and do the most sensible thing if this not available including continuing with old buggy behaviour

Can you elaborate on this? Even though practically speaking the current lack of this functionality is really an oversight, technically speaking we are adding new functionality here. The chain of issues makes very clear that there really is no way around adding this, but still there will be no current code that is using this function. So I'm not sure what you mean by "continuing with old buggy behavior"? While wrapping each usage of this method in a try-catch is anything from elegant, I'm not actually talking about elegance here. I just really don't understand what to do in such a case. At the end of the day this would mean that even with this patch we are not providing a proper API to fetch the primary key because you still need to somehow deal with the case where you don't know what the primary key is.

That is fine with me, I think in actual real life use-cases - similar to the code in Entity API in the related issues - what you actually want to do is make sure the primary key has a certain state, so instead of asking for the current primary key, it's easy enough to just recreate it to the desired state. I just thought up to this point that the scope of the issue was a different one and that we were actually adding an API function here.

But I guess that means we should mark the function as @internal?

Also in reviewing the code we can do the FALSE return when the table does not exist in this implementation.

Nice catch, good idea!

alexpott’s picture

@tstoeckler if the calling code does not catch the exception and do something sensible - which might be continuing with the buggy present behaviour - then this is a breaking change. Code that didn't halt execution now will and given this exception will occur during low level entity schema operations that might be even worse than the current behaviour.

tstoeckler’s picture

Sorry, I'm still not getting it.

which might be continuing with the buggy present behaviour

What behavior specifically? As I stated above the method cannot be called right now, so I'm not sure what you mean by that.

alexpott’s picture

So when #2974722: Fix dropping of columns in a composite primary key on MariaDB >=10.2.8 and make it consistent across database drivers and #2976493: Creating a table with an explicitly empty primary key is broken on PostgreSQL are fixed and use this new method they have to catch the exception rather than just allowing the exception to surface to the user.

tstoeckler’s picture

So #2976493: Creating a table with an explicitly empty primary key is broken on PostgreSQL doesn't actually use the new method outside of the tests.

#2974722: Fix dropping of columns in a composite primary key on MariaDB >=10.2.8 and make it consistent across database drivers does use it, but it's a special case: it's using it inside of the database drivers themselves. So there's no need to add a try-catch as the drivers themselves can be sure that it will never be thrown.

What I thought what we were talking about is code outside of tests and outside of the drivers.

alexpott’s picture

Well #52 really makes me question the wisdom of this public API addition then. Since there is no requirement for the addition to the public API. If we decide we want to add this then we do need to add a disclaimer on Schema::findPrimaryKeyColumns() that if this used outside of tests or the drivers themselves then the exception MUST be caught because we cannot guarantee that it is implemented by contrib/custom database drivers in Drupal 8.

tstoeckler’s picture

So do you think it would be sufficient to declare it @internal? I think that matches pretty well with only using it in tests and in the drivers themselves (and we can also document it, in this case).

The question is then just whether remove the abstract base method entirely and consider it an "implementation detail" of each of the three database drivers in core or whether to keep throwing the exception. I would prefer the latter, but more than anything I would love for this to land, so I'll pretty much comply with anything you and @catch can agree on ;-)

daffie’s picture

If we are going to make the method Schema::findPrimaryKeyColumns() @internal, are we then going to make it a protected method instead of public?

alexpott’s picture

I'd be +1 on making it protected and using reflection in tests to get access to it and having a default implementation that throws an exception. That way:

  • when contributed db drivers run a test and it fails for this they get a great message.
  • we don't increase the public API unnecessarily
  • contrib/custom are still encouraged to implement but know that not implementing won't break a thing.
tstoeckler’s picture

daffie’s picture

Status: Needs review » Reviewed & tested by the community

It all looks good to me. This is what @alexpott would like to happen.

I have only 1 minor documentation point, that can be solved on submit:

+++ b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
@@ -25,6 +27,8 @@ class SchemaTest extends KernelTestBase {
   public function testSchema() {
+    $schema = Database::getConnection()->schema();

Add between those 2 lines of code the following documentation:

/** @var \Drupal\Core\Database\Schema $schema */

Back to RTBC.

alexpott’s picture

@daffie that's not necessary - \Drupal\Core\Database\Connection::schema() already provides the typehint.

+++ b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
@@ -791,6 +799,126 @@ protected function assertFieldChange($old_spec, $new_spec, $test_data = NULL) {
+    /** @var \Drupal\Core\Database\Schema $schema */
+    $schema = Database::getConnection()->schema();

In fact the typehint here is not necessary.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 3edde1f and pushed to 8.6.x. Thanks!

diff --git a/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
index 4107cb7aa2..278d75635d 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/SchemaTest.php
@@ -803,7 +803,6 @@ protected function assertFieldChange($old_spec, $new_spec, $test_data = NULL) {
    * @covers ::findPrimaryKeyColumns
    */
   public function testFindPrimaryKeyColumns() {
-    /** @var \Drupal\Core\Database\Schema $schema */
     $schema = Database::getConnection()->schema();
     $method = new \ReflectionMethod(get_class($schema), 'findPrimaryKeyColumns');
     $method->setAccessible(TRUE);

Fixed on commit.

  • alexpott committed 3edde1f on 8.6.x
    Issue #2881522 by mondrake, tstoeckler, daffie, alexpott, amateescu,...
daffie’s picture

@alexpott: Thank you for the explanation.

Status: Fixed » Closed (fixed)

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