From e0841cfe78fd9fd398751d43433fd3c3436e59f1 Mon Sep 17 00:00:00 2001 From: AnnoyingTechnology Date: Mon, 20 Jul 2026 19:04:51 +0200 Subject: [PATCH] Use WAL for SQLite connections Configure file-backed SQLite databases for WAL with FULL synchronous writes, foreign-key enforcement, and an explicit busy timeout. Keep DELETE as the narrowly documented fallback for unsupported filesystems and verify every applied pragma in the SQLite test path.\n\nDocument WAL filesystem, backup, and patched-version requirements, including SQLite's WAL-reset advisory. --- .env | 2 + README.md | 4 ++ config/services.yaml | 8 +++ src/Doctrine/SQLiteConnectionMiddleware.php | 60 +++++++++++++++++++++ tests/Functional/SQLiteConnectionTest.php | 23 ++++++++ 5 files changed, 97 insertions(+) create mode 100644 src/Doctrine/SQLiteConnectionMiddleware.php create mode 100644 tests/Functional/SQLiteConnectionTest.php diff --git a/.env b/.env index 47f3c7ff..9e4a5951 100644 --- a/.env +++ b/.env @@ -28,6 +28,8 @@ DATABASE_DRIVER=mysql # or postgresql, or sqlite # For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=UTF-8" # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" (without the quotes so Symfony can resolve it if it's an absolute path) DATABASE_URL="mysql://davis:davis@127.0.0.1:3306/davis?serverVersion=10.9.3-MariaDB&charset=utf8mb4" +# SQLite only. Use DELETE when the database cannot reside on a local filesystem. +SQLITE_JOURNAL_MODE=WAL ###< doctrine/doctrine-bundle ### ###> symfony/mailer ### diff --git a/README.md b/README.md index 01b825c6..e83c3e31 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,10 @@ DATABASE_DRIVER=mysql # or postgresql, or sqlite DATABASE_URL=mysql://db_user:db_pass@host:3306/db_name?serverVersion=10.9.3-MariaDB&charset=utf8mb4 ``` +SQLite databases use WAL journaling, full synchronous writes, and a 60-second busy timeout by default. WAL requires the database and its auxiliary files to reside on a local filesystem. Set `SQLITE_JOURNAL_MODE=DELETE` when WAL is unavailable. + +Keep SQLite patched: SQLite reports a rare multi-connection [WAL-reset bug](https://www.sqlite.org/wal.html#the_wal_reset_bug) as likely present from 3.7.0 through 3.51.2, with fixes in 3.51.3 and the documented backports. The `-wal` file is part of a live database's state, so use SQLite's backup API or stop Davis and checkpoint before copying the database file. + **b. The admin password for the backend** ```shell diff --git a/config/services.yaml b/config/services.yaml index 25662b12..3f4dafe6 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -5,6 +5,7 @@ # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration parameters: default_database_driver: "mysql" + default_sqlite_journal_mode: "WAL" default_admin_auth_bypass: "false" timezone: '%env(APP_TIMEZONE)%' public_calendars_enabled: '%env(default:default_public_calendars_enabled:bool:PUBLIC_CALENDARS_ENABLED)%' @@ -30,6 +31,13 @@ services: arguments: $authRealm: "%env(AUTH_REALM)%" + App\Doctrine\SQLiteConnectionMiddleware: + autoconfigure: false + arguments: + $journalMode: "%env(default:default_sqlite_journal_mode:SQLITE_JOURNAL_MODE)%" + tags: + - { name: doctrine.middleware, priority: 100 } + App\Services\IMAPAuth: arguments: $IMAPAuthUrl: "%env(IMAP_AUTH_URL)%" diff --git a/src/Doctrine/SQLiteConnectionMiddleware.php b/src/Doctrine/SQLiteConnectionMiddleware.php new file mode 100644 index 00000000..80dc437e --- /dev/null +++ b/src/Doctrine/SQLiteConnectionMiddleware.php @@ -0,0 +1,60 @@ +journalMode = $journalMode; + } + + public function wrap(Driver $driver): Driver + { + if (!$driver->getDatabasePlatform() instanceof SqlitePlatform) { + return $driver; + } + + return new class($driver, $this->journalMode) extends AbstractDriverMiddleware { + public function __construct(Driver $driver, private readonly string $journalMode) + { + parent::__construct($driver); + } + + public function connect( + #[\SensitiveParameter] + array $params, + ): Connection { + $connection = parent::connect($params); + + $connection->exec('PRAGMA busy_timeout=60000'); + + if (!($params['memory'] ?? false)) { + $journalMode = $connection->query('PRAGMA journal_mode='.$this->journalMode)->fetchOne(); + if (strtoupper((string) $journalMode) !== $this->journalMode) { + throw new \RuntimeException(sprintf('SQLite could not enable %s journal mode; check filesystem support and database permissions.', $this->journalMode)); + } + } + + $connection->exec('PRAGMA synchronous=FULL'); + + return $connection; + } + }; + } +} diff --git a/tests/Functional/SQLiteConnectionTest.php b/tests/Functional/SQLiteConnectionTest.php new file mode 100644 index 00000000..1dee7389 --- /dev/null +++ b/tests/Functional/SQLiteConnectionTest.php @@ -0,0 +1,23 @@ +get(EntityManagerInterface::class)->getConnection(); + if ('sqlite' !== $connection->getDatabasePlatform()->getName()) { + self::markTestSkipped('SQLite-specific connection defaults.'); + } + + self::assertSame('wal', $connection->fetchOne('PRAGMA journal_mode')); + self::assertSame(2, (int) $connection->fetchOne('PRAGMA synchronous')); + self::assertSame(60000, (int) $connection->fetchOne('PRAGMA busy_timeout')); + } +}