Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -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 ###
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)%'
Expand All @@ -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)%"
Expand Down
60 changes: 60 additions & 0 deletions src/Doctrine/SQLiteConnectionMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace App\Doctrine;

use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Connection;
use Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
use Doctrine\DBAL\Platforms\SqlitePlatform;

final class SQLiteConnectionMiddleware implements Middleware
{
private string $journalMode;

public function __construct(string $journalMode)
{
$journalMode = strtoupper($journalMode);
if (!in_array($journalMode, ['WAL', 'DELETE'], true)) {
throw new \InvalidArgumentException('SQLITE_JOURNAL_MODE must be WAL or DELETE.');
}

$this->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;
}
};
}
}
23 changes: 23 additions & 0 deletions tests/Functional/SQLiteConnectionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace App\Tests\Functional;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class SQLiteConnectionTest extends KernelTestCase
{
public function testServerDefaults(): void
{
self::bootKernel();

$connection = self::getContainer()->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'));
}
}