diff --git a/.env b/.env index 47f3c7f..9e4a595 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 01b825c..e83c3e3 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 25662b1..3f4dafe 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 0000000..80dc437 --- /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 0000000..1dee738 --- /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')); + } +}