Backend cleanup: remove schema file bootstrap and finalize SQLCipher-only DB init

This commit is contained in:
Jacob Schmidt 2026-02-28 17:49:18 -06:00
parent f6ff9d2acb
commit aafb08e63f
8 changed files with 37 additions and 71 deletions

View File

@ -15,7 +15,7 @@ internal sealed record EntryTemplateLoadPayload(string FilePath);
internal sealed record EntryTemplateDeletePayload(string FilePath); internal sealed record EntryTemplateDeletePayload(string FilePath);
public sealed record EntryTemplateLoadResult(string FileName, string FilePath, string Content); public sealed record EntryTemplateLoadResult(string FileName, string FilePath, string Content);
public sealed record EntryTemplateSavePayload(string Name, string Content, string? FilePath = null); public sealed record EntryTemplateSavePayload(string Name, string Content, string? FilePath = null);
internal sealed record DatabasePayload(string Password, string? DataDirectory = null); internal sealed record DatabasePayload(string Password);
internal sealed record AiSummarizeEntryPayload(string Content, string? FileStem = null); internal sealed record AiSummarizeEntryPayload(string Content, string? FileStem = null);
internal sealed record AiSummarizeAllPayload(List<string>? Entries); internal sealed record AiSummarizeAllPayload(List<string>? Entries);
internal sealed record AiChatPayload(string Prompt); internal sealed record AiChatPayload(string Prompt);

View File

@ -6,13 +6,11 @@ public sealed record JournalDatabaseStatus(
int Iterations, int Iterations,
string KeyDerivation, string KeyDerivation,
IReadOnlyList<string> SchemaTables, IReadOnlyList<string> SchemaTables,
string SchemaBootstrapPath,
bool RuntimeReady, bool RuntimeReady,
string RuntimeMessage); string RuntimeMessage);
public sealed record JournalDatabaseHydrationResult( public sealed record JournalDatabaseHydrationResult(
string DatabasePath, string DatabasePath,
string SchemaBootstrapPath,
int EntryFilesProcessed, int EntryFilesProcessed,
bool RuntimeReady, bool RuntimeReady,
string Message); string Message);

View File

