Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2e3ee1a
Validate DDL column options and PDO DSN parameters
simon-mundy Aug 31, 2026
0446b2a
Add @todo to promote getDsnParameter to AbstractPdoConnection
simon-mundy Aug 31, 2026
8c5c679
Merge branch '0.5.x' into security-ddl-column-option-injection
simon-mundy Aug 31, 2026
912ba8f
Apply mago 1.47 formatting
simon-mundy Aug 31, 2026
3ede3bb
Remove tests duplicated by the 0.5.x merge
simon-mundy Aug 31, 2026
b90afb7
Prefer guard clauses over parenthesised multi-line returns
simon-mundy Aug 31, 2026
256f146
mago analyze: fix findings in DDL option handling, regenerate baseline
simon-mundy Aug 31, 2026
90a858f
Implement ResultInterface::getQueryResult() for the mysqli Result
simon-mundy Aug 31, 2026
1e54de9
Point coverage metadata at ColumnOptionTrait
simon-mundy Aug 31, 2026
1fc4d08
Credit enum coverage exercised by the DDL option tests
simon-mundy Aug 31, 2026
60e0381
Close the coverage gaps in the driver classes
simon-mundy Aug 31, 2026
9b005f6
Reduce coverage exclusions to the two irreducible branches
simon-mundy Aug 31, 2026
78f2eb3
Drive column option rendering from a single table
simon-mundy Sep 4, 2026
ba9ad35
Regenerate the analyze baseline for Mago 1.47.5
simon-mundy Sep 4, 2026
b5facc2
Build the mysqli row with array_combine instead of an index loop
simon-mundy Sep 4, 2026
2879a49
Keep the mysqli row assignment on one line for the formatter
simon-mundy Sep 4, 2026
fbf9b38
Type the keyword resolver against the concrete enums
simon-mundy Sep 4, 2026
1904dda
Changed phpdb dependency to dev
simon-mundy Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 53 additions & 197 deletions analysis-baseline.toml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
"require": {
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
"php-db/phpdb": "^0.6.0"
"php-db/phpdb": "^0.6.x-dev"
},
"require-dev": {
"ext-mysqli": "*",
Expand Down
17 changes: 8 additions & 9 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 2 additions & 8 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,6 @@ public function connect(): ConnectionInterface
);
}

if ($this->resource->connect_error) {
throw new Exception\RuntimeException(
'Connection error',
$this->resource->connect_errno,
new Exception\ErrorException($this->resource->connect_error, $this->resource->connect_errno),
);
}

if ('' !== ($p['charset'] ?? '')) {
$this->resource->set_charset($p['charset']);
}
Expand Down Expand Up @@ -264,9 +256,11 @@ public function getCurrentSchema(): string|false
}

$r = $result->fetch_row();
// @codeCoverageIgnoreStart
if (false === $r) {
throw new Exception\RuntimeException($this->resource->error);
}
// @codeCoverageIgnoreEnd

/** @var array{0: string|null}|null $r */
if (null === $r || null === $r[0]) {
Expand Down
2 changes: 2 additions & 0 deletions src/Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ public function __construct(
#[Override]
public function checkEnvironment(): bool
{
// @codeCoverageIgnoreStart
if (! extension_loaded('mysqli')) {
throw new Exception\RuntimeException(
'The Mysqli extension is required for this adapter but the extension is not loaded',
);
}
// @codeCoverageIgnoreEnd
return true;
}

Expand Down
47 changes: 32 additions & 15 deletions src/Pdo/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
use function is_array;
use function is_int;
use function is_string;
use function preg_match;
use function sprintf;
use function strtolower;

// @mago-expect lint:cyclomatic-complexity
// @mago-expect lint:kan-defect
final class Connection extends AbstractPdoConnection
{
// @mago-expect analysis:write-only-property - read by the parent's final AbstractPdoConnection::getDsn()
Expand Down Expand Up @@ -104,22 +105,22 @@ public function connect(): ConnectionInterface
if (null === $dsn) {
$dsn = [];
if (null !== $database) {
$dsn[] = "dbname={$database}";
$dsn[] = "dbname={$this->getDsnParameter('dbname', $database)}";
}
if (null !== $hostname) {
$dsn[] = "host={$hostname}";
$dsn[] = "host={$this->getDsnParameter('host', $hostname)}";
}
if (null !== $port) {
$dsn[] = "port={$port}";
}
if (null !== $charset) {
$dsn[] = "charset={$charset}";
$dsn[] = "charset={$this->getDsnParameter('charset', $charset)}";
}
if (null !== $unixSocket) {
$dsn[] = "unix_socket={$unixSocket}";
$dsn[] = "unix_socket={$this->getDsnParameter('unix_socket', $unixSocket)}";
}
if (null !== $version) {
$dsn[] = "version={$version}";
$dsn[] = "version={$this->getDsnParameter('version', $version)}";
}
$dsn = 'mysql:' . implode(';', $dsn);
}
Expand All @@ -132,9 +133,7 @@ public function connect(): ConnectionInterface
$this->driverName = strtolower((string) $this->resource->getAttribute(PDO::ATTR_DRIVER_NAME));
} catch (PDOException $e) {
$code = $e->getCode();
if (! is_int($code)) {
$code = 0;
}
$code = is_int($code) ? $code : 0;
throw new Exception\RuntimeException("Connect Error: {$e->getMessage()}", $code, $e);
}

Expand All @@ -154,13 +153,10 @@ public function getCurrentSchema(): string|false
$this->connect();
}

if (null === $this->resource) {
throw new Exception\RuntimeException(
'Cannot query current schema without a connected resource; call connect() first.',
);
}
/** @var PDO $resource */
$resource = $this->resource;

$result = $this->resource->query('SELECT DATABASE()');
$result = $resource->query('SELECT DATABASE()');
if (! $result instanceof PDOStatement) {
return false;
}
Expand All @@ -186,4 +182,25 @@ public function getLastGeneratedValue(?string $name = null): string|int|false|nu

return false;
}

