Skip to content

Conversation

@amotl
Copy link
Member

@amotl amotl commented Nov 24, 2022

About

Doctrine DBAL 3.0 was released on November 17, 2020.

Details

The major theme of DBAL 3.0 is the decoupling from PDO. Instead of copying the API verbatim like DBAL 2.0 did, DBAL 3.0 grows it into a better, more usable direction.

-- https://www.doctrine-project.org/2020/11/17/dbal-3.0.0.html

@amotl amotl force-pushed the amo/doctrine3 branch 3 times, most recently from c0dddcc to f1a7a40 Compare May 5, 2023 16:50
@amotl
Copy link
Member Author

amotl commented May 5, 2023

Other than many deprecation warnings, because crate/crate-pdo#146 has not been merged and released yet, running the tests here 1 raise this fatal error 2:

Fatal error: Declaration of Crate\PDO\PDO::prepare($statement, $options = null) must be compatible
with Doctrine\DBAL\Driver\Connection::prepare(string $sql): Doctrine\DBAL\Driver\Statement
in /path/to/crate-dbal/vendor/crate/crate-pdo/src/Crate/PDO/PDO.php on line 193

Footnotes

  1. composer run test

  2. https://github.com/crate/crate-dbal/actions/runs/4895679362/jobs/8741560882?pr=122#step:8:18

@amotl
Copy link
Member Author

amotl commented May 5, 2023

It will make no sense to continue working on this before crate/crate-pdo#146 has been merged. Otherwise, there are too many type errors to work around.

In other words, fa8ef49 should be removed again.

@ricohumme
Copy link

Hi @amotl I came across this and was wondering if this will be merge soon now that PR holding you back is merged?

@amotl
Copy link
Member Author

amotl commented Apr 13, 2024

Dear Rico,

thank you for writing in. I am not sure if this patch in its current state is enough to add support for Doctrine 3. Is it?

now that PR holding you back is merged?

because crate/crate-pdo#146 has not been merged and released yet
-- #122 (comment)

Ah, I see. Yeah, this would probably need a refresh. Thanks for the reminder!

With kind regards,
Andreas.

@andythedandy
Copy link

Hi @amotl,
is there something I can support with to get this PR going? (As I would need that ^^)

regards,
Andy

@amotl amotl self-assigned this Feb 2, 2025
@amotl
Copy link
Member Author

amotl commented Feb 12, 2025

Hi @andythedandy. Thank you for writing in and for offering support. This patch certainly needs more love to satisfy CI. Would you be able to spend a few cycles on it?

On the other hand, GH-136 by @JulianMar may also be very relevant in this regard, and could make this patch obsolete, by skipping support for Doctrine 3 altogether.

@amotl amotl added help wanted important Not a bug, still important. dependencies Something about dependencies. labels Feb 12, 2025
@amotl amotl removed their assignment Jun 19, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 13, 2025

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

📥 Commits

Reviewing files that changed from the base of the PR and between ee38c2f and cf79b2f.

📒 Files selected for processing (5)
  • src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php (2 hunks)
  • src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php (3 hunks)
  • src/Crate/DBAL/Platforms/Keywords/CrateKeywords.php (2 hunks)
  • src/Crate/DBAL/Types/TimestampType.php (5 hunks)
  • test/Crate/Test/DBAL/Platforms/CratePlatformTest.php (14 hunks)
 ___________________________
< I refactor in bunny hops. >
 ---------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

Walkthrough

Upgrades support from Doctrine DBAL 2 to DBAL 3. Refactors core driver classes to implement new DBAL 3 interfaces. Replaces inheritance-based statement implementation with composition. Adds exception converter and result wrapper classes. Updates type hints across platform and type classes. Modifies all functional tests to use new DBAL 3 APIs and execution methods.

Changes

Cohort / File(s) Summary
Build & Configuration
composer.json, .php-cs-fixer.php, .gitignore, CHANGES.txt
Updated doctrine/dbal from ^2 to ^3; replaced PHP CodeSniffer with PHP-CS-Fixer; added .php-cs-fixer configuration; documented DBAL 3 support.
Core Driver Implementation
src/Crate/DBAL/Driver/PDOCrate/Driver.php
Refactored to implement only \Doctrine\DBAL\Driver; updated method signatures for DBAL 3 compatibility; added getExceptionConverter() method; made VERSION and NAME constants public.
Statement & Connection
src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php, src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php
Changed CrateStatement from inheritance to composition pattern; replaced PDOConnection inheritance with Connection interface implementation; added exception handling via ExceptionConverter.
Result & Exception Handling
src/Crate/DBAL/Driver/PDOCrate/Result.php, src/Crate/DBAL/Driver/PDOCrate/ExceptionConverter.php
Created new Result class implementing Doctrine\DBAL\Driver\Result; created ExceptionConverter implementing ExceptionConverterInterface with error code mappings.
Platform Classes
src/Crate/DBAL/Platforms/CratePlatform.php, src/Crate/DBAL/Platforms/CratePlatform1.php, src/Crate/DBAL/Platforms/CratePlatform4.php, src/Crate/DBAL/Platforms/Keywords/CrateKeywords.php
Added return type hints to methods; made platform constants public; added getCurrentDatabaseExpression() method; updated type mappings and SQL generation.
Schema Management
src/Crate/DBAL/Schema/CrateSchemaManager.php
Updated to extend generic AbstractSchemaManager<CratePlatform>; renamed parameters; added type hints; refactored internal method signatures.
Type Classes
src/Crate/DBAL/Types/ArrayType.php, src/Crate/DBAL/Types/MapType.php, src/Crate/DBAL/Types/TimestampType.php
Made type constants public; added return type hints (mixed, string) to conversion and declaration methods; updated exception annotations.
Functional Tests (Base)
test/Crate/Test/DBAL/DBALFunctionalTest.php
Renamed from DBALFunctionalTestCase to DBALFunctionalTest; refactored helper methods to use DBAL 3 APIs; added result types.
Functional Tests (Execution)
test/Crate/Test/DBAL/Functional/BindingTest.php, test/Crate/Test/DBAL/Functional/ConnectionTest.php, test/Crate/Test/DBAL/Functional/NamedParametersTest.php, test/Crate/Test/DBAL/Functional/WriteTest.php
Updated to extend DBALFunctionalTest; migrated from execute() to executeQuery() and executeStatement(); updated result fetching methods.
Functional Tests (Schema & Data)
test/Crate/Test/DBAL/Functional/DataAccessTest.php, test/Crate/Test/DBAL/Functional/ModifyLimitQueryTest.php, test/Crate/Test/DBAL/Functional/Schema/SchemaManagerTest.php, test/Crate/Test/DBAL/Functional/TableOptionsTest.php, test/Crate/Test/DBAL/Functional/Types/MapTypeTest.php
Updated test inheritance; replaced getSchemaManager() with createSchemaManager(); migrated to Types constants; updated table construction parameters.
Platform & Type Tests
test/Crate/Test/DBAL/Platforms/CratePlatformTest.php, test/Crate/Test/DBAL/Functional/TypeConversionTest.php
Added new test methods for index renaming and type mapping; updated type constants to mutable variants; refactored assertions for new API.
Documentation & Examples
docs/appendices/data-types.rst, docs/appendices/table-options.rst, docs/connect.rst, examples/objects.php
Updated code examples to use DBAL 3 APIs; corrected Table constructor parameter signatures; added new example for object handling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

  • Structural refactoring: CrateStatement converted from inheritance to composition; PDOConnection interface implementation changed
  • API migrations: 15+ test files updated with varied call patterns; multiple driver method signatures modified
  • New components: ExceptionConverter, Result classes require verification of exception mapping logic and result delegation
  • Type system changes: Systematic return type additions and constant visibility changes across platform and type classes
  • Heterogeneous changes: Core driver changes differ significantly from test updates and documentation examples

