From 217789d97716e200620e4ca9e2f1f80c227cac52 Mon Sep 17 00:00:00 2001 From: Jacob Baker-Kretzmar Date: Wed, 10 Jul 2024 15:49:27 -0400 Subject: [PATCH] [11.x] Add ability to configure SQLite `busy_timeout`, `journal_mode`, and `synchronous` pragmas (#52052) * Add tests for SQLite `busy_timeout` config option * Add `busy_timeout` config option * Add support for setting the SQLite `busy_timeout` connection option * Wip * Add tests for setting `journal_mode` and `synchronous` * Add `journal_mode` and `synchronous` * Wip * formatting --------- Co-authored-by: Taylor Otwell --- config/database.php | 3 + src/Illuminate/Database/SQLiteConnection.php | 91 ++++++++++++++++--- .../Schema/Grammars/SQLiteGrammar.php | 53 ++++++++++- .../Database/Schema/SQLiteBuilder.php | 39 ++++++++ .../DatabaseConnectionFactoryTest.php | 23 +++++ .../Database/SchemaBuilderTest.php | 15 +++ tests/Support/ConfigurationUrlParserTest.php | 24 +++++ 7 files changed, 233 insertions(+), 15 deletions(-) diff --git a/config/database.php b/config/database.php index f8e8dcb8a6c3..125949ed5a15 100644 --- a/config/database.php +++ b/config/database.php @@ -37,6 +37,9 @@ 'database' => env('DB_DATABASE', database_path('database.sqlite')), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, ], 'mysql' => [ diff --git a/src/Illuminate/Database/SQLiteConnection.php b/src/Illuminate/Database/SQLiteConnection.php index 07ca896e7ebf..ce9286fa2911 100755 --- a/src/Illuminate/Database/SQLiteConnection.php +++ b/src/Illuminate/Database/SQLiteConnection.php @@ -25,7 +25,20 @@ public function __construct($pdo, $database = '', $tablePrefix = '', array $conf { parent::__construct($pdo, $database, $tablePrefix, $config); - $enableForeignKeyConstraints = $this->getForeignKeyConstraintsConfigurationValue(); + $this->configureForeignKeyConstraints(); + $this->configureBusyTimeout(); + $this->configureJournalMode(); + $this->configureSynchronous(); + } + + /** + * Enable or disable foreign key constraints if configured. + * + * @return void + */ + protected function configureForeignKeyConstraints(): void + { + $enableForeignKeyConstraints = $this->getConfig('foreign_key_constraints'); if ($enableForeignKeyConstraints === null) { return; @@ -44,6 +57,72 @@ public function __construct($pdo, $database = '', $tablePrefix = '', array $conf } } + /** + * Set the busy timeout if configured. + * + * @return void + */ + protected function configureBusyTimeout(): void + { + $milliseconds = $this->getConfig('busy_timeout'); + + if ($milliseconds === null) { + return; + } + + try { + $this->getSchemaBuilder()->setBusyTimeout($milliseconds); + } catch (QueryException $e) { + if (! $e->getPrevious() instanceof SQLiteDatabaseDoesNotExistException) { + throw $e; + } + } + } + + /** + * Set the journal mode if configured. + * + * @return void + */ + protected function configureJournalMode(): void + { + $mode = $this->getConfig('journal_mode'); + + if ($mode === null) { + return; + } + + try { + $this->getSchemaBuilder()->setJournalMode($mode); + } catch (QueryException $e) { + if (! $e->getPrevious() instanceof SQLiteDatabaseDoesNotExistException) { + throw $e; + } + } + } + + /** + * Set the synchronous mode if configured. + * + * @return void + */ + protected function configureSynchronous(): void + { + $mode = $this->getConfig('synchronous'); + + if ($mode === null) { + return; + } + + try { + $this->getSchemaBuilder()->setSynchronous($mode); + } catch (QueryException $e) { + if (! $e->getPrevious() instanceof SQLiteDatabaseDoesNotExistException) { + throw $e; + } + } + } + /** * Escape a binary value for safe SQL embedding. * @@ -128,14 +207,4 @@ protected function getDefaultPostProcessor() { return new SQLiteProcessor; } - - /** - * Get the database connection foreign key constraints configuration option. - * - * @return bool|null - */ - protected function getForeignKeyConstraintsConfigurationValue() - { - return $this->getConfig('foreign_key_constraints'); - } } diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 67622327eaf3..19689ab6526e 100644 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -591,7 +591,7 @@ public function compileRenameIndex(Blueprint $blueprint, Fluent $command, Connec */ public function compileEnableForeignKeyConstraints() { - return 'PRAGMA foreign_keys = ON;'; + return $this->pragma('foreign_keys', 'ON'); } /** @@ -601,7 +601,40 @@ public function compileEnableForeignKeyConstraints() */ public function compileDisableForeignKeyConstraints() { - return 'PRAGMA foreign_keys = OFF;'; + return $this->pragma('foreign_keys', 'OFF'); + } + + /** + * Compile the command to set the busy timeout. + * + * @param int $milliseconds + * @return string + */ + public function compileSetBusyTimeout($milliseconds) + { + return $this->pragma('busy_timeout', $milliseconds); + } + + /** + * Compile the command to set the journal mode. + * + * @param string $mode + * @return string + */ + public function compileSetJournalMode($mode) + { + return $this->pragma('journal_mode', $mode); + } + + /** + * Compile the command to set the synchronous mode. + * + * @param string $mode + * @return string + */ + public function compileSetSynchronous($mode) + { + return $this->pragma('synchronous', $mode); } /** @@ -611,7 +644,7 @@ public function compileDisableForeignKeyConstraints() */ public function compileEnableWriteableSchema() { - return 'PRAGMA writable_schema = 1;'; + return $this->pragma('writable_schema', 1); } /** @@ -621,7 +654,19 @@ public function compileEnableWriteableSchema() */ public function compileDisableWriteableSchema() { - return 'PRAGMA writable_schema = 0;'; + return $this->pragma('writable_schema', 0); + } + + /** + * Get the SQL to set a PRAGMA value. + * + * @param string $name + * @param mixed $value + * @return string + */ + protected function pragma(string $name, mixed $value): string + { + return sprintf('PRAGMA %s = %s;', $name, $value); } /** diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index 8c14e4dfbae7..5295584ea12f 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -104,6 +104,45 @@ public function dropAllViews() $this->connection->select($this->grammar->compileRebuild()); } + /** + * Set the busy timeout. + * + * @param int $milliseconds + * @return bool + */ + public function setBusyTimeout($milliseconds) + { + return $this->connection->statement( + $this->grammar->compileSetBusyTimeout($milliseconds) + ); + } + + /** + * Set the journal mode. + * + * @param string $mode + * @return bool + */ + public function setJournalMode($mode) + { + return $this->connection->statement( + $this->grammar->compileSetJournalMode($mode) + ); + } + + /** + * Set the synchronous mode. + * + * @param int $mode + * @return bool + */ + public function setSynchronous($mode) + { + return $this->connection->statement( + $this->grammar->compileSetSynchronous($mode) + ); + } + /** * Empty the database file. * diff --git a/tests/Database/DatabaseConnectionFactoryTest.php b/tests/Database/DatabaseConnectionFactoryTest.php index c4ba720948cf..a8b9782ad62d 100755 --- a/tests/Database/DatabaseConnectionFactoryTest.php +++ b/tests/Database/DatabaseConnectionFactoryTest.php @@ -144,4 +144,27 @@ public function testSqliteForeignKeyConstraints() $this->assertEquals(1, $this->db->getConnection('constraints_set')->select('PRAGMA foreign_keys')[0]->foreign_keys); } + + public function testSqliteBusyTimeout() + { + $this->db->addConnection([ + 'url' => 'sqlite:///:memory:?busy_timeout=1234', + ], 'busy_timeout_set'); + + // Can't compare to 0, default value may be something else + $this->assertNotSame(1234, $this->db->getConnection()->select('PRAGMA busy_timeout')[0]->timeout); + + $this->assertSame(1234, $this->db->getConnection('busy_timeout_set')->select('PRAGMA busy_timeout')[0]->timeout); + } + + public function testSqliteSynchronous() + { + $this->db->addConnection([ + 'url' => 'sqlite:///:memory:?synchronous=NORMAL', + ], 'synchronous_set'); + + $this->assertSame(2, $this->db->getConnection()->select('PRAGMA synchronous')[0]->synchronous); + + $this->assertSame(1, $this->db->getConnection('synchronous_set')->select('PRAGMA synchronous')[0]->synchronous); + } } diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index 9022277aee63..b0c1f67f3c9a 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -831,6 +831,21 @@ public function testAddAndDropPrimaryOnSqlite() $this->assertTrue(Schema::hasIndex('posts', ['user_name'], 'unique')); } + public function testSetJournalModeOnSqlite() + { + if ($this->driver !== 'sqlite') { + $this->markTestSkipped('Test requires a SQLite connection.'); + } + + file_put_contents(DB::connection('sqlite')->getConfig('database'), ''); + + $this->assertSame('delete', DB::connection('sqlite')->select('PRAGMA journal_mode')[0]->journal_mode); + + Schema::connection('sqlite')->setJournalMode('WAL'); + + $this->assertSame('wal', DB::connection('sqlite')->select('PRAGMA journal_mode')[0]->journal_mode); + } + public function testAddingMacros() { Schema::macro('foo', fn () => 'foo'); diff --git a/tests/Support/ConfigurationUrlParserTest.php b/tests/Support/ConfigurationUrlParserTest.php index b641f51e43a3..7c99d581d1da 100644 --- a/tests/Support/ConfigurationUrlParserTest.php +++ b/tests/Support/ConfigurationUrlParserTest.php @@ -257,6 +257,30 @@ public static function databaseUrls() 'foreign_key_constraints' => true, ], ], + 'Sqlite with busy_timeout' => [ + 'sqlite:////absolute/path/to/database.sqlite?busy_timeout=5000', + [ + 'driver' => 'sqlite', + 'database' => '/absolute/path/to/database.sqlite', + 'busy_timeout' => 5000, + ], + ], + 'Sqlite with journal_mode' => [ + 'sqlite:////absolute/path/to/database.sqlite?journal_mode=WAL', + [ + 'driver' => 'sqlite', + 'database' => '/absolute/path/to/database.sqlite', + 'journal_mode' => 'WAL', + ], + ], + 'Sqlite with synchronous' => [ + 'sqlite:////absolute/path/to/database.sqlite?synchronous=NORMAL', + [ + 'driver' => 'sqlite', + 'database' => '/absolute/path/to/database.sqlite', + 'synchronous' => 'NORMAL', + ], + ], 'Most complex example with read and write subarrays all in string' => [ 'mysql://root:@null/database?read[host][]=192.168.1.1&write[host][]=196.168.1.2&sticky=true&charset=utf8mb4&collation=utf8mb4_unicode_ci&prefix=',