From 36b98fd2fd13a8787c04f4dc56945cacd1720747 Mon Sep 17 00:00:00 2001 From: guvra Date: Tue, 30 Dec 2025 00:04:05 +0100 Subject: [PATCH] feat(database): add foreign key support for sqlite dialect --- .../QueryStatements/BelongsToStatement.php | 4 +--- .../ConstraintNameStatement.php | 6 +---- .../QueryStatements/CreateTableStatement.php | 10 +++++++-- .../AlterTableStatementTest.php | 21 ++---------------- .../CreateTableStatementTest.php | 22 ++++++++++--------- 5 files changed, 24 insertions(+), 39 deletions(-) diff --git a/packages/database/src/QueryStatements/BelongsToStatement.php b/packages/database/src/QueryStatements/BelongsToStatement.php index 2b046a199..4692885fb 100644 --- a/packages/database/src/QueryStatements/BelongsToStatement.php +++ b/packages/database/src/QueryStatements/BelongsToStatement.php @@ -5,7 +5,6 @@ namespace Tempest\Database\QueryStatements; use Tempest\Database\Config\DatabaseDialect; -use Tempest\Database\DialectWasNotSupported; use Tempest\Database\QueryStatement; final readonly class BelongsToStatement implements QueryStatement @@ -44,7 +43,7 @@ public function compile(DatabaseDialect $dialect): string 'ON UPDATE ' . $this->onUpdate->value, )), ), - DatabaseDialect::POSTGRESQL => new ConstraintStatement( + DatabaseDialect::POSTGRESQL, DatabaseDialect::SQLITE => new ConstraintStatement( $constraintName, new RawStatement(sprintf( 'FOREIGN KEY(%s) REFERENCES %s(%s) %s %s', @@ -55,7 +54,6 @@ public function compile(DatabaseDialect $dialect): string 'ON UPDATE ' . $this->onUpdate->value, )), ), - DatabaseDialect::SQLITE => throw new DialectWasNotSupported(), }; return $statement->compile($dialect); diff --git a/packages/database/src/QueryStatements/ConstraintNameStatement.php b/packages/database/src/QueryStatements/ConstraintNameStatement.php index 1a3550943..2be4d311d 100644 --- a/packages/database/src/QueryStatements/ConstraintNameStatement.php +++ b/packages/database/src/QueryStatements/ConstraintNameStatement.php @@ -5,7 +5,6 @@ namespace Tempest\Database\QueryStatements; use Tempest\Database\Config\DatabaseDialect; -use Tempest\Database\DialectWasNotSupported; use Tempest\Database\QueryStatement; final readonly class ConstraintNameStatement implements QueryStatement @@ -21,9 +20,6 @@ public function __construct( public function compile(DatabaseDialect $dialect): string { - return match ($dialect) { - DatabaseDialect::MYSQL, DatabaseDialect::POSTGRESQL => sprintf('CONSTRAINT %s', $this->name->compile($dialect)), - default => throw new DialectWasNotSupported(), - }; + return sprintf('CONSTRAINT %s', $this->name->compile($dialect)); } } diff --git a/packages/database/src/QueryStatements/CreateTableStatement.php b/packages/database/src/QueryStatements/CreateTableStatement.php index ab3bba8d7..44416cb37 100644 --- a/packages/database/src/QueryStatements/CreateTableStatement.php +++ b/packages/database/src/QueryStatements/CreateTableStatement.php @@ -395,8 +395,14 @@ public function compile(DatabaseDialect $dialect): string 'CREATE TABLE %s (%s);', new TableDefinition($this->tableName), arr($this->statements) - // Remove BelongsTo for sqlLite as it does not support those queries - ->filter(fn (QueryStatement $queryStatement) => ! ($dialect === DatabaseDialect::SQLITE && $queryStatement instanceof BelongsToStatement)) + ->sortByCallback(function (QueryStatement $a, QueryStatement $b) { + // Foreign keys must be defined last in SQLite + if ($a instanceof BelongsToStatement && $b instanceof BelongsToStatement) { + return 0; + } + + return $a instanceof BelongsToStatement ? 1 : -1; + }) ->map(fn (QueryStatement $queryStatement) => str($queryStatement->compile($dialect))->trim()->replace(' ', ' ')) ->filter(fn (ImmutableString $str) => $str->isNotEmpty()) ->implode(', ' . PHP_EOL . ' ') diff --git a/packages/database/tests/QueryStatements/AlterTableStatementTest.php b/packages/database/tests/QueryStatements/AlterTableStatementTest.php index 547877387..4c64136ab 100644 --- a/packages/database/tests/QueryStatements/AlterTableStatementTest.php +++ b/packages/database/tests/QueryStatements/AlterTableStatementTest.php @@ -59,6 +59,7 @@ public function test_alter_add_belongs_to_mysql(DatabaseDialect $dialect): void } #[TestWith([DatabaseDialect::POSTGRESQL])] + #[TestWith([DatabaseDialect::SQLITE])] public function test_alter_add_belongs_to_postgresql(DatabaseDialect $dialect): void { $expected = 'ALTER TABLE `table` ADD CONSTRAINT `fk_parent_table_foo` FOREIGN KEY(foo) REFERENCES parent(bar) ON DELETE RESTRICT ON UPDATE NO ACTION ;'; @@ -71,16 +72,6 @@ public function test_alter_add_belongs_to_postgresql(DatabaseDialect $dialect): $this->assertEqualsIgnoringCase($expected, $normalized); } - #[TestWith([DatabaseDialect::SQLITE])] - public function test_alter_add_belongs_to_unsupported(DatabaseDialect $dialect): void - { - $this->expectException(DialectWasNotSupported::class); - - new AlterTableStatement('table') - ->add(new BelongsToStatement('table.foo', 'parent.bar')) - ->compile($dialect); - } - #[TestWith([DatabaseDialect::MYSQL])] #[TestWith([DatabaseDialect::POSTGRESQL])] #[TestWith([DatabaseDialect::SQLITE])] @@ -98,6 +89,7 @@ public function test_alter_table_drop_column(DatabaseDialect $dialect): void #[TestWith([DatabaseDialect::MYSQL, 'ALTER TABLE `table` DROP CONSTRAINT `foo` ;'])] #[TestWith([DatabaseDialect::POSTGRESQL, 'ALTER TABLE `table` DROP CONSTRAINT `foo` ;'])] + #[TestWith([DatabaseDialect::SQLITE, 'ALTER TABLE `table` DROP CONSTRAINT `foo` ;'])] public function test_alter_table_drop_constraint(DatabaseDialect $dialect, string $expected): void { $statement = new AlterTableStatement('table') @@ -109,15 +101,6 @@ public function test_alter_table_drop_constraint(DatabaseDialect $dialect, strin $this->assertEqualsIgnoringCase($expected, $normalized); } - #[TestWith([DatabaseDialect::SQLITE])] - public function test_alter_table_drop_constraint_unsupported_dialects(DatabaseDialect $dialect): void - { - $this->expectException(DialectWasNotSupported::class); - new AlterTableStatement('table') - ->dropConstraint('foo') - ->compile($dialect); - } - #[TestWith([DatabaseDialect::MYSQL, 'ALTER TABLE `table` ADD `foo` VARCHAR(42) DEFAULT \'bar\' NOT NULL ;'])] #[TestWith([ DatabaseDialect::POSTGRESQL, diff --git a/packages/database/tests/QueryStatements/CreateTableStatementTest.php b/packages/database/tests/QueryStatements/CreateTableStatementTest.php index 2d68cefe9..515bfeff0 100644 --- a/packages/database/tests/QueryStatements/CreateTableStatementTest.php +++ b/packages/database/tests/QueryStatements/CreateTableStatementTest.php @@ -98,8 +98,8 @@ public static function provide_fk_create_table_database_drivers(): Generator CREATE TABLE `books` ( `id` INTEGER PRIMARY KEY AUTO_INCREMENT, `author_id` INTEGER NOT NULL, - CONSTRAINT `fk_authors_books_author_id` FOREIGN KEY books(author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE NO ACTION, - `name` VARCHAR(255) NOT NULL + `name` VARCHAR(255) NOT NULL, + CONSTRAINT `fk_authors_books_author_id` FOREIGN KEY books(author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE NO ACTION ); SQL, ]; @@ -110,8 +110,8 @@ public static function provide_fk_create_table_database_drivers(): Generator CREATE TABLE `books` ( `id` SERIAL PRIMARY KEY, `author_id` INTEGER NOT NULL, - CONSTRAINT `fk_authors_books_author_id` FOREIGN KEY(author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE NO ACTION, - `name` VARCHAR(255) NOT NULL + `name` VARCHAR(255) NOT NULL, + CONSTRAINT `fk_authors_books_author_id` FOREIGN KEY(author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE NO ACTION ); SQL, ]; @@ -122,7 +122,8 @@ public static function provide_fk_create_table_database_drivers(): Generator CREATE TABLE `books` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `author_id` INTEGER NOT NULL, - `name` VARCHAR(255) NOT NULL + `name` VARCHAR(255) NOT NULL, + CONSTRAINT `fk_authors_books_author_id` FOREIGN KEY(author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE NO ACTION ); SQL, ]; @@ -149,8 +150,8 @@ public static function provide_fk_create_table_database_drivers_explicit(): Gene CREATE TABLE `books` ( `id` INTEGER PRIMARY KEY AUTO_INCREMENT, `author_id` INTEGER NOT NULL, - CONSTRAINT `fk_authors_books_author_id` FOREIGN KEY books(author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE NO ACTION, - `name` VARCHAR(255) NOT NULL + `name` VARCHAR(255) NOT NULL, + CONSTRAINT `fk_authors_books_author_id` FOREIGN KEY books(author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE NO ACTION ); SQL, ]; @@ -161,8 +162,8 @@ public static function provide_fk_create_table_database_drivers_explicit(): Gene CREATE TABLE `books` ( `id` SERIAL PRIMARY KEY, `author_id` INTEGER NOT NULL, - CONSTRAINT `fk_authors_books_author_id` FOREIGN KEY(author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE NO ACTION, - `name` VARCHAR(255) NOT NULL + `name` VARCHAR(255) NOT NULL, + CONSTRAINT `fk_authors_books_author_id` FOREIGN KEY(author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE NO ACTION ); SQL, ]; @@ -173,7 +174,8 @@ public static function provide_fk_create_table_database_drivers_explicit(): Gene CREATE TABLE `books` ( `id` INTEGER PRIMARY KEY AUTOINCREMENT, `author_id` INTEGER NOT NULL, - `name` VARCHAR(255) NOT NULL + `name` VARCHAR(255) NOT NULL, + CONSTRAINT `fk_authors_books_author_id` FOREIGN KEY(author_id) REFERENCES authors(id) ON DELETE CASCADE ON UPDATE NO ACTION ); SQL, ];