Skip to content
Merged
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
28 changes: 21 additions & 7 deletions src/SquidStd.Database/Connection/ConnectionStringParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
/// Parses the given URI connection string.
/// </summary>
/// <param name="connectionString">The URI connection string.</param>
/// <param name="sqliteBaseDirectory">Base directory used to resolve relative SQLite file paths; null keeps them verbatim.</param>
/// <returns>The parsed provider and native connection string.</returns>
public static ParsedConnection Parse(string connectionString)
public static ParsedConnection Parse(string connectionString, string? sqliteBaseDirectory = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);

Expand All @@ -28,11 +29,14 @@
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)
Expand Down Expand Up @@ -76,14 +80,24 @@
};
}

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));

Check notice

Code scanning / CodeQL

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments.
}

return remainder;
}

private static DatabaseProviderType ResolveProvider(string scheme)
Expand Down
7 changes: 6 additions & 1 deletion src/SquidStd.Database/Connection/ParsedConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ namespace SquidStd.Database.Connection;
/// </summary>
/// <param name="Provider">The resolved database provider.</param>
/// <param name="NativeConnectionString">The provider-native connection string for FreeSql.</param>
public sealed record ParsedConnection(DatabaseProviderType Provider, string NativeConnectionString);
/// <param name="SqliteFilePath">The resolved on-disk SQLite file path, or null for :memory: and server providers.</param>
public sealed record ParsedConnection(
DatabaseProviderType Provider,
string NativeConnectionString,
string? SqliteFilePath = null
);
19 changes: 17 additions & 2 deletions src/SquidStd.Database/Services/DatabaseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

private readonly DatabaseConfig _config;
private readonly DirectoriesConfig _directories;
private IFreeSql? _orm;
private int _started;

Expand All @@ -25,9 +27,11 @@ public sealed class DatabaseService : IDatabaseService
/// Initializes the database service.
/// </summary>
/// <param name="config">The database configuration section.</param>
public DatabaseService(DatabaseConfig config)
/// <param name="directories">The directories configuration, providing the root directory.</param>
public DatabaseService(DatabaseConfig config, DirectoriesConfig directories)
{
_config = config;
_directories = directories;
}

/// <inheritdoc />
Expand All @@ -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()
Expand Down
47 changes: 47 additions & 0 deletions tests/SquidStd.Tests/Database/ConnectionStringParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,51 @@
[Fact]
public void Parse_UnknownScheme_Throws()
=> Assert.Throws<NotSupportedException>(() => ConnectionStringParser.Parse("oracle://h/db"));

[Fact]
public void Parse_SqliteRelativePath_WithBaseDirectory_ResolvesAgainstBase()
{
var baseDir = Path.Combine(Path.GetTempPath(), "squidstd-base");

Check notice

Code scanning / CodeQL

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note test

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments.
var result = ConnectionStringParser.Parse("sqlite://app.db", baseDir);

var expected = Path.GetFullPath(Path.Combine(baseDir, "app.db"));

Check notice

Code scanning / CodeQL

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note test

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments.
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);
}
}
34 changes: 34 additions & 0 deletions tests/SquidStd.Tests/Database/DatabaseServiceTests.cs
Original file line number Diff line number Diff line change
@@ -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"));

Check notice

Code scanning / CodeQL

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note test

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments.
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")));

Check notice

Code scanning / CodeQL

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments Note test

Call to 'System.IO.Path.Combine' may silently drop its earlier arguments.
}
finally
{
await service.StopAsync();

if (Directory.Exists(root))
{
Directory.Delete(root, true);
}
}
}
}
4 changes: 3 additions & 1 deletion tests/SquidStd.Tests/Database/FreeSqlDataAccessTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SquidStd.Core.Directories;
using SquidStd.Database.Abstractions.Data.Entities;
using SquidStd.Database.Data;
using SquidStd.Database.Services;
Expand Down Expand Up @@ -33,7 +34,8 @@ public async Task InitializeAsync()
{
ConnectionString = $"sqlite://{_dbPath}",
AutoMigrate = true
}
},
new DirectoriesConfig(Path.GetTempPath(), [])
);
await _service.StartAsync();
}
Expand Down
4 changes: 3 additions & 1 deletion tests/SquidStd.Tests/Vfs/Database/DatabaseFileSystemTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,7 +22,8 @@ public async Task InitializeAsync()
{
ConnectionString = $"sqlite://{_dbPath}",
AutoMigrate = true
}
},
new DirectoriesConfig(Path.GetTempPath(), [])
);
await _service.StartAsync();
_fs = new DatabaseFileSystem(NewAccess());
Expand Down
Loading