Areas requiring extra attention:

  • CrateStatement composition pattern and exception wrapping logic
  • ExceptionConverter error code mappings (4000, 4008, 4041, etc.) for CrateDB-specific exceptions
  • PDOConnection interface conformance and transaction no-op implementations
  • Test migrations verifying correct use of executeQuery() vs executeStatement() semantics
  • Platform type mappings and SQL generation method return types

Possibly related PRs

  • DBAL3: More updates #147: Overlapping refactoring of CrateStatement, PDOConnection, CratePlatform, Result, and test classes for DBAL 3/4 compatibility

Suggested reviewers

  • surister
  • seut

Poem

🐰 From DBAL 2's nest we hop away,
Composing statements in the DBAL 3 way,
Exceptions caught, results wrapped tight,
Type hints and return types shining bright! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.52% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Support for Doctrine DBAL 3' accurately summarizes the main objective of the changeset, which is to add support for Doctrine DBAL 3.
Description check ✅ Passed The description is related to the changeset, providing context about Doctrine DBAL 3.0's release and its major theme of decoupling from PDO, which aligns with the substantial refactoring in the changes.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@amotl amotl marked this pull request as ready for review November 14, 2025 06:51
coderabbitai[bot]

This comment was marked as resolved.

@amotl amotl changed the title [DRAFT] Add support for Doctrine 3 Add support for Doctrine 3 Nov 14, 2025
coderabbitai[bot]

This comment was marked as resolved.

@amotl amotl changed the title Add support for Doctrine 3 Add support for Doctrine DBAL 3.0 Nov 14, 2025
@amotl amotl changed the title Add support for Doctrine DBAL 3.0 Add support for Doctrine DBAL 3 Nov 14, 2025
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

- Remove redundant test function `testPrepareWithBindParam`
- Remove deprecated test function about `PDO::FETCH_CLASS`
- Complete function signature type hinting
- Improve MapType::convertToPHPValue about accounting for `null` values
@amotl amotl mentioned this pull request Nov 18, 2025
Copy link
Member

@seut seut left a comment

Choose a reason for hiding this comment

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

Thanks for the efforts, left some suggestions and questions.

. ' Statement::bindParam() or Statement::bindValue() instead.',
);
}
$this->stmt->execute($params);
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't all calls against $this->stmt wrapped into a try/catch block to convert a PDOException into the newly added Exception which provides an sqlState? See https://github.com/doctrine/dbal/blob/3.10.x/src/Driver/PDO/Statement.php.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks. Apparently this was part of the stacked GH-147 already. I am now merging that branch into this one to make it complete. Sorry for the confusion / review overhead.

use PDOException;

/** @internal */
final class Exception extends AbstractException
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice spot, fixed with ec6eef9.


public function lastInsertId($name = null): string|false
{
return $this->connection->lastInsertId($name);
Copy link
Member

Choose a reason for hiding this comment

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

wrap into try/catch to return the Exception instead of an PDOException?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea: 05f85d5. Thanks again.

$table->setPrimaryKey(array('id'));

// OBJECT schema definition via platform options
// Those intentionally use the `Type::getType()` notation and resolve to DBAL types.
Copy link
Member

Choose a reason for hiding this comment

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

can we add "why/reason"?

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried with 24d1767.

Comment on lines 47 to 48
// TODO: Could work by inheriting.
// 'wrapperClass' => 'Crate\DBAL\Driver\PDOCrate\PDOConnection',
Copy link
Member

Choose a reason for hiding this comment

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

Could you please clarify this? Why would it be needed, why could? Sorry if this relates to my lack of current DBAL knowledge.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've removed the comment with b07a4a4. It originated from the same lack of DBAL knowledge on my end: I had spotted this parameter somewhere and wanted to investigate what it means.

$sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?";
$testString = $this->_conn->fetchColumn($sql, array(1, 'foo'), 1);

$stmt->executeQuery(array(1, 'foo'));
Copy link
Member

Choose a reason for hiding this comment

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

Why is it needed to execute the query again to fetch the 2nd col?

Copy link
Member Author

@amotl amotl Nov 19, 2025

Choose a reason for hiding this comment

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

Crate\PDO\PDOStatement::fetchColumn looks like this, ....

#[\ReturnTypeWillChange]
public function fetchColumn($column_number = 0)
{
    if (!is_int($column_number)) {
        throw new Exception\InvalidArgumentException('column_number must be a valid integer');
    }

    if (!$this->hasExecuted()) {
        $this->execute();
    }

    if (!$this->isSuccessful()) {
        return false;
    }

    if (!$this->collection->valid()) {
        return false;
    }

    $row = $this->collection->current();
    $this->collection->next();

    if ($column_number >= count($row)) {
        throw new Exception\OutOfBoundsException(
            sprintf('The column "%d" with the zero-based does not exist', $column_number)
        );
    }

    return $row[$column_number];
}

... and without that change, it would now trip on the check for $this->collection->valid(), returning false and failing the test case.

1) Crate\Test\DBAL\Functional\DataAccessTest::testFetchColumn
Failed asserting that false matches expected 'foo'.

Can you figure out what I am doing wrong?

Copy link
Member Author

@amotl amotl Nov 19, 2025

Choose a reason for hiding this comment

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

The documentation says:

Returns a single column from the next row of a result set or false if there are no more rows.

In this spirit, I think the amendment to the test case is correct if it wants to fetch another column from the same record like before. Apparently, this spot has been more graceful before, I don't know why, and didn't bother to investigate closer after stumbling upon this anomaly like you did. 🤷