/**
* Return a value that is safe to interpolate into a generated DSN.
*
* @todo Promote to AbstractPdoConnection in php-db/phpdb as a protected method once a second
* PDO driver package needs it — the validation is generic to all semicolon-delimited
* PDO DSN formats and has no MySQL-specific dependencies.
*
* @throws Exception\InvalidConnectionParametersException If the value contains DSN control characters.
*/
private function getDsnParameter(string $name, string $value): string
{
if (preg_match('/[;\x00-\x1f]/', $value) === 1) {
throw new Exception\InvalidConnectionParametersException(
sprintf('The "%s" connection parameter contains invalid characters', $name),
$this->connectionParameters,
);
}

return $value;
}
}
7 changes: 1 addition & 6 deletions src/Pdo/Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use Override;
use PDO;
use PDOStatement;
use PhpDb\Adapter\Driver\Feature\DriverFeatureProviderInterface;
use PhpDb\Adapter\Driver\Pdo\AbstractPdo;
use PhpDb\Adapter\Driver\Pdo\Result;
use PhpDb\Adapter\Driver\Pdo\Statement;
Expand All @@ -24,6 +23,7 @@ final class Driver extends AbstractPdo
/**
* @param array<string, mixed> $features
*/
// @mago-expect analysis:unused-parameter
public function __construct(
(PdoConnectionInterface&PdoDriverAwareInterface)|PDO $connection,
StatementInterface&PdoDriverAwareInterface $statementPrototype = new Statement(),
Expand All @@ -39,11 +39,6 @@ public function __construct(
}

$this->statementPrototype->setDriver($this);

// $features is not constructor promoted because $this->features is defined in the trait
if ([] !== $features && $this instanceof DriverFeatureProviderInterface) {
$this->addFeatures($features);
}
}

/**
Expand Down
48 changes: 36 additions & 12 deletions src/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@
use Override;
use PhpDb\Adapter\Driver\ResultInterface;
use PhpDb\Adapter\Exception;
use PhpDb\ResultSet\ResultSet;
use PhpDb\ResultSet\ResultSetInterface;
// phpcs:ignore SlevomatCodingStandard.Namespaces.UnusedUses.UnusedUse
use ReturnTypeWillChange;

use function array_combine;
use function array_fill;
use function call_user_func_array;
use function count;
Expand Down Expand Up @@ -140,6 +143,29 @@ public function getGeneratedValue(): string|int|false|null
return $this->generatedValue;
}

/**
* {@inheritDoc}
*
* @throws Exception\RuntimeException When isQueryResult() is false.
* @throws \Exception If the seeded result set rejects this result as its data source.
*/
#[Override]
public function getQueryResult(?ResultSetInterface $resultPrototype = null): ResultSetInterface
{
if (! $this->isQueryResult()) {
throw new Exception\RuntimeException(
'Cannot produce a query result set from a result that is not a query result;'
. ' check isQueryResult() first',
);
}

$resultPrototype ??= new ResultSet();
$resultSet = clone $resultPrototype;
$resultSet->initialize($this);

return $resultSet;
}

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -282,14 +308,13 @@ public function valid()
*/
protected function loadDataFromMysqliStatement(): bool
{
if (! $this->resource instanceof mysqli_stmt) {
throw new Exception\RuntimeException('Expected resource to be an instance of mysqli_stmt');
}
/** @var mysqli_stmt $statement */
$statement = $this->resource;

// build the default reference based bind structure, if it does not already exist
if (null === $this->statementBindValues['keys']) {
$this->statementBindValues['keys'] = [];
$resultResource = $this->resource->result_metadata();
$resultResource = $statement->result_metadata();
if (false === $resultResource) {
return $resultResource;
}
Expand All @@ -307,24 +332,23 @@ protected function loadDataFromMysqliStatement(): bool
foreach ($this->statementBindValues['values'] as $i => &$f) {
$refs[$i] = &$f;
}
call_user_func_array([$this->resource, 'bind_result'], $this->statementBindValues['values']);
call_user_func_array([$statement, 'bind_result'], $this->statementBindValues['values']);
}

if (($r = $this->resource->fetch()) === null) {
if (($r = $statement->fetch()) === null) {
if (! $this->isBuffered) {
$this->resource->close();
$statement->close();
}
return false;
}

if (! $r) {
throw new Exception\RuntimeException($this->resource->error);
throw new Exception\RuntimeException($statement->error);
}

// dereference
for ($i = 0, $count = count($this->statementBindValues['keys']); $i < $count; $i++) {
$this->currentData[$this->statementBindValues['keys'][$i]] = $this->statementBindValues['values'][$i];
}
// dereference: values was filled to the same length as keys when the bindings were built
$this->currentData = array_combine($this->statementBindValues['keys'], $this->statementBindValues['values']);

$this->currentComplete = true;
$this->nextComplete = true;
$this->position++;
Expand Down
17 changes: 17 additions & 0 deletions src/Sql/ColumnFormatEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace PhpDb\Mysql\Sql;

/**
* Keywords accepted by the COLUMN_FORMAT column option.
*
* @see https://dev.mysql.com/doc/refman/8.4/en/create-table.html
*/
enum ColumnFormatEnum: string
{
case Fixed = 'FIXED';
case Dynamic = 'DYNAMIC';
case Default = 'DEFAULT';
}
Loading
Loading