@ -342,16 +342,6 @@ public class Entry(
_databaseSession.SetPassword(loadPayload.Password); _databaseSession.SetPassword(loadPayload.Password);
result = loaded; result = loaded;
break; break;
case "vault.save_current_month":
var saveCurrentPayload = DeserializePayload<VaultPayload>(cmd.Payload);
if (saveCurrentPayload is null)
return Error("Missing or invalid payload");
_vaultStorage.RebuildAllVaults(
saveCurrentPayload.Password,
saveCurrentPayload.VaultDirectory,
ResolveVaultStorageDirectory());
result = true;
break;
case "vault.rebuild_all": case "vault.rebuild_all":
var rebuildPayload = DeserializePayload<VaultPayload>(cmd.Payload); var rebuildPayload = DeserializePayload<VaultPayload>(cmd.Payload);
if (rebuildPayload is null) if (rebuildPayload is null)
@ -373,20 +363,25 @@ public class Entry(
var dbStatusPayload = DeserializePayload<DatabasePayload>(cmd.Payload); var dbStatusPayload = DeserializePayload<DatabasePayload>(cmd.Payload);
if (dbStatusPayload is null || string.IsNullOrWhiteSpace(dbStatusPayload.Password)) if (dbStatusPayload is null || string.IsNullOrWhiteSpace(dbStatusPayload.Password))
return Error("Missing or invalid payload"); return Error("Missing or invalid payload");
result = _database.GetStatus(dbStatusPayload.Password, dbStatusPayload.DataDirectory); result = _database.GetStatus(dbStatusPayload.Password);
break; break;
case "db.initialize_schema": case "db.initialize_schema":
var dbInitPayload = DeserializePayload<DatabasePayload>(cmd.Payload); var dbInitPayload = DeserializePayload<DatabasePayload>(cmd.Payload);
if (dbInitPayload is null) if (dbInitPayload is null || string.IsNullOrWhiteSpace(dbInitPayload.Password))
return Error("Missing or invalid payload"); return Error("Missing or invalid payload");
var schemaPath = _database.WriteSchemaBootstrap(dbInitPayload.DataDirectory); var initResult = _database.HydrateWorkspace(dbInitPayload.Password);
result = new { schemaPath }; result = new
{
initialized = initResult.RuntimeReady,
databasePath = initResult.DatabasePath,
initResult.Message
};
break; break;
case "db.hydrate_workspace": case "db.hydrate_workspace":
var dbHydratePayload = DeserializePayload<DatabasePayload>(cmd.Payload); var dbHydratePayload = DeserializePayload<DatabasePayload>(cmd.Payload);
if (dbHydratePayload is null || string.IsNullOrWhiteSpace(dbHydratePayload.Password)) if (dbHydratePayload is null || string.IsNullOrWhiteSpace(dbHydratePayload.Password))
return Error("Missing or invalid payload"); return Error("Missing or invalid payload");
result = _database.HydrateWorkspace(dbHydratePayload.Password, dbHydratePayload.DataDirectory); result = _database.HydrateWorkspace(dbHydratePayload.Password);
_databaseSession.SetPassword(dbHydratePayload.Password); _databaseSession.SetPassword(dbHydratePayload.Password);
break; break;
default: default:
@ -450,7 +445,7 @@ public class Entry(
if (!VaultSyncActions.Contains(action)) if (!VaultSyncActions.Contains(action))
return; return;
if (!_databaseSession.TryGetSession(out var password, out _)) if (!_databaseSession.TryGetSession(out var password))
return; return;
try try

View File

@ -7,7 +7,6 @@ public sealed class DatabaseSessionService(IJournalDatabaseService database) : I
private readonly IJournalDatabaseService _database = database; private readonly IJournalDatabaseService _database = database;
private readonly Lock _lock = new(); private readonly Lock _lock = new();
private string? _password; private string? _password;
private string? _dataDirectory;
private SqliteConnection? _connection; private SqliteConnection? _connection;
public bool IsUnlocked public bool IsUnlocked
@ -18,38 +17,34 @@ public sealed class DatabaseSessionService(IJournalDatabaseService database) : I
} }
} }
public void SetPassword(string password, string? dataDirectory = null) public void SetPassword(string password)
{ {
if (string.IsNullOrWhiteSpace(password)) if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("Password cannot be empty.", nameof(password)); throw new ArgumentException("Password cannot be empty.", nameof(password));
lock (_lock) lock (_lock)
{ {
if (_connection is not null && if (_connection is not null && _password != password)
(_password != password || _dataDirectory != dataDirectory))
{ {
_connection.Dispose(); _connection.Dispose();
_connection = null; _connection = null;
} }
_password = password; _password = password;
_dataDirectory = dataDirectory;
} }
} }
public bool TryGetSession(out string password, out string? dataDirectory) public bool TryGetSession(out string password)
{ {
lock (_lock) lock (_lock)
{ {
if (string.IsNullOrWhiteSpace(_password)) if (string.IsNullOrWhiteSpace(_password))
{ {
password = ""; password = "";
dataDirectory = null;
return false; return false;
} }
password = _password; password = _password;
dataDirectory = _dataDirectory;
return true; return true;
} }
} }
@ -65,7 +60,7 @@ public sealed class DatabaseSessionService(IJournalDatabaseService database) : I
if (_connection is not null) if (_connection is not null)
return _connection; return _connection;
_connection = _database.OpenEncryptedConnection(_password, _dataDirectory); _connection = _database.OpenEncryptedConnection(_password);
_database.EnsureSchema(_connection); _database.EnsureSchema(_connection);
return _connection; return _connection;
} }

View File

@ -5,8 +5,8 @@ namespace Journal.Core.Services.Database;
public interface IDatabaseSessionService public interface IDatabaseSessionService
{ {
bool IsUnlocked { get; } bool IsUnlocked { get; }
void SetPassword(string password, string? dataDirectory = null); void SetPassword(string password);
bool TryGetSession(out string password, out string? dataDirectory); bool TryGetSession(out string password);
SqliteConnection GetConnection(); SqliteConnection GetConnection();
void CloseConnection(); void CloseConnection();
} }

View File