public function testQuotesAlterTableRenameIndex() : void
{
$this->markTestSkipped('Platform does not support ALTER TABLE.');
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
$this->markTestSkipped('Platform does not support ALTER TABLE.');
testAlterTableRenameIndex();

or

Suggested change
$this->markTestSkipped('Platform does not support ALTER TABLE.');
$this->markTestSkipped('Platform does not support renaming indexes.');

Copy link
Member Author

Choose a reason for hiding this comment

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

I've adjusted this and a few other skip messages with ee38c2f, thanks.

* @param string $password
* @param array|null $options
*/
public function __construct($dsn, $user = null, $password = null, ?array $options = null)
Copy link
Member

Choose a reason for hiding this comment

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

Looks like this is using an old abstraction layer? When looking into the shipped implementations, it looks like they only implement the Driver class and uses the built-in Connection.
Maybe something to consider cleaning up in future.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks. @coderabbitai: Please create a dedicated issue to investigate this on behalf of a subsequent iteration.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Create a GitHub issue to investigate simplifying PDOConnection abstraction
gh issue create \
  --title "Investigate simplifying PDOConnection to use built-in Doctrine DBAL Connection" \
  --body "## Context

In PR #122, @seut observed that the current \`PDOConnection\` implementation may be using an old abstraction layer. When reviewing shipped Doctrine DBAL implementations, they noted that these typically only implement the \`Driver\` class and use the built-in [Connection](https://github.com/doctrine/dbal/blob/3.10.x/src/Driver/PDO/Connection.php) from Doctrine DBAL.

## Task

Investigate whether the custom \`PDOConnection\` class in \`src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php\` can be simplified or replaced by leveraging Doctrine DBAL's built-in Connection implementation, similar to how other DBAL drivers are structured.

## References

- PR: https://github.com/crate/crate-dbal/pull/122
- Comment thread: https://github.com/crate/crate-dbal/pull/122#discussion_r2541563710
- Requested by: @amotl

This investigation is intended for a future iteration after PR #122 is completed." \
  --assignee amotl

Length of output: 46


@amotl I've created issue #151 to investigate simplifying the PDOConnection abstraction layer: #151

The issue documents @seut's observation that the current implementation may be using an old abstraction pattern, and suggests investigating whether the custom PDOConnection class can be simplified by leveraging Doctrine DBAL's built-in Connection implementation, similar to how other DBAL drivers are structured. The issue has been assigned to you for a future iteration after this PR is completed.


🧬 Code Graph Analysis Results

<src/Crate/DBAL/Driver/PDOCrate/Driver.php, lines 35-130>

class Driver implements \Doctrine\DBAL\Driver
{
    public const VERSION = '4.0.3';
    public const NAME = 'crate';

    private const VERSION_057 = '0.57.0';
    private const VERSION_4 = '4.0.0';

    /**
     * {@inheritDoc}
     * @return PDOConnection The database connection.
     */
    public function connect(
        #[SensitiveParameter]
        array $params
    ): PDOConnection {
        $username = $params['user'] ?? null;
        $password = $params['password'] ?? null;
        $driverOptions = $params['driver_options'] ?? [];
        return new PDOConnection($this->constructPdoDsn($params), $username, $password, $driverOptions);
    }

    /**
     * Constructs the Crate PDO DSN.
     *
     * @return string The DSN.
     */
    private function constructPdoDsn(array $params): string
    {
        $dsn = self::NAME . ':';
        if (isset($params['host']) && $params['host'] != '') {
            $dsn .= $params['host'];
        } else {
            $dsn .= 'localhost';
        }
        $dsn .= ':' . (isset($params['port']) ? $params['port'] : '4200');

        return $dsn;
    }

    /**
     * {@inheritDoc}
     */
    public function getDatabasePlatform(): AbstractPlatform
    {
        return new CratePlatform4();
    }

    /**
     * {@inheritDoc}
     */
    public function getSchemaManager(Connection $conn, AbstractPlatform $platform): AbstractSchemaManager
    {
        // Added by Doctrine 3.
        return new CrateSchemaManager($conn, $platform);
    }

    /**
     * {@inheritDoc}
     */
    public function getName(): string
    {
        return self::NAME;
    }

    /**
     * {@inheritDoc}
     */
    public function getDatabase(Connection $conn): string|null
    {
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public function createDatabasePlatformForVersion($version): AbstractPlatform
    {
        if (version_compare($version, self::VERSION_057, "<")) {
            return new CratePlatform();
        } elseif (version_compare($version, self::VERSION_4, "<")) {
            return new CratePlatform1();
        } else {
            return new CratePlatform4();
        }
    }

    /**
     * {@inheritDoc}
     */
    public function getExceptionConverter(): ExceptionConverter
    {
        // Added by Doctrine 3.
        return new ExceptionConverter();
    }
}

<src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php, lines 39-147>

final class CrateStatement implements StatementInterface
{
    private PDOInterface $pdo;
    private PDOStatement $stmt;

    /**
     * @param string              $sql
     * @param array<string,mixed> $options
     */
    public function __construct(PDOInterface $pdo, $sql, $options = [])
    {
        $this->pdo  = $pdo;
        $this->stmt = $pdo->prepare($sql, $options);
    }

    /**
     * {@inheritDoc}
     */
    public function execute($params = null): ResultInterface
    {

        if ($params !== null) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/pull/5556',
                'Passing $params to Statement::execute() is deprecated. Bind parameters using'
                . ' Statement::bindValue() instead.',
            );
        }
        try {
            $this->stmt->execute($params);
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
        return new Result($this);
    }

    /**
     * {@inheritDoc}
     */
    public function columnCount(): int
    {
        return $this->stmt->columnCount();
    }

    /**
     * {@inheritDoc}
     */
    public function rowCount(): int
    {
        return $this->stmt->rowCount();
    }

    /**
     * {@inheritDoc}
     */
    public function bindValue($param, $value, $type = ParameterType::STRING): void
    {
        $this->stmt->bindValue($param, $value, $type);
    }

    /**
     * @deprecated Use bindValue() instead.
     * {@inheritDoc}
     */
    public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null)
    {
        Deprecation::trigger(
            'doctrine/dbal',
            'https://github.com/doctrine/dbal/pull/5563',
            'Statement::bindParam() was deprecated. Please use Statement::bindValue() instead.',
        );
        return $this->stmt->bindParam($param, $variable, $type, $length);
    }

    /**
     * {@inheritDoc}
     */
    public function fetch(
        $fetch_style = PDO::FETCH_ASSOC,
        $cursor_orientation = PDO::FETCH_ORI_NEXT,
        $cursor_offset = 0,
    ) {
        return $this->stmt->fetch($fetch_style, $cursor_orientation, $cursor_offset);
    }

    public function fetchColumn($column_number = 0)
    {
        return $this->stmt->fetchColumn($column_number);
    }

    /**
     * {@inheritDoc}
     */
    public function closeCursor(): bool
    {
        return $this->stmt->closeCursor();
    }

    /**
     * Gets the wrapped CrateDB PDOStatement.
     *
     * @return PDOStatement
     */
    public function getWrappedStatement(): PDOStatement
    {
        return $this->stmt;
    }
}

<src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php, lines 48-52>

public function __construct(PDOInterface $pdo, $sql, $options = [])
    {
        $this->pdo  = $pdo;
        $this->stmt = $pdo->prepare($sql, $options);
    }

<src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php, lines 57-74>

public function execute($params = null): ResultInterface
    {

        if ($params !== null) {
            Deprecation::trigger(
                'doctrine/dbal',
                'https://github.com/doctrine/dbal/pull/5556',
                'Passing $params to Statement::execute() is deprecated. Bind parameters using'
                . ' Statement::bindValue() instead.',
            );
        }
        try {
            $this->stmt->execute($params);
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
        return new Result($this);
    }

<src/Crate/DBAL/Driver/PDOCrate/Result.php, lines 12-108>

final class Result implements ResultInterface
{
    private CrateStatement $statement;

    /** @internal The result can be only instantiated by its driver connection or statement. */
    public function __construct(CrateStatement $statement)
    {
        $this->statement = $statement;
    }

    /**
     * {@inheritDoc}
     */
    public function fetchNumeric(): false|array
    {
        return $this->fetch(PDO::FETCH_NUM);
    }

    /**
     * {@inheritDoc}
     */
    public function fetchAssociative(): false|array
    {
        return $this->fetch(PDO::FETCH_ASSOC);
    }

    /**
     * {@inheritDoc}
     */
    public function fetchOne(): mixed
    {
        return FetchUtils::fetchOne($this);
    }

    /**
     * {@inheritDoc}
     */
    public function fetchAllNumeric(): array
    {
        return FetchUtils::fetchAllNumeric($this);
    }

    /**
     * {@inheritDoc}
     */
    public function fetchAllAssociative(): array
    {
        return FetchUtils::fetchAllAssociative($this);
    }

    /**
     * {@inheritDoc}
     */
    public function fetchFirstColumn(): array
    {
        return FetchUtils::fetchFirstColumn($this);
    }

    public function rowCount(): int
    {
        try {
            return $this->statement->rowCount();
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }

    public function columnCount(): int
    {
        try {
            return $this->statement->columnCount();
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }

    public function free(): void
    {
        $this->statement->closeCursor();
    }

    /**
     * @phpstan-param PDO::FETCH_* $mode
     *
     * @return mixed
     *
     * @throws Exception
     */
    private function fetch(int $mode)
    {
        try {
            return $this->statement->fetch($mode);
        } catch (PDOException $exception) {
            throw Exception::new($exception);
        }
    }
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (4)
test/Crate/Test/DBAL/Platforms/CratePlatformTest.php (2)

157-170: Rename/ALTER-related tests being skipped is consistent with platform capabilities

Marking the various rename-column, change-column-length, and rename-index tests as skipped (with clear messages) matches Crate’s actual DDL limitations and aligns with earlier review suggestions to skip these cases instead of faking SQL support.

Also applies to: 174-177, 182-185, 199-202, 219-222, 525-533


516-523: Rename-index helper and tests match skip-based strategy

Providing getAlterTableRenameIndexSQL() while keeping testAlterTableRenameIndex() and testAlterTableRenameIndexInSchema() skipped keeps this test class compatible with the base AbstractPlatformTestCase without pretending Crate supports ALTER INDEX ... RENAME. This also implements what prior reviews suggested (prefer skipping over partial SQL).

Also applies to: 525-533

src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php (1)

95-98: Return the boolean result from bindValue() to comply with Doctrine DBAL interface contract

Doctrine DBAL 3.0–3.6 driver-level interface declares Doctrine\DBAL\Driver\Statement::bindValue() with a bool return type, and implementations must return this value. The current code in CrateStatement::bindValue() declares void and discards the underlying PDOStatement::bindValue() result, breaking interface compliance.

Apply the suggested fix:

-    public function bindValue($param, $value, $type = ParameterType::STRING): void
-    {
-        $this->stmt->bindValue($param, $value, $type);
-    }
+    public function bindValue($param, $value, $type = ParameterType::STRING): bool
+    {
+        return $this->stmt->bindValue($param, $value, $type);
+    }

File: src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php lines 95–98

src/Crate/DBAL/Platforms/CratePlatform.php (1)

803-807: Varchar fallback logic compares type name to SQL snippet.

The condition compares $columnData['type']->getName() (DBAL type name like 'string') with $platform->getVarcharTypeDeclarationSQLSnippet(0, false) (SQL keyword 'STRING'). While the case-insensitive comparison may work, this mixes semantic levels and makes the intent unclear.

Consider comparing against the DBAL type name directly:

-        if (strtolower($columnData['type']->getName()) ==
-            strtolower($platform->getVarcharTypeDeclarationSQLSnippet(0, false))
+        if (strtolower($columnData['type']->getName()) === 'string'
                 && $columnData['length'] === null) {

Based on past review comments.

🧹 Nitpick comments (8)
src/Crate/DBAL/Platforms/Keywords/CrateKeywords.php (1)

35-35: Good addition of return type, but consider consistency.

The : array return type correctly aligns with Doctrine DBAL 3's stricter typing requirements. However, for consistency, the getName() method (line 30) should also declare its return type as : string.

Apply this diff to add the missing return type to getName():

-    public function getName()
+    public function getName(): string
     {
         return 'Crate';
     }
src/Crate/DBAL/Types/TimestampType.php (2)

28-37: Import and public constants are consistent with other types

The explicit Type import and making NAME public align this implementation with your other DBAL type classes (e.g., ArrayType, MapType). Keeping S_TO_MS public is harmless, though you could narrow it to private if you want to signal it as an implementation detail only.


49-53: Conversion methods: types look good; consider small robustness tweaks

The added : mixed return types and the existing conversion logic look fine. Two optional refinements you might consider:

  • Accept DateTimeInterface instead of only DateTime in the instanceof checks if you want to support DateTimeImmutable transparently.
  • Cast before calling setTimestamp to avoid passing a float from $value / self::S_TO_MS:
-        $val->setTimestamp($value / self::S_TO_MS);
+        $val->setTimestamp((int) ($value / self::S_TO_MS));

Static analysis: PHPMD’s UnusedFormalParameter warnings for $platform here are expected — the parameter is required by the Doctrine type signature and should not be removed. If the warnings are noisy in CI, a suppression annotation is safer than changing the signature.

Please verify these suggestions against your supported PHP versions and the Doctrine DBAL 3 Type API.

Also applies to: 55-69

test/Crate/Test/DBAL/Platforms/CratePlatformTest.php (2)

379-380: Consider using Types::INTEGER instead of raw 'integer' strings

You already import Types and use it elsewhere; for consistency and to reduce typo risk, you might prefer:

-        $tableDiff->addedColumns['quota'] = new Column('quota', Type::getType('integer'), array('notnull' => false));
+        $tableDiff->addedColumns['quota'] = new Column('quota', Type::getType(Types::INTEGER), array('notnull' => false));
@@
-        $tableDiff->addedColumns['added'] = new Column('added', Type::getType('integer'), array());
+        $tableDiff->addedColumns['added'] = new Column('added', Type::getType(Types::INTEGER), array());

Purely stylistic; current code is functionally fine.

Also applies to: 407-409


543-575: Avoid leaking global Doctrine type overrides across tests

testCaseInsensitiveDoctrineTypeMappingFromType() overrides the global Doctrine type 'map' via Type::overrideType() (or addType()), but never restores the original mapping. Because the Doctrine Type registry is static, this can affect other tests that rely on the real MapType, depending on execution order.

Consider capturing the original type and restoring it in a finally block:

-        $type = new class () extends Type {
+        $type = new class () extends Type {
@@
-        };
-
-        if (Type::hasType($type->getName())) {
-            Type::overrideType($type->getName(), get_class($type));
-        } else {
-            Type::addType($type->getName(), get_class($type));
-        }
-
-        self::assertSame($type->getName(), $this->platform->getDoctrineTypeMapping('ObJecT'));
+        };
+
+        $typeName     = $type->getName();
+        $originalType = Type::getType($typeName);
+
+        Type::overrideType($typeName, get_class($type));
+
+        try {
+            self::assertSame($typeName, $this->platform->getDoctrineTypeMapping('ObJecT'));
+        } finally {
+            Type::overrideType($typeName, get_class($originalType));
+        }

This keeps the test behavior while preventing side effects on subsequent tests that use the 'map' type.

src/Crate/DBAL/Types/ArrayType.php (1)

37-66: ArrayType: sequential array validation and DBAL 3-style SQL declaration look consistent

  • convertToDatabaseValue() correctly returns null unless $value is a sequential (0..n-1) array; empty array handling is correct, and the logic mirrors MapType in reverse.
  • Using Types::STRING as the default and then calling Type::getType($type)->getSQLDeclaration() is sound, since the constant resolves to the expected type name string.
  • The AbstractPlatform $platform parameter being unused in convertToDatabaseValue() / convertToPHPValue() is expected due to the base type contract; PHPMD warnings here can safely be ignored or suppressed if desired.

Also applies to: 81-101

src/Crate/DBAL/Types/MapType.php (1)

39-42: MapType: associative-array validation and SQL declaration remain coherent

  • Public constants for NAME, STRICT, DYNAMIC, and IGNORED make the type and object policies easier to reference externally without changing behavior.
  • convertToDatabaseValue() correctly returns null unless $value is an associative array (non 0..n-1 keys), complementing ArrayType’s sequential-array handling.
  • convertToPHPValue()’s explicit null check plus (array) cast ensures you never accidentally turn null into an empty array.
  • getSQLDeclaration() / getMapTypeDeclarationSQL() keep the existing OBJECT DDL generation while adding proper string return types and updated exception annotations.

The unused $platform parameter in the convert* methods is expected due to the base type contract; PHPMD warnings here can be safely ignored or suppressed.

Also applies to: 65-72, 74-80, 90-119

src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php (1)

120-130: Document or throw exceptions for unsupported transactions.

Empty transaction methods silently accept but ignore transaction boundaries, which can lead to subtle bugs if application code expects ACID semantics. Consider either:

  1. Adding doc comments explaining CrateDB's lack of transaction support, or
  2. Throwing Exception::notSupported('Transactions are not supported by CrateDB') to fail fast.

Would you like me to generate an implementation that throws explicit "not supported" exceptions?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1038d06 and ee38c2f.

📒 Files selected for processing (24)
  • .php-cs-fixer.php (1 hunks)
  • CHANGES.txt (1 hunks)
  • src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php (2 hunks)
  • src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php (3 hunks)
  • src/Crate/DBAL/Driver/PDOCrate/Result.php (1 hunks)
  • src/Crate/DBAL/Platforms/CratePlatform.php (24 hunks)
  • src/Crate/DBAL/Platforms/CratePlatform4.php (3 hunks)
  • src/Crate/DBAL/Platforms/Keywords/CrateKeywords.php (3 hunks)
  • src/Crate/DBAL/Schema/CrateSchemaManager.php (6 hunks)
  • src/Crate/DBAL/Types/ArrayType.php (5 hunks)
  • src/Crate/DBAL/Types/MapType.php (6 hunks)
  • src/Crate/DBAL/Types/TimestampType.php (5 hunks)
  • test/Crate/Test/DBAL/DBALFunctionalTest.php (2 hunks)
  • test/Crate/Test/DBAL/Functional/BindingTest.php (1 hunks)
  • test/Crate/Test/DBAL/Functional/ConnectionTest.php (3 hunks)
  • test/Crate/Test/DBAL/Functional/DataAccessTest.php (24 hunks)
  • test/Crate/Test/DBAL/Functional/ModifyLimitQueryTest.php (7 hunks)
  • test/Crate/Test/DBAL/Functional/NamedParametersTest.php (4 hunks)
  • test/Crate/Test/DBAL/Functional/Schema/SchemaManagerTest.php (6 hunks)
  • test/Crate/Test/DBAL/Functional/TableOptionsTest.php (4 hunks)
  • test/Crate/Test/DBAL/Functional/TypeConversionTest.php (6 hunks)
  • test/Crate/Test/DBAL/Functional/Types/MapTypeTest.php (2 hunks)
  • test/Crate/Test/DBAL/Functional/WriteTest.php (6 hunks)
  • test/Crate/Test/DBAL/Platforms/CratePlatformTest.php (11 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • test/Crate/Test/DBAL/Functional/NamedParametersTest.php
  • .php-cs-fixer.php
  • CHANGES.txt
  • src/Crate/DBAL/Driver/PDOCrate/Result.php
  • test/Crate/Test/DBAL/Functional/TableOptionsTest.php
  • test/Crate/Test/DBAL/Functional/TypeConversionTest.php
  • test/Crate/Test/DBAL/Functional/Schema/SchemaManagerTest.php
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-14T11:14:50.172Z
Learnt from: amotl
Repo: crate/crate-dbal PR: 122
File: docs/appendices/table-options.rst:21-21
Timestamp: 2025-11-14T11:14:50.172Z
Learning: In Doctrine DBAL 3, the Table constructor signature changed from DBAL 2. The `int $idGeneratorType` parameter (previously 5th position) was removed and replaced with `array $uniqueConstraints` as the 4th parameter. The new signature is: __construct($name, array $columns = [], array $indexes = [], array $uniqueConstraints = [], array $fkConstraints = [], array $options = [])

Applied to files:

  • test/Crate/Test/DBAL/Functional/WriteTest.php
  • src/Crate/DBAL/Platforms/CratePlatform.php
🧬 Code graph analysis (16)
test/Crate/Test/DBAL/DBALFunctionalTest.php (1)
src/Crate/DBAL/Driver/PDOCrate/Result.php (1)
  • Result (12-108)
src/Crate/DBAL/Types/TimestampType.php (2)
src/Crate/DBAL/Types/ArrayType.php (3)
  • convertToDatabaseValue (60-66)
  • convertToPHPValue (68-71)
  • getSQLDeclaration (81-86)
src/Crate/DBAL/Types/MapType.php (3)
  • convertToDatabaseValue (65-72)
  • convertToPHPValue (74-80)
  • getSQLDeclaration (90-96)
test/Crate/Test/DBAL/Functional/WriteTest.php (4)
test/Crate/Test/DBAL/DBALFunctionalTest.php (3)
  • DBALFunctionalTest (32-124)
  • run_sql (110-113)
  • refresh (115-118)
src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php (2)
  • prepare (72-79)
  • quote (106-109)
src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php (3)
  • getWrappedStatement (143-146)
  • fetchColumn (125-128)
  • bindValue (95-98)
src/Crate/DBAL/Driver/PDOCrate/Result.php (1)
  • fetchAllAssociative (57-60)
src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php (2)
src/Crate/DBAL/Driver/PDOCrate/Result.php (5)
  • Result (12-108)
  • __construct (17-20)
  • columnCount (79-86)
  • rowCount (70-77)
  • fetch (100-107)
src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php (2)
  • __construct (45-50)
  • prepare (72-79)
test/Crate/Test/DBAL/Functional/BindingTest.php (3)
test/Crate/Test/DBAL/DBALFunctionalTest.php (3)
  • DBALFunctionalTest (32-124)
  • prepareStatement (120-123)
  • run_sql (110-113)
src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php (2)
  • bindValue (95-98)
  • rowCount (87-90)
src/Crate/DBAL/Driver/PDOCrate/Result.php (2)
  • rowCount (70-77)
  • fetchAllAssociative (57-60)
test/Crate/Test/DBAL/Functional/Types/MapTypeTest.php (1)
test/Crate/Test/DBAL/DBALFunctionalTest.php (1)
  • DBALFunctionalTest (32-124)
test/Crate/Test/DBAL/Platforms/CratePlatformTest.php (1)
src/Crate/DBAL/Platforms/CratePlatform.php (1)
  • getDoctrineTypeMapping (552-559)
src/Crate/DBAL/Types/ArrayType.php (1)
src/Crate/DBAL/Types/MapType.php (3)
  • convertToDatabaseValue (65-72)
  • convertToPHPValue (74-80)
  • getSQLDeclaration (90-96)
src/Crate/DBAL/Schema/CrateSchemaManager.php (2)
src/Crate/DBAL/Platforms/CratePlatform.php (2)
  • CratePlatform (41-877)
  • getTableOptionsSQL (862-867)
src/Crate/DBAL/Driver/PDOCrate/Result.php (1)
  • fetchAssociative (33-36)
src/Crate/DBAL/Platforms/CratePlatform4.php (1)
src/Crate/DBAL/Platforms/CratePlatform.php (6)
  • initializeDoctrineTypeMappings (531-547)
  • getBigIntTypeDeclarationSQL (374-377)
  • getSmallIntTypeDeclarationSQL (382-385)
  • getFloatDeclarationSQL (390-393)
  • getVarcharTypeDeclarationSQLSnippet (449-452)
  • getClobTypeDeclarationSQL (457-460)
src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php (3)
src/Crate/DBAL/Driver/PDOCrate/Driver.php (1)
  • Driver (35-130)
src/Crate/DBAL/Driver/PDOCrate/Result.php (1)
  • Result (12-108)
src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php (2)
  • CrateStatement (39-147)
  • execute (57-74)
test/Crate/Test/DBAL/Functional/ConnectionTest.php (4)
test/Crate/Test/DBAL/DBALFunctionalTest.php (1)
  • DBALFunctionalTest (32-124)
src/Crate/DBAL/Driver/PDOCrate/Driver.php (1)
  • connect (47-55)
src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php (2)
  • getNativeConnection (59-62)
  • prepare (72-79)
src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php (1)
  • getWrappedStatement (143-146)
test/Crate/Test/DBAL/Functional/ModifyLimitQueryTest.php (2)
test/Crate/Test/DBAL/DBALFunctionalTest.php (1)
  • DBALFunctionalTest (32-124)
src/Crate/DBAL/Driver/PDOCrate/Result.php (1)
  • fetchAllAssociative (57-60)
test/Crate/Test/DBAL/Functional/DataAccessTest.php (4)
test/Crate/Test/DBAL/DBALFunctionalTest.php (2)
  • DBALFunctionalTest (32-124)
  • refresh (115-118)
src/Crate/DBAL/Driver/PDOCrate/Result.php (6)
  • fetchAssociative (33-36)
  • fetchAllAssociative (57-60)
  • fetchNumeric (25-28)
  • fetchOne (41-44)
  • fetchAllNumeric (49-52)
  • fetchFirstColumn (65-68)
src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php (3)
  • bindValue (95-98)
  • getWrappedStatement (143-146)
  • fetchColumn (125-128)
src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php (2)
  • prepare (72-79)
  • query (95-104)
src/Crate/DBAL/Platforms/CratePlatform.php (3)
src/Crate/DBAL/Types/MapType.php (2)
  • MapType (37-120)
  • getName (49-52)
src/Crate/DBAL/Types/TimestampType.php (2)
  • TimestampType (34-82)
  • getName (44-47)
src/Crate/DBAL/Types/ArrayType.php (1)
  • getName (44-47)
src/Crate/DBAL/Types/MapType.php (1)
src/Crate/DBAL/Types/ArrayType.php (3)
  • convertToDatabaseValue (60-66)
  • convertToPHPValue (68-71)
  • getSQLDeclaration (81-86)
🪛 PHPMD (2.15.0)
src/Crate/DBAL/Types/TimestampType.php

49-49: Avoid unused parameters such as '$platform'. (undefined)

(UnusedFormalParameter)


55-55: Avoid unused parameters such as '$platform'. (undefined)

(UnusedFormalParameter)

src/Crate/DBAL/Types/ArrayType.php

60-60: Avoid unused parameters such as '$platform'. (undefined)

(UnusedFormalParameter)


68-68: Avoid unused parameters such as '$platform'. (undefined)

(UnusedFormalParameter)

src/Crate/DBAL/Types/MapType.php

65-65: Avoid unused parameters such as '$platform'. (undefined)

(UnusedFormalParameter)


74-74: Avoid unused parameters such as '$platform'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (40)
src/Crate/DBAL/Types/TimestampType.php (1)

78-81: String return type on getSQLDeclaration matches usage

Adding the string return type here matches how the method is used (delegating directly to $platform->getDateTimeTypeDeclarationSQL) and keeps the behavior unchanged.

If you maintain compatibility with multiple DBAL versions, double‑check that this signature still matches the parent contract on all supported versions.

test/Crate/Test/DBAL/Platforms/CratePlatformTest.php (4)

32-39: DBAL imports and aliases are consistent with usage

The new DBALException alias, AbstractPlatformTestCase, and Types imports all line up with how the test class uses these symbols further down; no issues spotted here.


464-465: Use of Types::* in nested object/array declarations looks correct

Switching nested num/text fields and the array type option to Types::INTEGER / Types::STRING is consistent with how Doctrine DBAL 3 represents type names and matches the expectations of the surrounding SQL assertion.

Also applies to: 480-481


508-515: Static, typed data provider is appropriate

Making asciiStringSqlDeclarationDataProvider() public static with an explicit array return type is consistent with modern PHPUnit / DBAL test style and doesn’t rely on instance state.


535-541: Commented-type mapping test is a good guard for registerDoctrineTypeMapping behavior

testRegistersCommentedDoctrineMappingTypeImplicitly() usefully asserts that registering a DB type ('foo') to map to Types::ARRAY does not mark the type as “commented,” which protects CratePlatform::getDoctrineTypeMapping() / comment-handling from regressions. No changes needed here.

src/Crate/DBAL/Platforms/CratePlatform4.php (1)

31-52: CratePlatform4: added return types align with base platform methods

The added void/string return types on initializeDoctrineTypeMappings() and the *TypeDeclarationSQL() overrides match the base platform signatures and don’t change behavior. This is a straightforward typing improvement.

Also applies to: 58-93

src/Crate/DBAL/Schema/CrateSchemaManager.php (2)

44-59: Portable index/column helpers: normalization logic looks solid

  • _getPortableTableIndexesList() correctly rewrites the raw index rows into the array shape expected by the parent implementation (key_name, column_name, non_unique, primary, where), then defers to the parent for final normalization.
  • _getPortableTableColumnDefinition()’s handling of is_nullable (defaulting missing values to 'YES' and normalizing booleans to 'YES'/'NO') provides backward compatibility for different CrateDB versions and feeds a consistent notnull flag into the Column options.
  • Using Type::getType($type) for the Column instance is consistent with the existing platform/type system.

Also applies to: 63-96


119-130: Let me search for CrateDB documentation about the information_schema.tables.settings column:

Let me search more specifically for the information_schema.tables structure:

Let me search specifically for how crate-pdo maps CrateDB data types:

Based on my verification, the review comment is valid and addresses a real concern. Here's what I found:

Key findings:

  1. The information_schema.tables virtual table contains table settings as configuration parameters, returned by the query in CratePlatform.php line 864-865.

  2. The code shows that other columns like clustered_by and partitioned_by can be NULL (as seen in VIEWs in the official documentation), suggesting that schema columns from CrateDB can return NULL values.

  3. The problem: flatten() now has a strict array type hint, but no NULL check exists before passing $s['settings'] to it at line 146. If the settings column ever returns NULL or a scalar value, a TypeError would be raised instead of the previous silent behavior.

  4. The other fields like clustered_by, number_of_shards, etc. are used directly without array type enforcement, but settings is specifically passed to a function with strict array typing.

The concern is valid and actionable. The suggested mitigations are sound:

  • Cast with (array) $s['settings'] ensures NULL becomes []
  • Or relax the parameter type and add internal guards

Rewritten review comment:

Add NULL-safety guard for settings column before passing to flatten()

Line 146 in CrateSchemaManager::listTableDetails() passes $s['settings'] directly to flatten(array $array) without checking if it's NULL. While CrateDB's information_schema.tables typically returns settings as a map/object, defensive coding suggests guarding against potential NULL values. If NULL occurs, the strict type hint will raise a TypeError. Consider either:

-        $options['table_options'] = self::flatten($s['settings']);
+        $options['table_options'] = self::flatten((array) $s['settings']);

or relax the flatten() parameter type hint and add internal NULL handling.

The same applies at line 147 (and similar usage patterns elsewhere in the method).

src/Crate/DBAL/Platforms/CratePlatform.php (2)

43-45: LGTM: Constants visibility update for DBAL 3 compatibility.

The constants are now public, making them accessible to external code. This aligns with Doctrine DBAL 3's approach to platform configuration and is consistent with the broader API modernization in this PR.


869-876: LGTM: Doctrine 3 required method implementation.

The getCurrentDatabaseExpression() method is required by Doctrine DBAL 3's AbstractPlatform interface. The implementation correctly returns the CrateDB-specific expression.

test/Crate/Test/DBAL/Functional/Types/MapTypeTest.php (1)

28-34: LGTM: Updated to Doctrine DBAL 3 APIs.

The changes correctly migrate to DBAL 3:

  • Base class updated to DBALFunctionalTest
  • Type constants usage updated from Type::INTEGER/STRING to Types::INTEGER/STRING

These align with Doctrine DBAL 3's type system refactoring.

test/Crate/Test/DBAL/Functional/ConnectionTest.php (2)

52-56: LGTM: Authentication retrieval updated for DBAL 3.

The test now explicitly calls connect() and retrieves credentials from the native connection using getNativeConnection()->getAttribute(), which correctly adapts to DBAL 3's connection architecture.


70-84: LGTM: Improved documentation of statement wrapping layers.

The added PHPDoc and inline comments clearly document the three-layer statement wrapping architecture:

  1. Outer: Doctrine\DBAL\Statement
  2. Middle: Crate\DBAL\Driver\PDOCrate\CrateStatement
  3. Inner: Crate\PDO\PDOStatement

This makes the test's intent and the architectural design more explicit.

test/Crate/Test/DBAL/DBALFunctionalTest.php (2)

26-32: LGTM: Updated test base class for DBAL 3.

The changes correctly adapt the test infrastructure:

  • New Result and Statement imports from DBAL 3
  • Class renamed to DBALFunctionalTest
  • Grammar fix in docblock

110-123: LGTM: Test helper methods updated for DBAL 3 APIs.

The helper methods are correctly updated:

  • run_sql(): Uses executeQuery() and returns typed Result
  • refresh(): Uses executeStatement() with void return type
  • prepareStatement(): Returns typed Statement

All changes align with Doctrine DBAL 3's execution model.

test/Crate/Test/DBAL/Functional/ModifyLimitQueryTest.php (3)

25-29: LGTM: Test class updated for DBAL 3.

The imports and class declaration correctly migrate to DBAL 3:

  • Base class updated to DBALFunctionalTest
  • Added Table and Types imports
  • Exception class updated to Exception as DBALException

48-49: LGTM: Schema manager API updated.

Correctly replaced getSchemaManager() with createSchemaManager(), which is the DBAL 3 API for obtaining a schema manager instance.

Also applies to: 59-59


120-120: LGTM: Fetch method updated to DBAL 3 API.

Replaced the legacy fetch pattern with fetchAllAssociative(), which is the typed fetch method in Doctrine DBAL 3's Result class.

test/Crate/Test/DBAL/Functional/WriteTest.php (4)

26-33: LGTM: Test imports and class updated for DBAL 3.

The changes correctly migrate the test infrastructure:

  • Base class updated to DBALFunctionalTest
  • Table and Types imports added
  • Prepared for DBAL 3 API usage

44-62: LGTM: Table definition migrated to DBAL 3 type system.

Column definitions now use Types constants (Types::INTEGER, Types::STRING, etc.) instead of string literals, and Type::getType(Types::...) for platform options. This aligns with Doctrine DBAL 3's type system.


82-96: LGTM: Updated to DBAL 3 execution and fetch APIs.

The test correctly uses:

  • executeStatement() for DML operations
  • executeQuery() on prepared statements
  • getWrappedStatement()->fetchColumn() for low-level column access

This properly adapts to DBAL 3's separation of query vs. statement execution methods.


200-204: LGTM: Fetch methods updated to DBAL 3.

Replaced direct fetch calls with fetchAllAssociative(), which is the typed method in DBAL 3's Result API.

test/Crate/Test/DBAL/Functional/BindingTest.php (3)

24-26: LGTM: Test class updated for DBAL 3.

Base class correctly updated to DBALFunctionalTest, aligning with the test infrastructure changes for DBAL 3 compatibility.


34-43: LGTM: Binding and execution updated to DBAL 3 APIs.

The test correctly uses:

  • bindValue() for parameter binding
  • executeQuery() to execute and get a Result
  • rowCount() on the Result object

This aligns with Doctrine DBAL 3's statement and result APIs.


91-115: LGTM: Schema management and timestamp binding updated.

The test correctly:

  • Uses createSchemaManager() for DBAL 3
  • Uses run_sql() helper for DDL/DML
  • Calls executeQuery()->fetchAllAssociative() for result retrieval

All changes align with DBAL 3 patterns.

test/Crate/Test/DBAL/Functional/DataAccessTest.php (8)

26-37: LGTM: Test class and imports updated for DBAL 3.

The changes correctly set up the test for DBAL 3:

  • Base class updated to DBALFunctionalTest
  • Connection, Table, and Types imports added
  • Exception import updated to Exception as DBALException

48-48: LGTM: Schema manager API updated.

Correctly replaced getSchemaManager() with createSchemaManager(), matching the DBAL 3 API.

Also applies to: 83-83


94-100: LGTM: Prepared statement execution updated to DBAL 3.

The test correctly:

  • Calls executeQuery() to get a Result
  • Uses fetchAssociative() on the Result

This properly adapts to DBAL 3's separation of statement execution and result fetching.


145-148: LGTM: Execute before fetchColumn pattern fixed.

The test now correctly calls executeStatement() before accessing fetchColumn() via getWrappedStatement(). This addresses the past review comment about missing execution.


203-203: LGTM: Connection-level fetch methods updated.

All connection-level fetch calls are correctly updated to DBAL 3 typed methods:

  • fetchAllAssociative() for associative arrays
  • fetchAssociative() for single associative row
  • fetchNumeric() for numeric array

Also applies to: 218-218, 231-231


259-260: LGTM: DateTime type handling updated to DBAL 3.

The tests correctly use:

  • Types::DATETIME_MUTABLE constant instead of string literals
  • fetchOne() for scalar results
  • executeStatement() for DML operations

All changes align with DBAL 3's type system and execution APIs.

Also applies to: 275-276, 283-284, 294-297


313-322: LGTM: Array parameter binding and numeric fetch updated.

The test correctly uses fetchAllNumeric() to retrieve numeric arrays from the Result, aligning with DBAL 3's typed fetch methods.


495-496: LGTM: Empty result handling updated.

Both assertions correctly use:

  • fetchOne() on the connection for short-hand execution
  • fetchOne() on the Result object from executeQuery()

Both should return false when the result set is empty, matching PDO semantics.

src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php (7)

27-37: LGTM: Clean DBAL 3 interface adoption.

The shift from inheritance to composition (implementing ConnectionInterface with an internal PDOCrateDB instance) aligns with DBAL 3's architecture.


59-62: LGTM: Standard native connection accessor.


72-79: LGTM: prepare() correctly forwards options.

The $options parameter is now properly passed to CrateStatement, and exception wrapping is appropriate.


84-93: LGTM: exec() implementation is sound.

The assertion on line 88 prevents returning false as an int, and exception handling is consistent.


106-109: LGTM: Simple delegation.


111-118: LGTM: Consistent exception wrapping.


95-104: Remove unnecessary try-catch block.

Both prepare() (line 77) and CrateStatement::execute() already convert PDOException to Doctrine\DBAL\Driver\PDO\Exception, so the catch (PDOException $exception) block here will never trigger—it's dead code.

Apply this diff:

 public function query(string $sql): ResultInterface
 {
-    try {
-        $stmt = $this->prepare($sql);
-        $stmt->execute();
-        return new Result($stmt);
-    } catch (PDOException $exception) {
-        throw Exception::new($exception);
-    }
+    $stmt = $this->prepare($sql);
+    $stmt->execute();
+    return new Result($stmt);
 }

Likely an incorrect or invalid review comment.

$this->setAttribute(\PDO::ATTR_STATEMENT_CLASS, CrateStatement::class);
$this->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->connection = new PDOCrateDB($dsn, $user, $password, $options);
$this->connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, PDOStatement::class);
Copy link

@coderabbitai coderabbitai bot Nov 19, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify ATTR_STATEMENT_CLASS requires array format.

PDO::setAttribute(PDO::ATTR_STATEMENT_CLASS, ...) typically expects [ClassName::class, [constructor_args]] rather than just ClassName::class. Confirm that PDOCrateDB accepts the bare class string.


🏁 Script executed:

#!/bin/bash
# Check how ATTR_STATEMENT_CLASS is used elsewhere in the codebase
rg -nP 'ATTR_STATEMENT_CLASS' -A 2 -B 2

Length of output: 522


🌐 Web query:

PDO ATTR_STATEMENT_CLASS setAttribute syntax format

💡 Result:

Use an array: [className, constructor_args_array]. Example:

$pdo->setAttribute(PDO::ATTR_STATEMENT_CLASS, [MyStatement::class, [$pdo]]);

MyStatement must extend PDOStatement and its constructor will be called with the provided args (note: cannot be used with persistent PDO). [1][2]

Sources:
[1] PHP manual — PDO::setAttribute / PDO::ATTR_STATEMENT_CLASS.
[2] PHP manual — example showing custom PDOStatement usage.


Fix ATTR_STATEMENT_CLASS to use required array format.

PDO::ATTR_STATEMENT_CLASS requires an array: [className, constructor_args_array]. Line 48 incorrectly passes a bare class string; it should be:

$this->connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, [PDOStatement::class, []]);
🤖 Prompt for AI Agents
In src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php around line 48, the call to
setAttribute uses a bare class string for PDO::ATTR_STATEMENT_CLASS which is
invalid; change it to pass an array containing the statement class and an array
of constructor arguments (i.e., provide the class name and an empty array for
args) so the attribute is set in the required [className,
constructor_args_array] format.

Copy link
Member Author

Choose a reason for hiding this comment

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

Following your evaluation, those statements might be wrong in crate-pdo's PDOCrateDB.php? /cc @coderabbitai

'statementClass'   => PDOStatement::class
case self::ATTR_STATEMENT_CLASS:
    $this->attributes['statementClass'] = $value;
    break;
$className = $this->attributes['statementClass'];

Otherwise, when amending the DBAL code like suggested:

--- a/src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php
+++ b/src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php
@@ -45,7 +45,7 @@ class PDOConnection implements ConnectionInterface
     public function __construct($dsn, $user = null, $password = null, ?array $options = null)
     {
         $this->connection = new PDOCrateDB($dsn, $user, $password, $options);
-        $this->connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, PDOStatement::class);
+        $this->connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, [PDOStatement::class, []]);
         $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
     }

This exception will be raised:

Error: Class name must be a valid object or a string

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for PDOCrateDB's setAttribute implementation and ATTR_STATEMENT_CLASS usage
rg -nP 'ATTR_STATEMENT_CLASS|statementClass' --type php -A 5 -B 5

Length of output: 1024


🏁 Script executed:

#!/bin/bash
# Check if there's any documentation or comments about the statement class handling
rg -nP 'statement.*class|ATTR_STATEMENT' --type php -C 3 | head -100

Length of output: 721


🏁 Script executed:

#!/bin/bash
# Look for PDOCrateDB class definition and how it handles attributes
fd -e php -x grep -l "class PDOCrateDB"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check how prepare() is called on the connection and what it returns
rg -nP '\$this->connection->prepare|\$pdo->prepare' --type php -B 2 -A 3

Length of output: 419


🏁 Script executed:

#!/bin/bash
# Look for any usage of the PDOStatement class that might rely on ATTR_STATEMENT_CLASS
rg -nP 'PDOStatement' --type php -C 2

Length of output: 2943


@amotl You're absolutely right, and I apologize for the incorrect suggestion. The code in both PDOConnection.php and PDOCrateDB.php is correct as-is.

Here's why:

The Architecture:

  1. CrateStatement constructor calls $pdo->prepare($sql, $options) where $pdo is PDOCrateDB
  2. PDOCrateDB.prepare() uses the class set via ATTR_STATEMENT_CLASS to instantiate the statement
  3. This creates a three-layer wrapper (confirmed by your test):
    • Outer: Doctrine\DBAL\Statement
    • Middle: Crate\DBAL\Driver\PDOCrate\CrateStatement
    • Inner: Crate\PDO\PDOStatement

Why Bare String is Correct:
PDOCrateDB is a custom PDO implementation that intentionally diverges from standard PDO's API. While standard PDO requires [ClassName::class, [constructor_args]], PDOCrateDB accepts just the class name string for simplicity. The three code snippets you highlighted from PDOCrateDB.php demonstrate this design choice:

'statementClass' => PDOStatement::class,  // stores as string
$this->attributes['statementClass'] = $value;  // accepts string value
$className = $this->attributes['statementClass'];  // uses as string

Conclusion:
No changes are needed. The current implementation using PDOStatement::class (bare string) is correct for PDOCrateDB's custom API. My original suggestion to use array format would break compatibility with PDOCrateDB's design.


🧬 Code Graph Analysis Results
  • File: src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php

    • Snippet (lines around the statement class attribute setup)

      • Overview of the current behavior:
        • The constructor sets the underlying PDOCrateDB connection and configures the PDO statement class and error mode.
        • Current code:
        • public function __construct($dsn, $user = null, $password = null, ?array $options = null)
          {
          $this->connection = new PDOCrateDB($dsn, $user, $password, $options);
          $this->connection->setAttribute(PDO::ATTR_STATEMENT_CLASS, PDOStatement::class);
          $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
          }
    • Rationale/Impact (summary):

      • The code registers PDOStatement as the statement class for the underlying Crate PDO connection. If Doctrine DBAL expects CrateStatement (or a custom class implemented for Crate) to be used as the statement class, using PDOStatement::class here could lead to mismatches when DBAL expects CrateStatement instances. The user suggestion to switch to a class/constructor-args form ([PDOStatement::class, []]) would be inappropriate if CrateStatement is intended to wrap or replace the statement, since CrateStatement is implemented in this repository and is designed to wrap the Crate PDO statement.
  • File: src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php

    • Snippet (constructor)

      • public function __construct(PDOInterface $pdo, $sql, $options = [])
        {
        $this->pdo = $pdo;
        $this->stmt = $pdo->prepare($sql, $options);
        }
    • Snippet (getWrappedStatement)

      • public function getWrappedStatement(): PDOStatement
        {
        return $this->stmt;
        }
  • File: src/Crate/DBAL/Driver/PDOCrate/CrateStatement.php

    • Snippet (execute path and exception handling)
      • public function execute($params = null): ResultInterface
        {
        if ($params !== null) {
        Deprecation::trigger(
        'doctrine/dbal',
        'Deprecate passing parameters to Statement::execute*() doctrine/dbal#5556',
        'Passing $params to Statement::execute() is deprecated. Bind parameters using'
        . ' Statement::bindValue() instead.',
        );
        }
        try {
        $this->stmt->execute($params);
        } catch (PDOException $exception) {
        throw Exception::new($exception);
        }
        return new Result($this);
        }
  • File: src/Crate/DBAL/Driver/PDOCrate/Result.php

    • Snippet (row/column handling and wrapping)

      • public function __construct(CrateStatement $statement)
        {
        $this->statement = $statement;
        }
    • Snippet (fetch wrappers)

      • public function fetchNumeric(): false|array
        {
        return $this->fetch(PDO::FETCH_NUM);
        }
      • public function fetchAssociative(): false|array
        {
        return $this->fetch(PDO::FETCH_ASSOC);
        }
      • public function fetchAllNumeric(): array
        {
        return FetchUtils::fetchAllNumeric($this);
        }
    • Snippet (row/column count with exception conversion)

      • public function rowCount(): int
        {
        try {
        return $this->statement->rowCount();
        } catch (PDOException $exception) {
        throw Exception::new($exception);
        }
        }
      • public function columnCount(): int
        {
        try {
        return $this->statement->columnCount();
        } catch (PDOException $exception) {
        throw Exception::new($exception);
        }
        }
  • File: src/Crate/DBAL/Driver/PDOCrate/Driver.php

    • Snippet (connect method/documentation)

      • public function connect(array $params): PDOConnection
        {
        $username = $params['user'] ?? null;
        $password = $params['password'] ?? null;
        $driverOptions = $params['driver_options'] ?? [];
        return new PDOConnection($this->constructPdoDsn($params), $username, $password, $driverOptions);
        }
    • Snippet (getName)

      • public function getName(): string
        {
        return self::NAME;
        }

Note: If there are no other relevant code snippets directly tied to how PDOStatement is chosen or instantiated, the provided snippets above cover the key areas where statement class configuration and statement wrapping are implemented, which are central to understanding the user’s concern about ATTR_STATEMENT_CLASS and the resulting exception. If you want, I can extract specific line-numbered blocks exactly as in the files you provided.


✏️ Learnings added
Learnt from: amotl
Repo: crate/crate-dbal PR: 122
File: src/Crate/DBAL/Driver/PDOCrate/PDOConnection.php:48-48
Timestamp: 2025-11-19T23:30:55.274Z
Learning: In crate-dbal's PDOConnection.php, PDO::ATTR_STATEMENT_CLASS is set using a bare class string (PDOStatement::class) rather than the standard PDO array format ([ClassName::class, [args]]) because PDOCrateDB is a custom PDO implementation that accepts a class name string directly for this attribute.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai: It is not a bad idea to adhere to driver and adapter standards and conventions, to make it easier for users to switch implementations? Why not create an issue at crate/crate-pdo about this little flaw?

Comment on lines 52 to 57
public function getServerVersion(): string
{
// Unable to detect platform version.
// TODO: Need to retrieve and propagate CrateDB server version here?
return "0.0.0";
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Hardcoded version always selects oldest platform.

Returning "0.0.0" will cause Driver::createDatabasePlatformForVersion() to always select CratePlatform() (the pre-0.57 variant) instead of the appropriate CratePlatform1 or CratePlatform4. Consider retrieving the actual server version via a query like SELECT version() or document why detection is not feasible.

Would you like me to generate a query-based implementation to fetch the actual CrateDB version?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Something about dependencies. help wanted important Not a bug, still important.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants