Skip to content

Commit 1db4d90

Browse files
authored
Merge pull request #26 from tgiachi/feature/sqlite-rootdir
feat(database): resolve relative SQLite paths against RootDirectory
2 parents eef0b2e + 6491a15 commit 1db4d90

7 files changed

Lines changed: 131 additions & 12 deletions

File tree

src/SquidStd.Database/Connection/ConnectionStringParser.cs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ public static class ConnectionStringParser
1212
/// Parses the given URI connection string.
1313
/// </summary>
1414
/// <param name="connectionString">The URI connection string.</param>
15+
/// <param name="sqliteBaseDirectory">Base directory used to resolve relative SQLite file paths; null keeps them verbatim.</param>
1516
/// <returns>The parsed provider and native connection string.</returns>
16-
public static ParsedConnection Parse(string connectionString)
17+
public static ParsedConnection Parse(string connectionString, string? sqliteBaseDirectory = null)
1718
{
1819
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
1920

@@ -28,11 +29,14 @@ public static ParsedConnection Parse(string connectionString)
2829
var remainder = connectionString[(schemeEnd + 3)..];
2930
var provider = ResolveProvider(scheme);
3031

31-
var native = provider == DatabaseProviderType.Sqlite
32-
? BuildSqlite(remainder)
33-
: BuildServer(provider, remainder);
32+
if (provider == DatabaseProviderType.Sqlite)
33+
{
34+
var filePath = ResolveSqlitePath(remainder, sqliteBaseDirectory);
35+
36+
return new(provider, $"Data Source={filePath ?? remainder}", filePath);
37+
}
3438

35-
return new(provider, native);
39+
return new(provider, BuildServer(provider, remainder));
3640
}
3741

3842
private static string BuildServer(DatabaseProviderType provider, string remainder)
@@ -76,14 +80,24 @@ private static string BuildServer(DatabaseProviderType provider, string remainde
7680
};
7781
}
7882

79-
private static string BuildSqlite(string remainder)
83+
private static string? ResolveSqlitePath(string remainder, string? baseDirectory)
8084
{
8185
if (string.IsNullOrWhiteSpace(remainder))
8286
{
8387
throw new FormatException("SQLite connection string requires a file path or ':memory:'.");
8488
}
8589

86-
return $"Data Source={remainder}";
90+
if (string.Equals(remainder, ":memory:", StringComparison.OrdinalIgnoreCase))
91+
{
92+
return null;
93+
}
94+
95+
if (!Path.IsPathRooted(remainder) && !string.IsNullOrWhiteSpace(baseDirectory))
96+
{
97+
return Path.GetFullPath(Path.Combine(baseDirectory, remainder));
98+
}
99+
100+
return remainder;
87101
}
88102

89103
private static DatabaseProviderType ResolveProvider(string scheme)

src/SquidStd.Database/Connection/ParsedConnection.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,9 @@ namespace SquidStd.Database.Connection;
77
/// </summary>
88
/// <param name="Provider">The resolved database provider.</param>
99
/// <param name="NativeConnectionString">The provider-native connection string for FreeSql.</param>
10-
public sealed record ParsedConnection(DatabaseProviderType Provider, string NativeConnectionString);
10+
/// <param name="SqliteFilePath">The resolved on-disk SQLite file path, or null for :memory: and server providers.</param>
11+
public sealed record ParsedConnection(
12+
DatabaseProviderType Provider,
13+
string NativeConnectionString,
14+
string? SqliteFilePath = null
15+
);

src/SquidStd.Database/Services/DatabaseService.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Serilog;
33
using SquidStd.Database.Abstractions.Data.Database;
44
using SquidStd.Database.Abstractions.Types.Data;
5+
using SquidStd.Core.Directories;
56
using SquidStd.Database.Connection;
67
using SquidStd.Database.Interfaces.Services;
78

@@ -15,6 +16,7 @@ public sealed class DatabaseService : IDatabaseService
1516
private static readonly ILogger Logger = Log.ForContext<DatabaseService>();
1617

1718
private readonly DatabaseConfig _config;
19+
private readonly DirectoriesConfig _directories;
1820
private IFreeSql? _orm;
1921
private int _started;
2022

@@ -25,9 +27,11 @@ public sealed class DatabaseService : IDatabaseService
2527
/// Initializes the database service.
2628
/// </summary>
2729
/// <param name="config">The database configuration section.</param>
28-
public DatabaseService(DatabaseConfig config)
30+
/// <param name="directories">The directories configuration, providing the root directory.</param>
31+
public DatabaseService(DatabaseConfig config, DirectoriesConfig directories)
2932
{
3033
_config = config;
34+
_directories = directories;
3135
}
3236

3337
/// <inheritdoc />
@@ -40,7 +44,18 @@ public ValueTask StartAsync(CancellationToken cancellationToken = default)
4044
return ValueTask.CompletedTask;
4145
}
4246

43-
var parsed = ConnectionStringParser.Parse(_config.ConnectionString);
47+
var parsed = ConnectionStringParser.Parse(_config.ConnectionString, _directories.Root);
48+
49+
if (parsed.SqliteFilePath is { } sqliteFile)
50+
{
51+
var directory = Path.GetDirectoryName(sqliteFile);
52+
53+
if (!string.IsNullOrEmpty(directory))
54+
{
55+
Directory.CreateDirectory(directory);
56+
}
57+
}
58+
4459
Logger.Verbose("Building FreeSql for provider {Provider}", parsed.Provider);
4560