@ -5,13 +5,12 @@ namespace Journal.Core.Services.Database;
public interface IJournalDatabaseService public interface IJournalDatabaseService
{ {
string GetDatabasePath(string? dataDirectory = null); string GetDatabasePath();
byte[] DeriveDatabaseKey(string password); byte[] DeriveDatabaseKey(string password);
string BuildPragmaKeyStatement(string password); string BuildPragmaKeyStatement(string password);
IReadOnlyDictionary<string, string> GetSchemaStatements(); IReadOnlyDictionary<string, string> GetSchemaStatements();
SqliteConnection OpenEncryptedConnection(string password, string? dataDirectory = null); SqliteConnection OpenEncryptedConnection(string password);
void EnsureSchema(SqliteConnection connection); void EnsureSchema(SqliteConnection connection);
string WriteSchemaBootstrap(string? dataDirectory = null); JournalDatabaseStatus GetStatus(string password);
JournalDatabaseStatus GetStatus(string password, string? dataDirectory = null); JournalDatabaseHydrationResult HydrateWorkspace(string password);
JournalDatabaseHydrationResult HydrateWorkspace(string password, string? dataDirectory = null);
} }

View File

@ -19,7 +19,7 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
private readonly IJournalConfigService _config = config; private readonly IJournalConfigService _config = config;
public string GetDatabasePath(string? dataDirectory = null) public string GetDatabasePath()
{ {
var directory = ResolveDatabaseDirectory(); var directory = ResolveDatabaseDirectory();
@ -133,51 +133,30 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
}; };
} }
public string WriteSchemaBootstrap(string? dataDirectory = null) public JournalDatabaseStatus GetStatus(string password)
{
var directory = ResolveDatabaseDirectory();
Directory.CreateDirectory(directory);
var bootstrapPath = Path.GetFullPath(Path.Combine(directory, "journal_schema.sql"));
var statements = GetSchemaStatements()
.Select(pair => $"-- {pair.Key}\n{pair.Value.Trim()}")
.ToArray();
var content = string.Join("\n\n", statements) + "\n";
File.WriteAllText(bootstrapPath, content);
return bootstrapPath;
}
public JournalDatabaseStatus GetStatus(string password, string? dataDirectory = null)
{ {
var tables = GetSchemaStatements().Keys.OrderBy(x => x, StringComparer.Ordinal).ToArray(); var tables = GetSchemaStatements().Keys.OrderBy(x => x, StringComparer.Ordinal).ToArray();
var bootstrapPath = WriteSchemaBootstrap(dataDirectory); var runtime = ProbeRuntime(password);
var runtime = ProbeRuntime(password, dataDirectory);
return new JournalDatabaseStatus( return new JournalDatabaseStatus(
DatabasePath: GetDatabasePath(dataDirectory), DatabasePath: GetDatabasePath(),
KeyLengthBytes: DeriveDatabaseKey(password).Length, KeyLengthBytes: DeriveDatabaseKey(password).Length,
Iterations: Iterations, Iterations: Iterations,
KeyDerivation: "PBKDF2-HMAC-SHA256", KeyDerivation: "PBKDF2-HMAC-SHA256",
SchemaTables: tables, SchemaTables: tables,
SchemaBootstrapPath: bootstrapPath,
RuntimeReady: runtime.Ready, RuntimeReady: runtime.Ready,
RuntimeMessage: runtime.Message); RuntimeMessage: runtime.Message);
} }
public JournalDatabaseHydrationResult HydrateWorkspace(string password, string? dataDirectory = null) public JournalDatabaseHydrationResult HydrateWorkspace(string password)
{ {
var directory = ResolveDatabaseDirectory(); using var connection = OpenEncryptedConnection(password);
Directory.CreateDirectory(directory);
using var connection = OpenEncryptedConnection(password, directory);
EnsureSchema(connection); EnsureSchema(connection);
var runtimeReady = HasRequiredTables(connection); var runtimeReady = HasRequiredTables(connection);
var entryDocumentsProcessed = CountEntryDocuments(connection); var entryDocumentsProcessed = CountEntryDocuments(connection);
var schemaPath = WriteSchemaBootstrap(directory);
return new JournalDatabaseHydrationResult( return new JournalDatabaseHydrationResult(
DatabasePath: GetDatabasePath(directory), DatabasePath: GetDatabasePath(),
SchemaBootstrapPath: schemaPath,
EntryFilesProcessed: entryDocumentsProcessed, EntryFilesProcessed: entryDocumentsProcessed,
RuntimeReady: runtimeReady, RuntimeReady: runtimeReady,
Message: runtimeReady Message: runtimeReady
@ -200,14 +179,14 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
} }
} }
public SqliteConnection OpenEncryptedConnection(string password, string? dataDirectory = null) public SqliteConnection OpenEncryptedConnection(string password)
{ {
if (string.IsNullOrWhiteSpace(password)) if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("Password cannot be empty.", nameof(password)); throw new ArgumentException("Password cannot be empty.", nameof(password));
EnsureSqliteInitialized(); EnsureSqliteInitialized();
var connection = new SqliteConnection($"Data Source={GetDatabasePath(dataDirectory)};Mode=ReadWriteCreate;Pooling=False"); var connection = new SqliteConnection($"Data Source={GetDatabasePath()};Mode=ReadWriteCreate;Pooling=False");
connection.Open(); connection.Open();
using var keyCmd = connection.CreateCommand(); using var keyCmd = connection.CreateCommand();
@ -246,11 +225,11 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
return RequiredSchemaTables.All(existing.Contains); return RequiredSchemaTables.All(existing.Contains);
} }
private (bool Ready, string Message) ProbeRuntime(string password, string? dataDirectory) private (bool Ready, string Message) ProbeRuntime(string password)
{ {
try try
{ {
using var connection = OpenEncryptedConnection(password, dataDirectory); using var connection = OpenEncryptedConnection(password);
EnsureSchema(connection); EnsureSchema(connection);
var ready = HasRequiredTables(connection); var ready = HasRequiredTables(connection);
return ready return ready
@ -282,4 +261,5 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
return Path.GetFullPath(Path.Combine(_config.Current.VaultDirectory, "db")); return Path.GetFullPath(Path.Combine(_config.Current.VaultDirectory, "db"));
} }
} }

View File

@ -381,12 +381,11 @@ Error:
| `search.entries` | Search entries with filters | optional query/section/date/tags/types/checked/unchecked | | `search.entries` | Search entries with filters | optional query/section/date/tags/types/checked/unchecked |
| `vault.initialize` | Ensure vault directory exists | `payload.password`, `payload.vaultDirectory` | | `vault.initialize` | Ensure vault directory exists | `payload.password`, `payload.vaultDirectory` |
| `vault.load_all` | Restore encrypted SQLCipher DB snapshot from vault | `payload.password`, `payload.vaultDirectory` | | `vault.load_all` | Restore encrypted SQLCipher DB snapshot from vault | `payload.password`, `payload.vaultDirectory` |
| `vault.save_current_month` | Alias for vault rebuild (DB snapshot persist) | `payload.password`, `payload.vaultDirectory` |
| `vault.rebuild_all` | Persist encrypted SQLCipher DB snapshot to vault | `payload.password`, `payload.vaultDirectory` | | `vault.rebuild_all` | Persist encrypted SQLCipher DB snapshot to vault | `payload.password`, `payload.vaultDirectory` |
| `vault.clear_data_directory` | No-op for SQLCipher-first mode (compat command) | — | | `vault.clear_data_directory` | No-op for SQLCipher-first mode (compat command) | — |
| `db.status` | DB key/schema compatibility snapshot | `payload.password`, optional `payload.dataDirectory` | | `db.status` | DB key/schema compatibility snapshot | `payload.password` |
| `db.initialize_schema` | Write SQL schema bootstrap file | optional `payload.dataDirectory` | | `db.initialize_schema` | Initialize SQLCipher schema in the database file | `payload.password` |
| `db.hydrate_workspace` | Bootstrap DB + set session password | `payload.password`, optional `payload.dataDirectory` | | `db.hydrate_workspace` | Bootstrap DB + set session password | `payload.password` |
| `config.get` | Return current config snapshot | — | | `config.get` | Return current config snapshot | — |
| `ai.health` | AI provider health status | — | | `ai.health` | AI provider health status | — |
| `ai.summarize_entry` | Summarize one entry | `payload.content`, optional `payload.fileStem` | | `ai.summarize_entry` | Summarize one entry | `payload.content`, optional `payload.fileStem` |