using Microsoft.Data.Sqlite;
namespace ClawdDotNet.Core.Storage;
///
/// Zentrale Stelle für den Zugang zur Instanz-Datenbank.
///
/// Die frühere Fassung öffnete pro Aufruf eine Verbindung ohne weitere Einstellungen —
/// ohne WAL und ohne Wartezeit bei Sperren. Sobald mehrere Agenten gleichzeitig
/// schreiben, quittiert SQLite das mit "database is locked". Das sah nach einer Grenze
/// von SQLite aus, war aber nur fehlende Konfiguration.
///
/// - WAL erlaubt beliebig viele Leser parallel zu einem Schreiber.
/// - busy_timeout lässt einen Schreiber kurz warten, statt sofort zu scheitern.
/// - Connection-Pooling vermeidet den Aufbau je Aufruf.
///
public sealed class SqliteStorage
{
private readonly string _connectionString;
private readonly SemaphoreSlim _writeGate = new(1, 1);
public string DatabasePath { get; }
public SqliteStorage(string databasePath)
{
DatabasePath = databasePath;
var directory = Path.GetDirectoryName(Path.GetFullPath(databasePath));
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);
_connectionString = new SqliteConnectionStringBuilder
{
DataSource = databasePath,
Mode = SqliteOpenMode.ReadWriteCreate,
Cache = SqliteCacheMode.Shared,
Pooling = true,
DefaultTimeout = 30
}.ToString();
Initialize();
// #575: SQLite legt die Datenbank mit der Standardmaske an (auf der Test-VM 0644),
// und WAL- und Shared-Memory-Datei erben deren Rechte. Darin stehen Chatverläufe,
// Gedächtnis und die Argumente jedes Werkzeugaufrufs. Auch für bestehende Dateien,
// die noch aus der Zeit vor dieser Zeile stammen.
foreach (var datei in new[] { databasePath, databasePath + "-wal", databasePath + "-shm" })
{
if (File.Exists(datei))
AppPaths.RestrictToOwner(datei);
}
}
public SqliteConnection OpenConnection()
{
var connection = new SqliteConnection(_connectionString);
connection.Open();
ApplyPragmas(connection);
return connection;
}
public async Task OpenConnectionAsync(CancellationToken ct)
{
var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync(ct);
ApplyPragmas(connection);
return connection;
}
///
/// Serialisiert Schreibvorgänge im Prozess. SQLite lässt ohnehin nur einen Schreiber
/// zu — die Warteschlange hier ist verlässlicher als das Zurückweisen durch die
/// Datenbank und macht Fehlerbilder reproduzierbar.
///
public async Task WriteAsync(Func> action, CancellationToken ct)
{
await _writeGate.WaitAsync(ct);
try
{
await using var connection = await OpenConnectionAsync(ct);
return await action(connection);
}
finally
{
_writeGate.Release();
}
}
public async Task WriteAsync(Func action, CancellationToken ct)
=> await WriteAsync