From c91014330e7770d8857177b0ee5c3de0eb3e4015 Mon Sep 17 00:00:00 2001 From: Jacob Baker-Kretzmar Date: Sun, 7 Jul 2024 15:18:50 -0400 Subject: [PATCH 1/8] Add tests for SQLite `busy_timeout` config option --- tests/Database/DatabaseConnectionFactoryTest.php | 12 ++++++++++++ tests/Support/ConfigurationUrlParserTest.php | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/Database/DatabaseConnectionFactoryTest.php b/tests/Database/DatabaseConnectionFactoryTest.php index c4ba720948cf..f45a487b78e1 100755 --- a/tests/Database/DatabaseConnectionFactoryTest.php +++ b/tests/Database/DatabaseConnectionFactoryTest.php @@ -144,4 +144,16 @@ 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); + } } diff --git a/tests/Support/ConfigurationUrlParserTest.php b/tests/Support/ConfigurationUrlParserTest.php index b641f51e43a3..a91c3fb6375b 100644 --- a/tests/Support/ConfigurationUrlParserTest.php +++ b/tests/Support/ConfigurationUrlParserTest.php @@ -257,6 +257,14 @@ 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, + ], + ], '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=', From 172e66704524b2c3c049bc8101051fac409f96b1 Mon Sep 17 00:00:00 2001 From: Jacob Baker-Kretzmar Date: Sun, 7 Jul 2024 15:19:00 -0400 Subject: [PATCH 2/8] Add `busy_timeout` config option --- config/database.php | 1 + 1 file changed, 1 insertion(+) diff --git a/config/database.php b/config/database.php index f8e8dcb8a6c3..3217612fadaf 100644 --- a/config/database.php +++ b/config/database.php @@ -37,6 +37,7 @@ 'database' => env('DB_DATABASE', database_path('database.sqlite')), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => env('DB_BUSY_TIMEOUT'), ], 'mysql' => [ From 4355011e4359ca70727e49cc4573408084da064a Mon Sep 17 00:00:00 2001 From: Jacob Baker-Kretzmar Date: Sun, 7 Jul 2024 15:19:37 -0400 Subject: [PATCH 3/8] Add support for setting the SQLite `busy_timeout` connection option --- src/Illuminate/Database/SQLiteConnection.php | 64 +++++++++++++------ .../Schema/Grammars/SQLiteGrammar.php | 11 ++++ .../Database/Schema/SQLiteBuilder.php | 13 ++++ 3 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/Illuminate/Database/SQLiteConnection.php b/src/Illuminate/Database/SQLiteConnection.php index 07ca896e7ebf..5330f1acf1ca 100755 --- a/src/Illuminate/Database/SQLiteConnection.php +++ b/src/Illuminate/Database/SQLiteConnection.php @@ -25,23 +25,9 @@ public function __construct($pdo, $database = '', $tablePrefix = '', array $conf { parent::__construct($pdo, $database, $tablePrefix, $config); - $enableForeignKeyConstraints = $this->getForeignKeyConstraintsConfigurationValue(); + $this->configureForeignKeyConstraints(); - if ($enableForeignKeyConstraints === null) { - return; - } - - $schemaBuilder = $this->getSchemaBuilder(); - - try { - $enableForeignKeyConstraints - ? $schemaBuilder->enableForeignKeyConstraints() - : $schemaBuilder->disableForeignKeyConstraints(); - } catch (QueryException $e) { - if (! $e->getPrevious() instanceof SQLiteDatabaseDoesNotExistException) { - throw $e; - } - } + $this->configureBusyTimeout(); } /** @@ -130,12 +116,50 @@ protected function getDefaultPostProcessor() } /** - * Get the database connection foreign key constraints configuration option. + * Enable or disable foreign key constraints if configured. * - * @return bool|null + * @return void */ - protected function getForeignKeyConstraintsConfigurationValue() + protected function configureForeignKeyConstraints(): void { - return $this->getConfig('foreign_key_constraints'); + $enableForeignKeyConstraints = $this->getConfig('foreign_key_constraints'); + + if ($enableForeignKeyConstraints === null) { + return; + } + + $schemaBuilder = $this->getSchemaBuilder(); + + try { + $enableForeignKeyConstraints + ? $schemaBuilder->enableForeignKeyConstraints() + : $schemaBuilder->disableForeignKeyConstraints(); + } catch (QueryException $e) { + if (! $e->getPrevious() instanceof SQLiteDatabaseDoesNotExistException) { + throw $e; + } + } + } + + /** + * 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; + } + } } } diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 67622327eaf3..63b16596f643 100644 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -604,6 +604,17 @@ public function compileDisableForeignKeyConstraints() return 'PRAGMA foreign_keys = OFF;'; } + /** + * Compile the command to set the busy timeout. + * + * @param int $milliseconds + * @return string + */ + public function compileSetBusyTimeout($milliseconds) + { + return sprintf('PRAGMA busy_timeout = %s;', $milliseconds); + } + /** * Compile the SQL needed to enable a writable schema. * diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index 8c14e4dfbae7..3e4982185374 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -104,6 +104,19 @@ 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) + ); + } + /** * Empty the database file. * From e6360d440bc19ef68b948292e3631ea865825f7b Mon Sep 17 00:00:00 2001 From: Jacob Baker-Kretzmar Date: Sun, 7 Jul 2024 16:02:21 -0400 Subject: [PATCH 4/8] Wip --- config/database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/database.php b/config/database.php index 3217612fadaf..9eb3abe334d1 100644 --- a/config/database.php +++ b/config/database.php @@ -37,7 +37,7 @@ 'database' => env('DB_DATABASE', database_path('database.sqlite')), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), - 'busy_timeout' => env('DB_BUSY_TIMEOUT'), + 'busy_timeout' => null, ], 'mysql' => [ From 9f5563b8c405e7ee0d3acf8a3050f418d4264a8e Mon Sep 17 00:00:00 2001 From: Jacob Baker-Kretzmar Date: Sun, 7 Jul 2024 17:34:47 -0400 Subject: [PATCH 5/8] Add tests for setting `journal_mode` and `synchronous` --- tests/Database/DatabaseConnectionFactoryTest.php | 11 +++++++++++ tests/Integration/Database/SchemaBuilderTest.php | 15 +++++++++++++++ tests/Support/ConfigurationUrlParserTest.php | 16 ++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/tests/Database/DatabaseConnectionFactoryTest.php b/tests/Database/DatabaseConnectionFactoryTest.php index f45a487b78e1..a8b9782ad62d 100755 --- a/tests/Database/DatabaseConnectionFactoryTest.php +++ b/tests/Database/DatabaseConnectionFactoryTest.php @@ -156,4 +156,15 @@ public function testSqliteBusyTimeout() $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 a91c3fb6375b..7c99d581d1da 100644 --- a/tests/Support/ConfigurationUrlParserTest.php +++ b/tests/Support/ConfigurationUrlParserTest.php @@ -265,6 +265,22 @@ public static function databaseUrls() '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=', From c7e0ca4af4846be3691258084ba51ce9997d6475 Mon Sep 17 00:00:00 2001 From: Jacob Baker-Kretzmar Date: Sun, 7 Jul 2024 17:35:46 -0400 Subject: [PATCH 6/8] Add `journal_mode` and `synchronous` --- config/database.php | 2 + src/Illuminate/Database/SQLiteConnection.php | 48 +++++++++++++++++++ .../Schema/Grammars/SQLiteGrammar.php | 36 +++++++++++++- .../Database/Schema/SQLiteBuilder.php | 26 ++++++++++ 4 files changed, 111 insertions(+), 1 deletion(-) diff --git a/config/database.php b/config/database.php index 9eb3abe334d1..125949ed5a15 100644 --- a/config/database.php +++ b/config/database.php @@ -38,6 +38,8 @@ '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 5330f1acf1ca..2492fe4079a5 100755 --- a/src/Illuminate/Database/SQLiteConnection.php +++ b/src/Illuminate/Database/SQLiteConnection.php @@ -28,6 +28,10 @@ public function __construct($pdo, $database = '', $tablePrefix = '', array $conf $this->configureForeignKeyConstraints(); $this->configureBusyTimeout(); + + $this->configureJournalMode(); + + $this->configureSynchronous(); } /** @@ -162,4 +166,48 @@ protected function configureBusyTimeout(): void } } } + + /** + * 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; + } + } + } } diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 63b16596f643..9cc3acccf51d 100644 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -612,7 +612,29 @@ public function compileDisableForeignKeyConstraints() */ public function compileSetBusyTimeout($milliseconds) { - return sprintf('PRAGMA busy_timeout = %s;', $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); } /** @@ -635,6 +657,18 @@ public function compileDisableWriteableSchema() return '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); + } + /** * Create the column definition for a char type. * diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index 3e4982185374..5295584ea12f 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -117,6 +117,32 @@ public function setBusyTimeout($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. * From 40d9615841aa06aee5dca292d16d024d4277ef78 Mon Sep 17 00:00:00 2001 From: Jacob Baker-Kretzmar Date: Sun, 7 Jul 2024 17:35:49 -0400 Subject: [PATCH 7/8] Wip --- src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 9cc3acccf51d..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,7 @@ public function compileEnableForeignKeyConstraints() */ public function compileDisableForeignKeyConstraints() { - return 'PRAGMA foreign_keys = OFF;'; + return $this->pragma('foreign_keys', 'OFF'); } /** @@ -644,7 +644,7 @@ public function compileSetSynchronous($mode) */ public function compileEnableWriteableSchema() { - return 'PRAGMA writable_schema = 1;'; + return $this->pragma('writable_schema', 1); } /** @@ -654,7 +654,7 @@ public function compileEnableWriteableSchema() */ public function compileDisableWriteableSchema() { - return 'PRAGMA writable_schema = 0;'; + return $this->pragma('writable_schema', 0); } /** From 1759dc8538487a9ad1b74a96d1196f208938fd62 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 10 Jul 2024 14:45:30 -0500 Subject: [PATCH 8/8] formatting --- src/Illuminate/Database/SQLiteConnection.php | 173 +++++++++---------- 1 file changed, 85 insertions(+), 88 deletions(-) diff --git a/src/Illuminate/Database/SQLiteConnection.php b/src/Illuminate/Database/SQLiteConnection.php index 2492fe4079a5..ce9286fa2911 100755 --- a/src/Illuminate/Database/SQLiteConnection.php +++ b/src/Illuminate/Database/SQLiteConnection.php @@ -26,99 +26,11 @@ public function __construct($pdo, $database = '', $tablePrefix = '', array $conf parent::__construct($pdo, $database, $tablePrefix, $config); $this->configureForeignKeyConstraints(); - $this->configureBusyTimeout(); - $this->configureJournalMode(); - $this->configureSynchronous(); } - /** - * Escape a binary value for safe SQL embedding. - * - * @param string $value - * @return string - */ - protected function escapeBinary($value) - { - $hex = bin2hex($value); - - return "x'{$hex}'"; - } - - /** - * Determine if the given database exception was caused by a unique constraint violation. - * - * @param \Exception $exception - * @return bool - */ - protected function isUniqueConstraintError(Exception $exception) - { - return boolval(preg_match('#(column(s)? .* (is|are) not unique|UNIQUE constraint failed: .*)#i', $exception->getMessage())); - } - - /** - * Get the default query grammar instance. - * - * @return \Illuminate\Database\Query\Grammars\SQLiteGrammar - */ - protected function getDefaultQueryGrammar() - { - ($grammar = new QueryGrammar)->setConnection($this); - - return $this->withTablePrefix($grammar); - } - - /** - * Get a schema builder instance for the connection. - * - * @return \Illuminate\Database\Schema\SQLiteBuilder - */ - public function getSchemaBuilder() - { - if (is_null($this->schemaGrammar)) { - $this->useDefaultSchemaGrammar(); - } - - return new SQLiteBuilder($this); - } - - /** - * Get the default schema grammar instance. - * - * @return \Illuminate\Database\Schema\Grammars\SQLiteGrammar - */ - protected function getDefaultSchemaGrammar() - { - ($grammar = new SchemaGrammar)->setConnection($this); - - return $this->withTablePrefix($grammar); - } - - /** - * Get the schema state for the connection. - * - * @param \Illuminate\Filesystem\Filesystem|null $files - * @param callable|null $processFactory - * - * @throws \RuntimeException - */ - public function getSchemaState(?Filesystem $files = null, ?callable $processFactory = null) - { - return new SqliteSchemaState($this, $files, $processFactory); - } - - /** - * Get the default post processor instance. - * - * @return \Illuminate\Database\Query\Processors\SQLiteProcessor - */ - protected function getDefaultPostProcessor() - { - return new SQLiteProcessor; - } - /** * Enable or disable foreign key constraints if configured. * @@ -210,4 +122,89 @@ protected function configureSynchronous(): void } } } + + /** + * Escape a binary value for safe SQL embedding. + * + * @param string $value + * @return string + */ + protected function escapeBinary($value) + { + $hex = bin2hex($value); + + return "x'{$hex}'"; + } + + /** + * Determine if the given database exception was caused by a unique constraint violation. + * + * @param \Exception $exception + * @return bool + */ + protected function isUniqueConstraintError(Exception $exception) + { + return boolval(preg_match('#(column(s)? .* (is|are) not unique|UNIQUE constraint failed: .*)#i', $exception->getMessage())); + } + + /** + * Get the default query grammar instance. + * + * @return \Illuminate\Database\Query\Grammars\SQLiteGrammar + */ + protected function getDefaultQueryGrammar() + { + ($grammar = new QueryGrammar)->setConnection($this); + + return $this->withTablePrefix($grammar); + } + + /** + * Get a schema builder instance for the connection. + * + * @return \Illuminate\Database\Schema\SQLiteBuilder + */ + public function getSchemaBuilder() + { + if (is_null($this->schemaGrammar)) { + $this->useDefaultSchemaGrammar(); + } + + return new SQLiteBuilder($this); + } + + /** + * Get the default schema grammar instance. + * + * @return \Illuminate\Database\Schema\Grammars\SQLiteGrammar + */ + protected function getDefaultSchemaGrammar() + { + ($grammar = new SchemaGrammar)->setConnection($this); + + return $this->withTablePrefix($grammar); + } + + /** + * Get the schema state for the connection. + * + * @param \Illuminate\Filesystem\Filesystem|null $files + * @param callable|null $processFactory + * + * @throws \RuntimeException + */ + public function getSchemaState(?Filesystem $files = null, ?callable $processFactory = null) + { + return new SqliteSchemaState($this, $files, $processFactory); + } + + /** + * Get the default post processor instance. + * + * @return \Illuminate\Database\Query\Processors\SQLiteProcessor + */ + protected function getDefaultPostProcessor() + { + return new SQLiteProcessor; + } }