Skip to content
Closed
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
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net10.0-windows</TargetFramework>
<LangVersion>13.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand All @@ -20,6 +20,6 @@
<PackageReference Include="Microsoft.Data.Sqlite" Version="11.0.0-preview.5.26302.115" />
<PackageReference Include="CsvHelper" Version="33.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="DuckDB.NET.Data.Full" Version="1.4.4" />
<PackageReference Include="DuckDB.NET.Data.Full" Version="1.5.3" />
</ItemGroup>
</Project>
110 changes: 13 additions & 97 deletions DuckDB-Memory-Lib/ConnectionManager.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
using System.Data;
using DuckDB.NET.Data;
using MemoryDb_Lib.Shared;


namespace DuckDb_Memory_Lib;

public sealed class ConnectionManager
public sealed class ConnectionManager : ConnectionManagerBase<DuckDBConnection>
{
private static readonly Lazy<ConnectionManager> LazyInstance =
new(() => new ConnectionManager(), LazyThreadSafetyMode.ExecutionAndPublication);

private readonly object _syncRoot = new();
private readonly Dictionary<string, DuckDBConnection> _connections = new(StringComparer.OrdinalIgnoreCase);

private ConnectionManager() { }

/// <summary>
Expand All @@ -23,98 +20,15 @@ public static ConnectionManager GetInstance()
}

/// <summary>
/// Retrieves an open SQLite connection identified by the provided alias, creating it when necessary.
/// Retrieves an open DuckDB connection identified by the provided alias, creating it when necessary.
/// </summary>
public DuckDBConnection GetConnection(string? alias = null, string? path = null)
{
var normalizedAlias = NormalizeAlias(alias);

lock (_syncRoot)
{
if (_connections.TryGetValue(normalizedAlias, out var existingConnection))
{
EnsureOpen(existingConnection);
return existingConnection;
}

var newConnection = DuckTools.GetInstance(path);
EnsureOpen(newConnection);
_connections[normalizedAlias] = newConnection;

return newConnection;
}
}

/// <summary>
/// Closes and disposes the connection associated with the provided alias.
/// </summary>
public void CloseConnection(string? alias = null)
{
var normalizedAlias = NormalizeAlias(alias);

lock (_syncRoot)
{
if (!_connections.TryGetValue(normalizedAlias, out var connection))
{
return;
}

try
{
connection.Close();
}
finally
{
connection.Dispose();
_connections.Remove(normalizedAlias);
}
}
}

/// <summary>
/// Closes and disposes all active SQLite connections managed by this instance.
/// </summary>
public void CloseAllConnections()
{
lock (_syncRoot)
{
foreach (var connection in _connections.Values)
{
try
{
connection.Close();
}
finally
{
connection.Dispose();
}
}

_connections.Clear();
}
return GetConnectionCore(alias, path, DuckTools.GetInstance);
}

/// <summary>
/// Opens the provided SQLite connection when it is not already open.
/// </summary>
private static void EnsureOpen(DuckDBConnection connection)
{
if (connection.State != ConnectionState.Open)
{
connection.Open();
}
}

/// <summary>
/// Normalizes aliases to ensure consistent lookups inside the connection dictionary.
/// </summary>
private static string NormalizeAlias(string? alias)
{
return string.IsNullOrWhiteSpace(alias) ? "default" : alias.Trim();
}

/// <summary>
/// Static helper that delegates to <see cref="CloseConnection(string?)"/>.
/// Static helper that delegates to <see cref="ConnectionManagerBase{TConnection}.CloseConnection(string?)"/>.
/// </summary>
public static void Close(string? alias = null)
{
Expand All @@ -128,7 +42,7 @@ public static void CloseAll()
{
GetInstance().CloseAllConnections();
}
}
}

