Skip to content

Commit ca7fda7

Browse files
authored
Merge pull request #66 from webrium/fix/sqlite-foreign-key-enforcement
fix(sqlite): enforce foreign key constraints on connect
2 parents 6ce5fb3 + 6997e99 commit ca7fda7

4 files changed

Lines changed: 57 additions & 41 deletions

File tree

src/Connection/Connection.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,10 @@ protected function createPdo(array $config): PDO
683683
$this->setMysqlCharset($pdo, $config);
684684
}
685685

686+
if ($driver === Config::SQLITE) {
687+
$this->enableSqliteForeignKeys($pdo);
688+
}
689+
686690
return $pdo;
687691
}
688692

@@ -737,4 +741,18 @@ protected function setMysqlCharset(PDO $pdo, array $config): void
737741

738742
$pdo->exec("SET NAMES '{$charset}' COLLATE '{$collation}'");
739743
}
744+
745+
/**
746+
* Enable foreign key constraint enforcement for SQLite.
747+
*
748+
* SQLite ignores FOREIGN KEY constraints by default; each connection
749+
* must opt in explicitly.
750+
*
751+
* @param PDO $pdo
752+
* @return void
753+
*/
754+
protected function enableSqliteForeignKeys(PDO $pdo): void
755+
{
756+
$pdo->exec('PRAGMA foreign_keys = ON');
757+
}
740758
}

src/Schema.php

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,8 @@ public static function create(string $table, callable $callback, ?string $connec
7474
$conn->statement($idx);
7575
}
7676

77-
// Foreign keys are already inlined as column-level CONSTRAINTs by
78-
// compileCreate() above (see SchemaGrammar::compileCreate()), so
79-
// they must NOT be re-emitted here via compileForeignKeys() — doing
80-
// so duplicates every constraint as a separate ALTER TABLE ADD
81-
// CONSTRAINT statement, which MySQL rejects (errno 121, duplicate
82-
// constraint) and SQLite rejects outright (ALTER TABLE ADD
83-
// CONSTRAINT isn't valid SQLite DDL). compileForeignKeys() is still
84-
// used correctly by Schema::table() below, where the table (and
85-
// its inline constraints) already exist.
77+
// Foreign keys are already inlined into the CREATE TABLE statement
78+
// by compileCreate(); do not re-emit them via compileForeignKeys().
8679

8780
// PostgreSQL column comments (separate COMMENT ON COLUMN statements)
8881
if ($grammar instanceof PostgresSchemaGrammar) {

tests/Integration/SchemaForeignKeyTest.php

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,8 @@
99

1010
/**
1111
* Regression coverage for Schema::create() double-emitting foreign key
12-
* constraints.
13-
*
14-
* Schema::create() compiles the CREATE TABLE statement (which already
15-
* inlines every `$table->foreign()` as a column-level CONSTRAINT, see
16-
* SchemaGrammar::compileCreate()) and then, unconditionally, ALSO runs
17-
* SchemaGrammar::compileForeignKeys() — which re-emits the very same
18-
* constraints as separate `ALTER TABLE ... ADD CONSTRAINT` statements.
19-
* No grammar overrides compileForeignKeys() to suppress this for the
20-
* "already inlined at CREATE TABLE time" case, so every driver executes
21-
* a duplicate constraint statement:
22-
*
23-
* - MySQL: rejects the duplicate constraint name (errno 121,
24-
* "Duplicate key on write or update").
25-
* - SQLite: has no `ALTER TABLE ... ADD CONSTRAINT` syntax at all, so
26-
* the statement is a hard syntax error.
27-
*
28-
* A table defined with `$table->foreign()` therefore cannot be created
29-
* via Schema::create() on any driver.
12+
* constraints (once inlined in CREATE TABLE, once more via a redundant
13+
* ALTER TABLE ADD CONSTRAINT).
3014
*/
3115
class SchemaForeignKeyTest extends IntegrationTestCase
3216
{
@@ -54,19 +38,8 @@ public function testCreateWithForeignKeyDoesNotThrow(): void
5438
}
5539
}
5640

57-
/**
58-
* SQLite never enforces foreign keys unless the connection issues
59-
* `PRAGMA foreign_keys = ON` — this library does not do so, which is a
60-
* separate, pre-existing gap unrelated to the double-emission bug this
61-
* file targets. Skipped here rather than silently asserting something
62-
* false for that driver.
63-
*/
6441
public function testCreateWithForeignKeyActuallyEnforcesTheConstraint(): void
6542
{
66-
if (strtolower((string) (getenv('DB_DRIVER') ?: 'sqlite')) === 'sqlite') {
67-
$this->markTestSkipped('SQLite FK enforcement requires PRAGMA foreign_keys=ON, which this library does not set (separate gap).');
68-
}
69-
7043
$parent = 'sfk_test_parents2';
7144
$child = 'sfk_test_children2';
7245

@@ -81,9 +54,6 @@ public function testCreateWithForeignKeyActuallyEnforcesTheConstraint(): void
8154
$t->foreign('parent_id')->references('id')->on($parent);
8255
});
8356

84-
// A reference to a non-existent parent row must be rejected —
85-
// proof the constraint is really enforced by the DB, not just
86-
// that CREATE TABLE happened to succeed some other way.
8757
$threw = false;
8858
try {
8959
\Foxdb\DB::table($child)->insert(['parent_id' => 999999]);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Foxdb\Tests\Integration;
6+
7+
use Foxdb\DB;
8+
9+
/**
10+
* SQLite does not enforce FOREIGN KEY constraints unless a connection
11+
* issues `PRAGMA foreign_keys = ON`. Connection::createPdo() must set
12+
* this for every SQLite connection it opens.
13+
*
14+
* Run:
15+
* DB_DRIVER=sqlite vendor/bin/phpunit --testsuite=integration
16+
* --filter=SqliteForeignKeyPragmaTest
17+
*/
18+
class SqliteForeignKeyPragmaTest extends IntegrationTestCase
19+
{
20+
protected function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
if (strtolower((string) (getenv('DB_DRIVER') ?: 'sqlite')) !== 'sqlite') {
25+
$this->markTestSkipped('SQLite-specific.');
26+
}
27+
}
28+
29+
public function testForeignKeysPragmaIsEnabledOnConnect(): void
30+
{
31+
$row = DB::connection()->selectOne('PRAGMA foreign_keys');
32+
33+
$this->assertSame(1, (int) ((array) $row)['foreign_keys']);
34+
}
35+
}

0 commit comments

Comments
 (0)