Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure the pg_depend relation is for a table object #5800

Merged
merged 1 commit into from
Mar 2, 2023

Conversation

allenisalai
Copy link

@allenisalai allenisalai commented Oct 25, 2022

This fixes a bug where there was a pg_proc which was a dependency of an extension and also had an identical oid to the table class being described. Oids are not guaranteed to be unique because they will wrap around once they hit the unsigned integer max. The added conditional will ensure that the target object of the dependency is a table.

Q A
Type bug
Fixed issues #5781

Summary

See #5781 for an in depth description of the issue.

Since the issue itself it hard to replicate, I checked the backwards compatibility with #5668 (comment) which was the original cause of the change.

@derrabus
Copy link
Member

Thank you. Would it be possible to write a functional test for this issue?

@allenisalai
Copy link
Author

@derrabus There are 2 ways I can think to write a functional test for this. Let me know if you have any preference or other ideas.

  1. Write a script that will cause the oid wrap around. I have a script that attempted to do that, but it would take 2 days to run on my machine since we have to insert so many records. See below for an example of what I was trying. Maybe there's a more efficient way to load that many records.
  2. I can try to hack the pg_class table to force a conflict. Would that be a valid enough test? I not familiar with postgres internals to know what else I would have to change to ensure that it was a valid state but it seems pretty straight forward.
CREATE TABLE IF NOT EXISTS test(id INT NOT NULL);
CREATE EXTENSION IF NOT EXISTS pg_prewarm;
UPDATE pg_class SET oid = (
    SELECT objid 
    FROM pg_depend 
        JOIN pg_extension as ex on ex.oid = pg_depend.refobjid 
         WHERE ex.extname = 'pg_prewarm' 
         ORDER BY objid
         LIMIT 1
)
WHERE pg_class.relname='test';

Wrap around script

<?php

// touch empty_file.txt
// docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=Passw0rd -v $(pwd)/empty_file.txt:/tmp/empty_file.txt postgres:14.2-alpine

require 'vendor/autoload.php';

use Doctrine\DBAL\DriverManager;


$connection = DriverManager::getConnection([
    'driver' => 'pdo_pgsql',
    'host' => 'localhost',
    'user' => 'postgres',
    'password' => 'Passw0rd',
]);


$connection->executeStatement('CREATE TABLE IF NOT EXISTS oid_wrap (oid_number oid)');
$connection->executeStatement('CREATE EXTENSION IF NOT EXISTS pg_prewarm');

$procOid = $connection->fetchOne("
    SELECT objid 
    FROM pg_depend 
        JOIN pg_extension as ex on ex.oid = pg_depend.refobjid 
         WHERE ex.extname = 'pg_prewarm' 
         ORDER BY objid
         LIMIT 1"
);

echo sprintf("proc id: %s\n",$procOid);

$connection->executeStatement('CREATE TABLE IF NOT EXISTS abc (id INT NOT NULL)');
$tableOid = $connection->fetchOne("
    SELECT oid
    FROM pg_class
        WHERE relname='abc'
        LIMIT 1"
);
echo sprintf("Table id: %s\n", $tableOid);

$maxOidValue = 4294967295;
$totalObjsToInsert = $maxOidValue + $procOid - ($tableOid - $procOid); // 10000 + 16388 - (16391-16388)

echo sprintf("Objs To Insert: %s\n", $totalObjsToInsert);