public sealed class KeeperRegisterIdDataBase
{
Expand Down Expand Up @@ -182,11 +96,13 @@ public static void Register(string path, string idDataBase)
/// </summary>
public static void DeleteRegister(string idDataBase)
{
var keepIdDataBases = _mapIdDataBase.Values.ToList();
int indexValues = keepIdDataBases.IndexOf(idDataBase);
string pathIdDataBase = _mapIdDataBase.Keys.ToList()[indexValues];

_mapIdDataBase.Remove(pathIdDataBase);
var pathIdDataBase = _mapIdDataBase
.FirstOrDefault(entry => string.Equals(entry.Value, idDataBase, StringComparison.OrdinalIgnoreCase))
.Key;

if (pathIdDataBase is not null)
{
_mapIdDataBase.Remove(pathIdDataBase);
}
}
}
8 changes: 6 additions & 2 deletions DuckDB-Memory-Lib/DuckDB-Memory-Lib.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<RootNamespace>DuckDb_Memory_Lib</RootNamespace>
Expand All @@ -7,7 +7,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Update="Microsoft.Data.Sqlite" Version="11.0.0-preview.5.26302.115" />
<PackageReference Update="DuckDB.NET.Data.Full" Version="1.5.3" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Shared\ConnectionManagerBase.cs" Link="Shared\ConnectionManagerBase.cs" />
</ItemGroup>

</Project>
7 changes: 3 additions & 4 deletions DuckDB-Memory-Lib/DuckTools.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using DuckDB.NET.Data;
using DuckDB.NET.Data.DataChunk.Reader;
using DuckDB.NET.Data.DataChunk.Writer;
Expand Down Expand Up @@ -128,8 +128,7 @@ public static EnumsDuckMemory.Output RegisterScalarFunction<TInput, TOutput>(
{
db.RegisterScalarFunction<TInput, TOutput>(
idFunction,
func,
isPureFunction: true);
func);

return EnumsDuckMemory.Output.SUCCESS;
}
Expand Down Expand Up @@ -177,7 +176,7 @@ public static EnumsDuckMemory.Output AttachedDataBase(DuckDBConnection db, strin
}
else
{
attachedQry = $"ATTACH '{strConnection}' AS \"{aliasDataBase}\" ";
attachedQry = $"ATTACH '{strConnection}' AS {QuoteIdentifier(aliasDataBase)}";
}

try
Expand Down
47 changes: 35 additions & 12 deletions DuckDB-Memory-Lib/QueryExecutor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using DuckDB.NET.Data;
using System.IO;

Expand Down Expand Up @@ -30,15 +30,16 @@ public static EnumsDuckMemory.Output CreateTable(DuckDBConnection db, string idD
for (var i = 0; i < noFields; i++)
{
var t = NetTypeToDuckDbType.GetDuckDbType(types[i]);
fieldsDefinition.Add(headers[i] + " " + t);
fieldsDefinition.Add(DuckTools.QuoteIdentifier(headers[i]) + " " + t);
}

}

try
{
var qry = $"CREATE TABLE IF NOT EXISTS {idDataBase}.{idTable}({string.Join(",", fieldsDefinition)})";
var cmd = new DuckDBCommand(qry, db);
var tableName = $"{DuckTools.QuoteIdentifier(idDataBase)}.{DuckTools.QuoteIdentifier(idTable)}";
var qry = $"CREATE TABLE IF NOT EXISTS {tableName}({string.Join(",", fieldsDefinition)})";
using var cmd = new DuckDBCommand(qry, db);
cmd.ExecuteNonQuery();

return EnumsDuckMemory.Output.SUCCESS;
Expand Down Expand Up @@ -71,7 +72,7 @@ public static EnumsDuckMemory.Output CreateParquetTable(DuckDBConnection db, str
var tableName = $"{DuckTools.QuoteIdentifier(idDataBase)}.{DuckTools.QuoteIdentifier(idTable)}";
var parquetPath = parquetPathFile.Replace("'", "''");
var qry = $"CREATE OR REPLACE TABLE {tableName} AS SELECT * FROM '{parquetPath}';";
var cmd = new DuckDBCommand(qry, db);
using var cmd = new DuckDBCommand(qry, db);
cmd.ExecuteNonQuery();

return EnumsDuckMemory.Output.SUCCESS;
Expand All @@ -90,8 +91,8 @@ public static (EnumsDuckMemory.Output, List<Dictionary<string, object>>) Execute
{
try
{
var cmd = new DuckDBCommand(qry, db);
var qryResult = cmd.ExecuteReader();
using var cmd = new DuckDBCommand(qry, db);
using var qryResult = cmd.ExecuteReader();

var resultList = new List<Dictionary<string, object>>();

Expand All @@ -104,7 +105,6 @@ public static (EnumsDuckMemory.Output, List<Dictionary<string, object>>) Execute
}
}

qryResult.Close();
return (EnumsDuckMemory.Output.SUCCESS, resultList);
}
catch (Exception ex)
Expand All @@ -121,14 +121,37 @@ public static (EnumsDuckMemory.Output, List<Dictionary<string, object>>) Execute
{
try
{
qry = parameters.Keys.Aggregate(qry,
(current, param) => current.Replace(param, parameters[param], StringComparison.OrdinalIgnoreCase));
return ExecuteQryReader(db, qry);
using var cmd = new DuckDBCommand(qry, db);
foreach (var parameter in parameters)
{
cmd.Parameters.Add(new DuckDBParameter(
NormalizeParameterName(parameter.Key),
parameter.Value));
}

using var qryResult = cmd.ExecuteReader();
var resultList = new List<Dictionary<string, object>>();

if (qryResult.HasRows)
{
while (qryResult.Read())
{
resultList.Add(Enumerable.Range(0, qryResult.FieldCount)
.ToDictionary(qryResult.GetName, qryResult.GetValue));
}
}

return (EnumsDuckMemory.Output.SUCCESS, resultList);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
throw new Exception($"{ex.Message}-{EnumsDuckMemory.Output.ERROR_TO_EXECUTE_QUERY}", ex);
return (EnumsDuckMemory.Output.ERROR_TO_EXECUTE_QUERY, []);
}
}

private static string NormalizeParameterName(string parameterName)
{
return parameterName.TrimStart('@', '$', ':');
}
}
26 changes: 25 additions & 1 deletion DuckDB-Memory-Tests/T_DuckTools.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using DuckDb_Memory_Lib;
using DuckDb_Memory_Lib;

