diff --git a/src/Connection.php b/src/Connection.php index b6ad48b4..b29a5c74 100755 --- a/src/Connection.php +++ b/src/Connection.php @@ -863,6 +863,15 @@ public function getDatatype(DataType $type): string return $this->dbDriver->getDatatype($type); } + /** + * @inheritDoc + */ + #[Override] + public function mapsToSameDatatype(DataType $a, DataType $b): bool + { + return $this->getDatatype($a) === $this->getDatatype($b); + } + /** * @inheritDoc * @throws ConnectionException diff --git a/src/ConnectionInterface.php b/src/ConnectionInterface.php index 066aac01..7d908085 100755 --- a/src/ConnectionInterface.php +++ b/src/ConnectionInterface.php @@ -194,6 +194,17 @@ public function getTableInformation(string $tableName): Table; */ public function getDatatype(DataType $type): string; + /** + * Returns true when both datatypes resolve to the same backend SQL via + * {@see self::getDatatype()}. + * + * Useful for schema-drift checks: e.g. on Postgres, TINYINT and SMALLINT + * both map to "SMALLINT", and on SQLite all integer types collapse to + * "INTEGER". A naive enum comparison would falsely report drift after a + * round-trip through such a backend. + */ + public function mapsToSameDatatype(DataType $a, DataType $b): bool; + /** * Used to send a `CREATE TABLE` statement to the database. * By passing the query through this method, the driver can add db-specific commands. diff --git a/src/Driver/MysqliDriver.php b/src/Driver/MysqliDriver.php index 2801720a..883d2f50 100755 --- a/src/Driver/MysqliDriver.php +++ b/src/Driver/MysqliDriver.php @@ -320,6 +320,14 @@ public function getTableInformation(string $tableName): Table */ private function getCoreTypeForDbType(array $infoSchemaRow): ?DataType { + if ($infoSchemaRow['Type'] === 'tinyint(4)' || $infoSchemaRow['Type'] === 'tinyint') { + return DataType::TINYINT; + } + + if ($infoSchemaRow['Type'] === 'smallint(6)' || $infoSchemaRow['Type'] === 'smallint') { + return DataType::SMALLINT; + } + if ($infoSchemaRow['Type'] === 'int(11)' || $infoSchemaRow['Type'] === 'int') { return DataType::INT; } @@ -374,6 +382,8 @@ private function getCoreTypeForDbType(array $infoSchemaRow): ?DataType public function getDatatype(DataType $type): string { return match ($type) { + DataType::TINYINT => ' TINYINT ', + DataType::SMALLINT => ' SMALLINT ', DataType::INT => ' INT ', DataType::BIGINT => ' BIGINT ', DataType::FLOAT => ' DOUBLE ', diff --git a/src/Driver/PostgresDriver.php b/src/Driver/PostgresDriver.php index 8c10881f..df93a358 100755 --- a/src/Driver/PostgresDriver.php +++ b/src/Driver/PostgresDriver.php @@ -295,6 +295,10 @@ public function getTableInformation(string $tableName): Table */ private function getCoreTypeForDbType(array $infoSchemaRow): ?DataType { + if ($infoSchemaRow['data_type'] === 'smallint') { + return DataType::SMALLINT; + } + if ($infoSchemaRow['data_type'] === 'integer') { return DataType::INT; } @@ -341,6 +345,7 @@ private function getCoreTypeForDbType(array $infoSchemaRow): ?DataType public function getDatatype(DataType $type): string { return match ($type) { + DataType::TINYINT, DataType::SMALLINT => ' SMALLINT ', DataType::INT => ' INT ', DataType::BIGINT => ' BIGINT ', DataType::FLOAT => ' NUMERIC ', diff --git a/src/Driver/Sqlite3Driver.php b/src/Driver/Sqlite3Driver.php index 064f0c73..132737ba 100755 --- a/src/Driver/Sqlite3Driver.php +++ b/src/Driver/Sqlite3Driver.php @@ -610,7 +610,7 @@ public function dbImport(string $fileName): bool public function getDatatype(DataType $type): string { return match ($type) { - DataType::INT, DataType::BIGINT => ' INTEGER ', + DataType::TINYINT, DataType::SMALLINT, DataType::INT, DataType::BIGINT => ' INTEGER ', DataType::FLOAT => ' REAL ', default => ' TEXT ', }; diff --git a/src/MockConnection.php b/src/MockConnection.php index 18605e14..e6b728e7 100644 --- a/src/MockConnection.php +++ b/src/MockConnection.php @@ -245,6 +245,12 @@ public function getDatatype(DataType $type): string return DataType::TEXT->value; } + #[Override] + public function mapsToSameDatatype(DataType $a, DataType $b): bool + { + return $this->getDatatype($a) === $this->getDatatype($b); + } + #[Override] public function createTable(string $tableName, array $columns, array $keys, array $indices = []): bool { diff --git a/src/Schema/DataType.php b/src/Schema/DataType.php index 9b4abbbf..940d2f06 100644 --- a/src/Schema/DataType.php +++ b/src/Schema/DataType.php @@ -18,6 +18,8 @@ */ enum DataType: string { + case TINYINT = 'tinyint'; + case SMALLINT = 'smallint'; case INT = 'int'; case BIGINT = 'long'; case FLOAT = 'double'; diff --git a/tests/ConnectionColumnTypeTest.php b/tests/ConnectionColumnTypeTest.php index c2f58021..956749f6 100644 --- a/tests/ConnectionColumnTypeTest.php +++ b/tests/ConnectionColumnTypeTest.php @@ -16,6 +16,8 @@ use Artemeon\Database\Exception\ConnectionException; use Artemeon\Database\Exception\QueryException; use Artemeon\Database\Exception\TableNotFoundException; +use Artemeon\Database\Schema\DataType; +use PHPUnit\Framework\Attributes\DataProvider; /** * @internal @@ -43,4 +45,23 @@ public function testTypeConversion(): void ); } } + + /** + * @return iterable + */ + public static function provideMapsToSameDatatypeCases(): iterable + { + yield 'INT == INT' => [DataType::INT, DataType::INT, true]; + yield 'TEXT == TEXT' => [DataType::TEXT, DataType::TEXT, true]; + yield 'INT vs TEXT must stay distinct on every backend' => [DataType::INT, DataType::TEXT, false]; + } + + #[DataProvider('provideMapsToSameDatatypeCases')] + public function testMapsToSameDatatype(DataType $a, DataType $b, bool $expected): void + { + $this->assertSame( + $expected, + $this->getConnection()->mapsToSameDatatype($a, $b), + ); + } } diff --git a/tests/ConnectionMultiInsertTest.php b/tests/ConnectionMultiInsertTest.php index 3637d505..6d356ca4 100644 --- a/tests/ConnectionMultiInsertTest.php +++ b/tests/ConnectionMultiInsertTest.php @@ -52,6 +52,8 @@ public function testInserts(): void for ($i = 1; $i <= 50; $i++) { $row = $connection->getPRow('SELECT * FROM ' . self::TEST_TABLE_NAME . ' WHERE temp_int = ?', [123456 + $i]); + $this->assertEquals($i % 128, $row['temp_tinyint']); + $this->assertEquals(1000 + $i, $row['temp_smallint']); $this->assertEquals(123456 + $i, $row['temp_int']); $this->assertEquals(20200508095300 + $i, $row['temp_bigint']); $this->assertIsScalar($row['temp_float']); diff --git a/tests/ConnectionTestCase.php b/tests/ConnectionTestCase.php index fb8e59c6..8625a45b 100644 --- a/tests/ConnectionTestCase.php +++ b/tests/ConnectionTestCase.php @@ -92,6 +92,8 @@ protected function getTestTableColumns(): array { return [ 'temp_id' => [DataType::CHAR20, false], + 'temp_tinyint' => [DataType::TINYINT, true], + 'temp_smallint' => [DataType::SMALLINT, true], 'temp_int' => [DataType::INT, true], 'temp_bigint' => [DataType::BIGINT, true], 'temp_float' => [DataType::FLOAT, true], @@ -119,6 +121,8 @@ protected function getRows(int $count, bool $assoc = true): array for ($i = 1; $i <= $count; $i++) { $row = [ 'temp_id' => $this->generateSystemid(), + 'temp_tinyint' => $i % 128, + 'temp_smallint' => 1000 + $i, 'temp_int' => 123456 + $i, 'temp_bigint' => 20200508095300 + $i, 'temp_float' => 23.45, @@ -144,6 +148,8 @@ protected function getColumnNames(): array { return [ 'temp_id', + 'temp_tinyint', + 'temp_smallint', 'temp_int', 'temp_bigint', 'temp_float', diff --git a/tests/Driver/MysqliDriverTest.php b/tests/Driver/MysqliDriverTest.php index fc3ed58e..abae42d8 100644 --- a/tests/Driver/MysqliDriverTest.php +++ b/tests/Driver/MysqliDriverTest.php @@ -6,6 +6,7 @@ use Artemeon\Database\ConnectionParameters; use Artemeon\Database\Driver\MysqliDriver; +use Artemeon\Database\Schema\DataType; use Mockery; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -26,6 +27,16 @@ public function testBuildsDatabaseSpecificSubstringExpression(): void self::assertEquals('SUBSTRING("test value", 1, 1)', $mysqliDriver->getSubstringExpression('"test value"', 1, 1)); } + public function testKeepsIntegerTypesDistinct(): void + { + $driver = new MysqliDriver(); + + self::assertSame(' TINYINT ', $driver->getDatatype(DataType::TINYINT)); + self::assertSame(' SMALLINT ', $driver->getDatatype(DataType::SMALLINT)); + self::assertSame(' INT ', $driver->getDatatype(DataType::INT)); + self::assertSame(' BIGINT ', $driver->getDatatype(DataType::BIGINT)); + } + /** * @return array{string,list,string,string}[] */ diff --git a/tests/Driver/PostgresDriverTest.php b/tests/Driver/PostgresDriverTest.php index d6817e31..af2279b5 100644 --- a/tests/Driver/PostgresDriverTest.php +++ b/tests/Driver/PostgresDriverTest.php @@ -6,6 +6,7 @@ use Artemeon\Database\ConnectionParameters; use Artemeon\Database\Driver\PostgresDriver; +use Artemeon\Database\Schema\DataType; use Mockery; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -26,6 +27,17 @@ public function testBuildsDatabaseSpecificSubstringExpression(): void self::assertEquals('SUBSTRING(cast ("test value" as text), 1, 1)', $postgresDriver->getSubstringExpression('"test value"', 1, 1)); } + public function testCollapsesTinyintToSmallint(): void + { + $driver = new PostgresDriver(); + + // Postgres has no 1-byte integer type, so TINYINT round-trips as SMALLINT. + self::assertSame(' SMALLINT ', $driver->getDatatype(DataType::TINYINT)); + self::assertSame(' SMALLINT ', $driver->getDatatype(DataType::SMALLINT)); + self::assertSame(' INT ', $driver->getDatatype(DataType::INT)); + self::assertSame(' BIGINT ', $driver->getDatatype(DataType::BIGINT)); + } + /** * @return array{string,list,string,string}[] */ diff --git a/tests/Driver/Sqlite3DriverTest.php b/tests/Driver/Sqlite3DriverTest.php index b8acfc50..6039a43d 100644 --- a/tests/Driver/Sqlite3DriverTest.php +++ b/tests/Driver/Sqlite3DriverTest.php @@ -5,6 +5,7 @@ namespace Artemeon\Database\Tests\Driver; use Artemeon\Database\Driver\Sqlite3Driver; +use Artemeon\Database\Schema\DataType; use PHPUnit\Framework\TestCase; /** @@ -21,4 +22,14 @@ public function testBuildsDatabaseSpecificSubstringExpression(): void self::assertEquals('SUBSTR("test value", 1)', $sqlite3Driver->getSubstringExpression('"test value"', 1, null)); self::assertEquals('SUBSTR("test value", 1, 1)', $sqlite3Driver->getSubstringExpression('"test value"', 1, 1)); } + + public function testCollapsesAllIntegerTypesToInteger(): void + { + $driver = new Sqlite3Driver(); + + self::assertSame(' INTEGER ', $driver->getDatatype(DataType::TINYINT)); + self::assertSame(' INTEGER ', $driver->getDatatype(DataType::SMALLINT)); + self::assertSame(' INTEGER ', $driver->getDatatype(DataType::INT)); + self::assertSame(' INTEGER ', $driver->getDatatype(DataType::BIGINT)); + } }