From 621ad565ec823a309d798611371d01bb0ba24fa9 Mon Sep 17 00:00:00 2001 From: Simon Mundy Date: Mon, 5 Oct 2026 11:04:02 +1100 Subject: [PATCH] Describe any kind of table in Sql's foreign-table exception Sql::delete(), insert(), select() and update() passed their table to forForeignTable(string), which raised a TypeError when the table was a TableIdentifier or an aliased array. forForeignTable() now accepts all three and describes a TableIdentifier as schema.table and an aliased array as "table AS alias". Signed-off-by: Simon Mundy --- analyzer-baseline.toml | 12 +++---- .../Exception/InvalidArgumentException.php | 34 +++++++++++++++++-- .../InvalidArgumentExceptionTest.php | 30 ++++++++++++++++ test/unit/Sql/SqlTest.php | 29 ++++++++++++++++ 4 files changed, 97 insertions(+), 8 deletions(-) diff --git a/analyzer-baseline.toml b/analyzer-baseline.toml index 6a0f82f4..57a96b7e 100644 --- a/analyzer-baseline.toml +++ b/analyzer-baseline.toml @@ -4560,6 +4560,12 @@ code = "imprecise-type" message = "Type `array` in return type of `getTable` is imprecise, equivalent to `array`." count = 1 +[[issues]] +file = "src/Sql/Sql.php" +code = "less-specific-argument" +message = 'Argument type mismatch for argument #1 of `PhpDb\Sql\Exception\InvalidArgumentException::forForeignTable`: expected `PhpDb\Sql\TableIdentifier|array|string`, but provided type `PhpDb\Sql\TableIdentifier|array|string` is less specific.' +count = 4 + [[issues]] file = "src/Sql/Sql.php" code = "overly-wide-return-type" @@ -4578,12 +4584,6 @@ code = "possibly-invalid-argument" message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\Delete::__construct`: expected `PhpDb\Sql\TableIdentifier|null|string`, but possibly received `PhpDb\Sql\TableIdentifier|array|null|string`.' count = 1 -[[issues]] -file = "src/Sql/Sql.php" -code = "possibly-invalid-argument" -message = 'Possible argument type mismatch for argument #1 of `PhpDb\Sql\Exception\InvalidArgumentException::forForeignTable`: expected `string`, but possibly received `PhpDb\Sql\TableIdentifier|array|string`.' -count = 4 - [[issues]] file = "src/Sql/Sql.php" code = "possibly-invalid-argument" diff --git a/src/Sql/Exception/InvalidArgumentException.php b/src/Sql/Exception/InvalidArgumentException.php index 14b7f39f..730e4c17 100644 --- a/src/Sql/Exception/InvalidArgumentException.php +++ b/src/Sql/Exception/InvalidArgumentException.php @@ -5,7 +5,11 @@ namespace PhpDb\Sql\Exception; use PhpDb\Exception; +use PhpDb\Sql\TableIdentifier; +use function array_key_first; +use function get_debug_type; +use function is_string; use function sprintf; class InvalidArgumentException extends Exception\InvalidArgumentException @@ -111,9 +115,12 @@ public static function forEmptyTable(): self return new self(self::EMPTY_TABLE); } - public static function forForeignTable(string $table): self + /** + * @param string|TableIdentifier|array $table + */ + public static function forForeignTable(string|TableIdentifier|array $table): self { - return new self(sprintf(self::FOREIGN_TABLE, $table)); + return new self(sprintf(self::FOREIGN_TABLE, self::describeTable($table))); } public static function forInvalidArgumentType(): self @@ -195,4 +202,27 @@ public static function forValuesWithMergeFlag(): self { return new self(self::VALUES_WITH_MERGE_FLAG); } + + /** + * @param string|TableIdentifier|array $table + */ + private static function describeTable(string|TableIdentifier|array $table): string + { + if (is_string($table)) { + return $table; + } + + if ($table instanceof TableIdentifier) { + [$name, $schema] = $table->getTableAndSchema(); + + return null === $schema ? $name : "{$schema}.{$name}"; + } + + $alias = (string) array_key_first($table); + $target = $table[$alias] ?? null; + + return null === $target + ? get_debug_type($target) + : self::describeTable($target) . " AS {$alias}"; + } } diff --git a/test/unit/Sql/Exception/InvalidArgumentExceptionTest.php b/test/unit/Sql/Exception/InvalidArgumentExceptionTest.php index 83ed79ab..39ff8fd2 100644 --- a/test/unit/Sql/Exception/InvalidArgumentExceptionTest.php +++ b/test/unit/Sql/Exception/InvalidArgumentExceptionTest.php @@ -6,6 +6,7 @@ use PhpDb\Exception\ExceptionInterface; use PhpDb\Sql\Exception\InvalidArgumentException; +use PhpDb\Sql\TableIdentifier; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; @@ -26,6 +27,7 @@ #[CoversMethod(InvalidArgumentException::class, 'forEmptySeparator')] #[CoversMethod(InvalidArgumentException::class, 'forEmptyTable')] #[CoversMethod(InvalidArgumentException::class, 'forForeignTable')] +#[CoversMethod(InvalidArgumentException::class, 'describeTable')] #[CoversMethod(InvalidArgumentException::class, 'forInvalidArgumentType')] #[CoversMethod(InvalidArgumentException::class, 'forInvalidFromArray')] #[CoversMethod(InvalidArgumentException::class, 'forInvalidJoinName')] @@ -41,6 +43,19 @@ #[CoversMethod(InvalidArgumentException::class, 'forValuesWithMergeFlag')] final class InvalidArgumentExceptionTest extends TestCase { + /** @return array, string}> */ + public static function foreignTableProvider(): array + { + return [ + 'table name' => ['users', 'users'], + 'table identifier' => [new TableIdentifier('users'), 'users'], + 'table identifier with schema' => [new TableIdentifier('users', 'app'), 'app.users'], + 'aliased table name' => [['u' => 'users'], 'users AS u'], + 'aliased table identifier' => [['u' => new TableIdentifier('users', 'app')], 'app.users AS u'], + 'empty array' => [[], 'null'], + ]; + } + /** @return array, string}> */ public static function namedConstructorProvider(): array { @@ -168,6 +183,21 @@ public static function namedConstructorProvider(): array ]; } + /** + * @param string|TableIdentifier|array $table + */ + #[Test] + #[DataProvider('foreignTableProvider')] + public function forForeignTableDescribesEachKindOfTable( + string|TableIdentifier|array $table, + string $described, + ): void { + self::assertSame( + sprintf(InvalidArgumentException::FOREIGN_TABLE, $described), + InvalidArgumentException::forForeignTable($table)->getMessage(), + ); + } + /** @param list $arguments */ #[Test] #[DataProvider('namedConstructorProvider')] diff --git a/test/unit/Sql/SqlTest.php b/test/unit/Sql/SqlTest.php index 42907641..e49610e9 100644 --- a/test/unit/Sql/SqlTest.php +++ b/test/unit/Sql/SqlTest.php @@ -22,6 +22,7 @@ use PhpDb\Sql\Update; use PhpDbTest\TestAsset; use PHPUnit\Framework\Attributes\CoversMethod; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\MockObject\MockObject; @@ -49,6 +50,16 @@ final class SqlTest extends TestCase */ protected Sql $sql; + /** @return array|TableIdentifier, string}> */ + public static function foreignTableProvider(): array + { + return [ + 'table identifier with schema' => [new TableIdentifier('foo', 'sch'), 'sch.foo'], + 'aliased table name' => [['f' => 'foo'], 'foo AS f'], + 'aliased table identifier' => [['f' => new TableIdentifier('foo')], 'foo AS f'], + ]; + } + // @codingStandardsIgnoreStart #[Test] public function _construct(): void @@ -121,6 +132,24 @@ public function deleteThrowsWhenTableConflicts(): void $this->sql->delete(new TableIdentifier('bar')); } + /** + * @param array|TableIdentifier $table + */ + #[Test] + #[DataProvider('foreignTableProvider')] + public function foreignTableMessageDescribesTheConstructedTable( + array|TableIdentifier $table, + string $described, + ): void { + $sql = new Sql($this->mockAdapter, $table); + + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage( + "This Sql object is intended to work with only the table \"{$described}\" provided at construction time.", + ); + $sql->select('bar'); + } + #[Test] public function getSqlPlatformReturnsPlatformDecorator(): void {