From 351527f8346a0c69c1ee14ba86e786a89a5e4041 Mon Sep 17 00:00:00 2001 From: Roman Konz Date: Sat, 2 May 2026 13:05:47 +0200 Subject: [PATCH 1/3] feat: Add TINYINT and SMALLINT data types New DataType cases mapped per driver: - MySQL: native TINYINT / SMALLINT (read both legacy and modern forms) - Postgres: collapse both to SMALLINT (no native 1-byte int) - SQLite: collapse to INTEGER like the other int types Also expose Connection::mapsToSameDatatype() so consumers performing schema-drift checks can compare via the backend SQL form instead of enum identity. Required for safe use of the new types on Postgres, where TINYINT silently round-trips to SMALLINT. Refs #93 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Connection.php | 9 +++++++++ src/ConnectionInterface.php | 11 +++++++++++ src/Driver/MysqliDriver.php | 10 ++++++++++ src/Driver/PostgresDriver.php | 5 +++++ src/Driver/Sqlite3Driver.php | 2 +- src/MockConnection.php | 6 ++++++ src/Schema/DataType.php | 2 ++ tests/ConnectionColumnTypeTest.php | 12 ++++++++++++ tests/ConnectionMultiInsertTest.php | 2 ++ tests/ConnectionTestCase.php | 6 ++++++ 10 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/Connection.php b/src/Connection.php index a42310b8..46a635eb 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 1dfd3df2..58a9023f 100755 --- a/src/ConnectionInterface.php +++ b/src/ConnectionInterface.php @@ -193,6 +193,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 eae6ac27..c13fa86a 100644 --- a/src/MockConnection.php +++ b/src/MockConnection.php @@ -244,6 +244,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..5dc03255 100644 --- a/tests/ConnectionColumnTypeTest.php +++ b/tests/ConnectionColumnTypeTest.php @@ -16,6 +16,7 @@ use Artemeon\Database\Exception\ConnectionException; use Artemeon\Database\Exception\QueryException; use Artemeon\Database\Exception\TableNotFoundException; +use Artemeon\Database\Schema\DataType; /** * @internal @@ -43,4 +44,15 @@ public function testTypeConversion(): void ); } } + + public function testMapsToSameDatatype(): void + { + $connection = $this->getConnection(); + + $this->assertTrue($connection->mapsToSameDatatype(DataType::INT, DataType::INT)); + $this->assertTrue($connection->mapsToSameDatatype(DataType::TEXT, DataType::TEXT)); + + // distinct types must not collapse on any backend + $this->assertFalse($connection->mapsToSameDatatype(DataType::INT, DataType::TEXT)); + } } 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', From 5615eee291d2f43d4e1af67fd3ec167ee03f8edd Mon Sep 17 00:00:00 2001 From: Roman Konz Date: Sat, 2 May 2026 13:13:45 +0200 Subject: [PATCH 2/3] test: Pin per-driver integer-type mappings Adds direct unit tests for getDatatype() on each driver covering TINYINT/SMALLINT/INT/BIGINT. Documents the collapses mapsToSameDatatype() relies on: - MySQL: all four map to distinct SQL - Postgres: TINYINT collapses to SMALLINT - SQLite: all four collapse to INTEGER Refs #93 Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/Driver/MysqliDriverTest.php | 11 +++++++++++ tests/Driver/PostgresDriverTest.php | 12 ++++++++++++ tests/Driver/Sqlite3DriverTest.php | 11 +++++++++++ 3 files changed, 34 insertions(+) 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)); + } } From 401aac8afd187c40f27cbfd7834c332eab613c37 Mon Sep 17 00:00:00 2001 From: Roman Konz Date: Sat, 2 May 2026 13:55:24 +0200 Subject: [PATCH 3/3] test: Split mapsToSameDatatype into a data provider Per review feedback: with three assertions in one method, a failure shows only "Expected false, got true" without naming the case, and later assertions don't run after the first failure. Each case now runs as its own PHPUnit test named after the data set key. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/ConnectionColumnTypeTest.php | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/ConnectionColumnTypeTest.php b/tests/ConnectionColumnTypeTest.php index 5dc03255..956749f6 100644 --- a/tests/ConnectionColumnTypeTest.php +++ b/tests/ConnectionColumnTypeTest.php @@ -17,6 +17,7 @@ use Artemeon\Database\Exception\QueryException; use Artemeon\Database\Exception\TableNotFoundException; use Artemeon\Database\Schema\DataType; +use PHPUnit\Framework\Attributes\DataProvider; /** * @internal @@ -45,14 +46,22 @@ public function testTypeConversion(): void } } - public function testMapsToSameDatatype(): void + /** + * @return iterable + */ + public static function provideMapsToSameDatatypeCases(): iterable { - $connection = $this->getConnection(); - - $this->assertTrue($connection->mapsToSameDatatype(DataType::INT, DataType::INT)); - $this->assertTrue($connection->mapsToSameDatatype(DataType::TEXT, DataType::TEXT)); + 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]; + } - // distinct types must not collapse on any backend - $this->assertFalse($connection->mapsToSameDatatype(DataType::INT, DataType::TEXT)); + #[DataProvider('provideMapsToSameDatatypeCases')] + public function testMapsToSameDatatype(DataType $a, DataType $b, bool $expected): void + { + $this->assertSame( + $expected, + $this->getConnection()->mapsToSameDatatype($a, $b), + ); } }