$start = microtime(true);
while($totalObjsToInsert !== 0)
{
    $objsToInsert = 1000000;
    if ($totalObjsToInsert < 100000) {
        $objsToInsert = $totalObjsToInsert;
    }

    $connection->executeStatement("INSERT INTO oid_wrap 
            SELECT lo_import('/tmp/blob.txt')
            FROM generate_series(1, $objsToInsert)");
    
    $totalObjsToInsert -= $objsToInsert;
}
$end = microtime(true);

$connection->executeStatement('DROP TABLE IF EXISTS abc');
$connection->executeStatement('CREATE TABLE IF NOT EXISTS abc (id INT NOT NULL)');
$tableOid = $connection->fetchOne("
    SELECT oid
    FROM pg_class
        WHERE relname='abc'
        LIMIT 1"
);

echo sprintf("Time to run: %s\n", ($end - $start));
echo sprintf("Final Table id: %s\n", $tableOid);

@derrabus
Copy link
Member

Option 1 sounds like it's too slow to run that test in the CI. Can you give option 2 a try?

@derrabus derrabus changed the title Ensure the pg_depend relation is for a table object. Ensure the pg_depend relation is for a table object Oct 27, 2022
@allenisalai allenisalai force-pushed the psql-schema-manager-error branch 2 times, most recently from c60537d to 6fb7ab2 Compare November 2, 2022 20:30
@allenisalai
Copy link
Author

@derrabus I added the functional test you requested.

@allenisalai
Copy link
Author

Since psql 9.4 doesn't support setting the oid (https://github.com/doctrine/dbal/actions/runs/3380906837/jobs/5879984623#step:6:84) I mark the test as skipped for the 9.4 platform.

@allenisalai allenisalai force-pushed the psql-schema-manager-error branch 2 times, most recently from f183fcd to 6ddef91 Compare December 27, 2022 19:37
@klaussilveira
Copy link

Just checking the status on this. Would help us unlock the upgrade to the newest dbal version.

@derrabus
Copy link
Member

Just checking the status on this. Would help us unlock the upgrade to the newest dbal version.

Have you tested the changes in this PR? If you need this change, please don't query us for a status, but help us ship it.

@@ -561,6 +564,73 @@ public function testAlterTableAutoIncrementIntToBigInt(
self::assertTrue($tableFinal->getColumn('id')->getAutoincrement());
}

public function testListTableColumnsOidConflictWithNonTableObject(): void
{
$semanticVersion = $this->connection->fetchOne('SHOW SERVER_VERSION');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this work?

Suggested change
$semanticVersion = $this->connection->fetchOne('SHOW SERVER_VERSION');
$semanticVersion = $this->connection->getServerVersion();

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getServerVersion on the connection object is private. $this->connection->getWrappedConnection() has a public method, but is deprecated so I went with$this->connection->getNativeConnection()->getAttribute(PDO::ATTR_SERVER_VERSION).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getServerVersion on the connection object is private.

Indeed.

$this->connection->getWrappedConnection() has a public method, but is deprecated

Let's use that anyway. I'll have to adjust the call in 4.0.x, but that's fine.

so I went with$this->connection->getNativeConnection()->getAttribute(PDO::ATTR_SERVER_VERSION).

This will fail as soon as we merge your change to 3.6.x because we have a Postgres driver there that does not use PDO.

tests/Functional/Schema/PostgreSQLSchemaManagerTest.php Outdated Show resolved Hide resolved
Comment on lines 617 to 624
// revert to the original oid
$this->connection->executeStatement(
'UPDATE pg_attribute SET attrelid = ? WHERE attrelid = ?',
[$originalTableOid, $conflictingOid],
);
$this->connection->executeStatement(
'UPDATE pg_description SET objoid = ? WHERE objoid = ?',
[$originalTableOid, $conflictingOid],
);
$this->connection->executeStatement(
'UPDATE pg_class SET oid = ? WHERE oid = ?',
[$originalTableOid, $conflictingOid],
);

$this->connection->executeStatement(sprintf('DROP TABLE IF EXISTS %s', $table));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to change thing in the database after we have made our assertion?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I'm modifying the pg catalog directly, I wanted to put the original values back so that this wouldn't affect downstream tests. I tried wrapping the changes in a transaction but unfortunately that didn't work and pg threw an error when rolling back. Even if it doesn't cause problems now, if it does affect a test in the future I think it will be impossible to debug for whatever poor soul runs into it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, but that means, if the assertion fails, your cleanup won't run?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the assertion after undoing the database changes to account for this.

@allenisalai allenisalai force-pushed the psql-schema-manager-error branch 2 times, most recently from e97e9f9 to 7fd18a5 Compare February 1, 2023 00:44
@derrabus derrabus changed the base branch from 3.5.x to 3.6.x February 7, 2023 23:07
@allenisalai
Copy link
Author

FWIW I have had my fork running in production since the 8th based off of 3.5.3

$versionParts = [];
$wrappedConnection = $this->connection->getWrappedConnection();
assert($wrappedConnection instanceof ServerInfoAwareConnection);
assert(preg_match('/^(?P<major>\d+)/', $wrappedConnection->getServerVersion(), $versionParts) > 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're going to use $versionParts afterwards, we need to be sure the preg_match() is actually executed. Since assert()s can be disabled, you'll have to move that call out of the assert().

That being said, you might just use PHP's version_compare() function to check if we're on Postgres 10 or later.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if I implemented what you wanted. If assert can be disabled, should I throw an exception if $versionParts['major'] isn't set? That's what I was trying to accomplish with the original assert call.

@derrabus
Copy link
Member

FWIW I have had my fork running in production since the 8th based off of 3.5.3

I have no doubt that your change works. It's just that I need a proper test to make sure nobody's going to break it in the future. But I think we're close to merging the PR. 🤞🏻

Comment on lines 574 to 578
$matchCount = preg_match('/^(?P<major>\d+)/', $wrappedConnection->getServerVersion(), $versionParts);
assert($matchCount > 0);

$majorVersion = $versionParts['major'];
if (version_compare($majorVersion, '10', '<')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you understood my last comment about version_compare(). Have you tried if this works?

Suggested change
$matchCount = preg_match('/^(?P<major>\d+)/', $wrappedConnection->getServerVersion(), $versionParts);
assert($matchCount > 0);
$majorVersion = $versionParts['major'];
if (version_compare($majorVersion, '10', '<')) {
if (version_compare($wrappedConnection->getServerVersion(), '10.0', '<')) {

Using version_compare() is a bit pointless if we still have to parse the server version.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It worked. Thanks for the clarification.

This fixes a bug where there was a pg_proc which was a dependency of
an extention and also had an identical oid to the table class being
described.  Oids are not guaranteed to be unique because they will
wrap around once they hit the unsigned integer max.  The added
conditional will ensure that the target object of the dependency is a
table.

Fixes: doctrine#5781
@derrabus derrabus added this to the 3.6.1 milestone Mar 2, 2023
@derrabus derrabus merged commit 974a5ab into doctrine:3.6.x Mar 2, 2023
@allenisalai allenisalai deleted the psql-schema-manager-error branch March 2, 2023 19:39
@allenisalai
Copy link
Author

Thanks for your time and patience @derrabus

Comment on lines +571 to +573
if (version_compare($wrappedConnection->getServerVersion(), '10.0', '<')) {
self::markTestSkipped('Manually setting the Oid is not supported in 9.4');
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears we need to skip that test on Postgres 10 and 11 as well. #5955

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:( Sorry about that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, my comment was just FYI. 🙂

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 7, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants