diff --git a/src/SquidStd.Database/Connection/ConnectionStringParser.cs b/src/SquidStd.Database/Connection/ConnectionStringParser.cs
index 1559f0c0..72b7943d 100644
--- a/src/SquidStd.Database/Connection/ConnectionStringParser.cs
+++ b/src/SquidStd.Database/Connection/ConnectionStringParser.cs
@@ -12,8 +12,9 @@ public static class ConnectionStringParser
/// Parses the given URI connection string.
///
/// The URI connection string.
+ /// Base directory used to resolve relative SQLite file paths; null keeps them verbatim.
/// The parsed provider and native connection string.
- public static ParsedConnection Parse(string connectionString)
+ public static ParsedConnection Parse(string connectionString, string? sqliteBaseDirectory = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
@@ -28,11 +29,14 @@ public static ParsedConnection Parse(string connectionString)
var remainder = connectionString[(schemeEnd + 3)..];
var provider = ResolveProvider(scheme);
- var native = provider == DatabaseProviderType.Sqlite
- ? BuildSqlite(remainder)
- : BuildServer(provider, remainder);
+ if (provider == DatabaseProviderType.Sqlite)
+ {
+ var filePath = ResolveSqlitePath(remainder, sqliteBaseDirectory);
+
+ return new(provider, $"Data Source={filePath ?? remainder}", filePath);
+ }
- return new(provider, native);
+ return new(provider, BuildServer(provider, remainder));
}
private static string BuildServer(DatabaseProviderType provider, string remainder)
@@ -76,14 +80,24 @@ private static string BuildServer(DatabaseProviderType provider, string remainde
};
}
- private static string BuildSqlite(string remainder)
+ private static string? ResolveSqlitePath(string remainder, string? baseDirectory)
{
if (string.IsNullOrWhiteSpace(remainder))
{
throw new FormatException("SQLite connection string requires a file path or ':memory:'.");
}
- return $"Data Source={remainder}";
+ if (string.Equals(remainder, ":memory:", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ if (!Path.IsPathRooted(remainder) && !string.IsNullOrWhiteSpace(baseDirectory))
+ {
+ return Path.GetFullPath(Path.Combine(baseDirectory, remainder));
+ }
+
+ return remainder;
}
private static DatabaseProviderType ResolveProvider(string scheme)
diff --git a/src/SquidStd.Database/Connection/ParsedConnection.cs b/src/SquidStd.Database/Connection/ParsedConnection.cs
index b03c0d84..b5a6d1c3 100644
--- a/src/SquidStd.Database/Connection/ParsedConnection.cs
+++ b/src/SquidStd.Database/Connection/ParsedConnection.cs
@@ -7,4 +7,9 @@ namespace SquidStd.Database.Connection;
///
/// The resolved database provider.
/// The provider-native connection string for FreeSql.
-public sealed record ParsedConnection(DatabaseProviderType Provider, string NativeConnectionString);
+/// The resolved on-disk SQLite file path, or null for :memory: and server providers.
+public sealed record ParsedConnection(
+ DatabaseProviderType Provider,
+ string NativeConnectionString,
+ string? SqliteFilePath = null
+);
diff --git a/src/SquidStd.Database/Services/DatabaseService.cs b/src/SquidStd.Database/Services/DatabaseService.cs
index 7b606ea2..9d5d0c78 100644
--- a/src/SquidStd.Database/Services/DatabaseService.cs
+++ b/src/SquidStd.Database/Services/DatabaseService.cs
@@ -2,6 +2,7 @@
using Serilog;
using SquidStd.Database.Abstractions.Data.Database;
using SquidStd.Database.Abstractions.Types.Data;
+using SquidStd.Core.Directories;
using SquidStd.Database.Connection;
using SquidStd.Database.Interfaces.Services;
@@ -15,6 +16,7 @@ public sealed class DatabaseService : IDatabaseService
private static readonly ILogger Logger = Log.ForContext();
private readonly DatabaseConfig _config;
+ private readonly DirectoriesConfig _directories;
private IFreeSql? _orm;
private int _started;
@@ -25,9 +27,11 @@ public sealed class DatabaseService : IDatabaseService
/// Initializes the database service.
///
/// The database configuration section.
- public DatabaseService(DatabaseConfig config)
+ /// The directories configuration, providing the root directory.
+ public DatabaseService(DatabaseConfig config, DirectoriesConfig directories)
{
_config = config;
+ _directories = directories;
}
///
@@ -40,7 +44,18 @@ public ValueTask StartAsync(CancellationToken cancellationToken = default)
return ValueTask.CompletedTask;
}
- var parsed = ConnectionStringParser.Parse(_config.ConnectionString);
+ var parsed = ConnectionStringParser.Parse(_config.ConnectionString, _directories.Root);
+
+ if (parsed.SqliteFilePath is { } sqliteFile)
+ {
+ var directory = Path.GetDirectoryName(sqliteFile);
+
+ if (!string.IsNullOrEmpty(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+ }
+
Logger.Verbose("Building FreeSql for provider {Provider}", parsed.Provider);
var builder = new FreeSqlBuilder()
diff --git a/tests/SquidStd.Tests/Database/ConnectionStringParserTests.cs b/tests/SquidStd.Tests/Database/ConnectionStringParserTests.cs
index 73ee8d68..43673794 100644
--- a/tests/SquidStd.Tests/Database/ConnectionStringParserTests.cs
+++ b/tests/SquidStd.Tests/Database/ConnectionStringParserTests.cs
@@ -77,4 +77,51 @@ public void Parse_SqlServer_BuildsNativeString()
[Fact]
public void Parse_UnknownScheme_Throws()
=> Assert.Throws(() => ConnectionStringParser.Parse("oracle://h/db"));
+
+ [Fact]
+ public void Parse_SqliteRelativePath_WithBaseDirectory_ResolvesAgainstBase()
+ {
+ var baseDir = Path.Combine(Path.GetTempPath(), "squidstd-base");
+ var result = ConnectionStringParser.Parse("sqlite://app.db", baseDir);
+
+ var expected = Path.GetFullPath(Path.Combine(baseDir, "app.db"));
+ Assert.Equal(DatabaseProviderType.Sqlite, result.Provider);
+ Assert.Equal($"Data Source={expected}", result.NativeConnectionString);
+ Assert.Equal(expected, result.SqliteFilePath);
+ }
+
+ [Fact]
+ public void Parse_SqliteRelativePath_WithoutBaseDirectory_StaysVerbatim()
+ {
+ var result = ConnectionStringParser.Parse("sqlite://app.db");
+
+ Assert.Equal("Data Source=app.db", result.NativeConnectionString);
+ Assert.Equal("app.db", result.SqliteFilePath);
+ }
+
+ [Fact]
+ public void Parse_SqliteAbsolutePath_WithBaseDirectory_Unchanged()
+ {
+ var result = ConnectionStringParser.Parse("sqlite:///var/data/app.db", "/tmp/base");
+
+ Assert.Equal("Data Source=/var/data/app.db", result.NativeConnectionString);
+ Assert.Equal("/var/data/app.db", result.SqliteFilePath);
+ }
+
+ [Fact]
+ public void Parse_SqliteInMemory_WithBaseDirectory_HasNullFilePath()
+ {
+ var result = ConnectionStringParser.Parse("sqlite://:memory:", "/tmp/base");
+
+ Assert.Equal("Data Source=:memory:", result.NativeConnectionString);
+ Assert.Null(result.SqliteFilePath);
+ }
+
+ [Fact]
+ public void Parse_ServerProvider_HasNullSqliteFilePath()
+ {
+ var result = ConnectionStringParser.Parse("postgres://user:pass@db.host:5433/appdb");
+
+ Assert.Null(result.SqliteFilePath);
+ }
}
diff --git a/tests/SquidStd.Tests/Database/DatabaseServiceTests.cs b/tests/SquidStd.Tests/Database/DatabaseServiceTests.cs
new file mode 100644
index 00000000..8b94d01d
--- /dev/null
+++ b/tests/SquidStd.Tests/Database/DatabaseServiceTests.cs
@@ -0,0 +1,34 @@
+using SquidStd.Core.Directories;
+using SquidStd.Database.Abstractions.Data.Database;
+using SquidStd.Database.Services;
+
+namespace SquidStd.Tests.Database;
+
+public class DatabaseServiceTests
+{
+ [Fact]
+ public async Task StartAsync_RelativeSqlitePathWithSubdirectory_CreatesDirectoryUnderRoot()
+ {
+ var root = Path.Combine(Path.GetTempPath(), "squidstd-root-" + Guid.NewGuid().ToString("N"));
+ var service = new DatabaseService(
+ new DatabaseConfig { ConnectionString = "sqlite://sub/app.db", AutoMigrate = true },
+ new DirectoriesConfig(root, [])
+ );
+
+ try
+ {
+ await service.StartAsync();
+
+ Assert.True(Directory.Exists(Path.Combine(root, "sub")));
+ }
+ finally
+ {
+ await service.StopAsync();
+
+ if (Directory.Exists(root))
+ {
+ Directory.Delete(root, true);
+ }
+ }
+ }
+}
diff --git a/tests/SquidStd.Tests/Database/FreeSqlDataAccessTests.cs b/tests/SquidStd.Tests/Database/FreeSqlDataAccessTests.cs
index 4ecec4bd..c479456d 100644
--- a/tests/SquidStd.Tests/Database/FreeSqlDataAccessTests.cs
+++ b/tests/SquidStd.Tests/Database/FreeSqlDataAccessTests.cs
@@ -1,3 +1,4 @@
+using SquidStd.Core.Directories;
using SquidStd.Database.Abstractions.Data.Entities;
using SquidStd.Database.Data;
using SquidStd.Database.Services;
@@ -33,7 +34,8 @@ public async Task InitializeAsync()
{
ConnectionString = $"sqlite://{_dbPath}",
AutoMigrate = true
- }
+ },
+ new DirectoriesConfig(Path.GetTempPath(), [])
);
await _service.StartAsync();
}
diff --git a/tests/SquidStd.Tests/Vfs/Database/DatabaseFileSystemTests.cs b/tests/SquidStd.Tests/Vfs/Database/DatabaseFileSystemTests.cs
index 993ca6c4..3592f46c 100644
--- a/tests/SquidStd.Tests/Vfs/Database/DatabaseFileSystemTests.cs
+++ b/tests/SquidStd.Tests/Vfs/Database/DatabaseFileSystemTests.cs
@@ -1,4 +1,5 @@
using System.Text;
+using SquidStd.Core.Directories;
using SquidStd.Database.Abstractions.Data.Database;
using SquidStd.Database.Data;
using SquidStd.Database.Services;
@@ -21,7 +22,8 @@ public async Task InitializeAsync()
{
ConnectionString = $"sqlite://{_dbPath}",
AutoMigrate = true
- }
+ },
+ new DirectoriesConfig(Path.GetTempPath(), [])
);
await _service.StartAsync();
_fs = new DatabaseFileSystem(NewAccess());