namespace DuckDB_Memory_Tests;

Expand Down Expand Up @@ -88,6 +88,30 @@ public void T_GetListTables_And_DropTable_Handle_Existing_And_Missing_Tables()
});
}


[Test]
public void T_DeleteRegister_Ignores_Missing_Id()
{
Assert.DoesNotThrow(() => KeeperRegisterIdDataBase.DeleteRegister($"MISSING_{Guid.NewGuid():N}"));
}

[Test]
public void T_CreateTable_Quotes_Database_And_Table_Identifiers()
{
var databaseId = $"TOOLS DB {Guid.NewGuid():N}";
var tableId = $"TOOLS TABLE {Guid.NewGuid():N}";
var conn = ConnectionManager.GetInstance().GetConnection(databaseId);

var dbOutput = DuckTools.CreateDatabase(conn, databaseId, null);
Assert.That(dbOutput, Is.EqualTo(EnumsDuckMemory.Output.SUCCESS));

var createOutput = QueryExecutor.CreateTable(conn, databaseId, tableId, ["id INTEGER"]);
Assert.That(createOutput, Is.EqualTo(EnumsDuckMemory.Output.SUCCESS));

var listTables = DuckTools.GetListTables(conn, databaseId);
Assert.That(listTables.Item2, Does.Contain(tableId));
}

[Test]
public void T_DeleteDataBase_Detaches_Existing_Database_And_Validates_Blank_Id()
{
Expand Down
20 changes: 19 additions & 1 deletion DuckDB-Memory-Tests/T_Execute_Queries.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Reflection;
using System.Reflection;
using DuckDb_Memory_Lib;

namespace DuckDB_Memory_Tests;
Expand Down Expand Up @@ -41,4 +41,22 @@ public void T_Create_Table_From_Parquet()
Assert.That(resultQry.Item1, Is.EqualTo(EnumsDuckMemory.Output.SUCCESS));

}
[Test]
public void T_ExecuteQryReader_Uses_DuckDb_Parameters()
{
var manager = ConnectionManager.GetInstance();
var conn = manager.GetConnection();

var result = QueryExecutor.ExecuteQryReader(
conn,
"SELECT $param AS VALUE",
new Dictionary<string, string> { ["param"] = "O'Reilly" });

Assert.Multiple(() =>
{
Assert.That(result.Item1, Is.EqualTo(EnumsDuckMemory.Output.SUCCESS));
Assert.That(result.Item2.Single()["VALUE"], Is.EqualTo("O'Reilly"));
});
}

}
Loading
Loading