4661
var builder = new FreeSqlBuilder()

tests/SquidStd.Tests/Database/ConnectionStringParserTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,51 @@ public void Parse_SqlServer_BuildsNativeString()
7777
[Fact]
7878
public void Parse_UnknownScheme_Throws()
7979
=> Assert.Throws<NotSupportedException>(() => ConnectionStringParser.Parse("oracle://h/db"));
80+
81+
[Fact]
82+
public void Parse_SqliteRelativePath_WithBaseDirectory_ResolvesAgainstBase()
83+
{
84+
var baseDir = Path.Combine(Path.GetTempPath(), "squidstd-base");
85+
var result = ConnectionStringParser.Parse("sqlite://app.db", baseDir);
86+
87+
var expected = Path.GetFullPath(Path.Combine(baseDir, "app.db"));
88+
Assert.Equal(DatabaseProviderType.Sqlite, result.Provider);
89+
Assert.Equal($"Data Source={expected}", result.NativeConnectionString);
90+
Assert.Equal(expected, result.SqliteFilePath);
91+
}
92+
93+
[Fact]
94+
public void Parse_SqliteRelativePath_WithoutBaseDirectory_StaysVerbatim()
95+
{
96+
var result = ConnectionStringParser.Parse("sqlite://app.db");
97+
98+
Assert.Equal("Data Source=app.db", result.NativeConnectionString);
99+
Assert.Equal("app.db", result.SqliteFilePath);
100+
}
101+
102+
[Fact]
103+
public void Parse_SqliteAbsolutePath_WithBaseDirectory_Unchanged()
104+
{
105+
var result = ConnectionStringParser.Parse("sqlite:///var/data/app.db", "/tmp/base");
106+
107+
Assert.Equal("Data Source=/var/data/app.db", result.NativeConnectionString);
108+
Assert.Equal("/var/data/app.db", result.SqliteFilePath);
109+
}
110+
111+
[Fact]
112+
public void Parse_SqliteInMemory_WithBaseDirectory_HasNullFilePath()
113+
{
114+
var result = ConnectionStringParser.Parse("sqlite://:memory:", "/tmp/base");
115+
116+
Assert.Equal("Data Source=:memory:", result.NativeConnectionString);
117+
Assert.Null(result.SqliteFilePath);
118+
}
119+
120+
[Fact]
121+
public void Parse_ServerProvider_HasNullSqliteFilePath()
122+
{
123+
var result = ConnectionStringParser.Parse("postgres://user:pass@db.host:5433/appdb");
124+
125+
Assert.Null(result.SqliteFilePath);
126+
}
80127
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using SquidStd.Core.Directories;
2+
using SquidStd.Database.Abstractions.Data.Database;
3+
using SquidStd.Database.Services;
4+
5+
namespace SquidStd.Tests.Database;
6+
7+
public class DatabaseServiceTests
8+
{
9+
[Fact]
10+
public async Task StartAsync_RelativeSqlitePathWithSubdirectory_CreatesDirectoryUnderRoot()
11+
{
12+
var root = Path.Combine(Path.GetTempPath(), "squidstd-root-" + Guid.NewGuid().ToString("N"));
13+
var service = new DatabaseService(
14+
new DatabaseConfig { ConnectionString = "sqlite://sub/app.db", AutoMigrate = true },
15+
new DirectoriesConfig(root, [])
16+
);
17+
18+
try
19+
{
20+
await service.StartAsync();
21+
22+
Assert.True(Directory.Exists(Path.Combine(root, "sub")));
23+
}
24+
finally
25+
{
26+
await service.StopAsync();
27+
28+
if (Directory.Exists(root))
29+
{
30+
Directory.Delete(root, true);
31+
}
32+
}
33+
}
34+
}

tests/SquidStd.Tests/Database/FreeSqlDataAccessTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using SquidStd.Core.Directories;
12
using SquidStd.Database.Abstractions.Data.Entities;
23
using SquidStd.Database.Data;
34
using SquidStd.Database.Services;
@@ -33,7 +34,8 @@ public async Task InitializeAsync()
3334
{
3435
ConnectionString = $"sqlite://{_dbPath}",
3536
AutoMigrate = true
36-
}
37+
},
38+
new DirectoriesConfig(Path.GetTempPath(), [])
3739
);
3840
await _service.StartAsync();
3941
}

tests/SquidStd.Tests/Vfs/Database/DatabaseFileSystemTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Text;
2+
using SquidStd.Core.Directories;
23
using SquidStd.Database.Abstractions.Data.Database;
34
using SquidStd.Database.Data;
45
using SquidStd.Database.Services;
@@ -21,7 +22,8 @@ public async Task InitializeAsync()
2122
{
2223
ConnectionString = $"sqlite://{_dbPath}",
2324
AutoMigrate = true
24-
}
25+
},
26+
new DirectoriesConfig(Path.GetTempPath(), [])
2527
);
2628
await _service.StartAsync();
2729
_fs = new DatabaseFileSystem(NewAccess());

0 commit comments

Comments
 (0)