diff --git a/Directory.Build.props b/Directory.Build.props
index 2986c46..f85aa3d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -20,6 +20,6 @@
-
+
diff --git a/DuckDB-Memory-Lib/ConnectionManager.cs b/DuckDB-Memory-Lib/ConnectionManager.cs
index 5a2f800..aa55ece 100644
--- a/DuckDB-Memory-Lib/ConnectionManager.cs
+++ b/DuckDB-Memory-Lib/ConnectionManager.cs
@@ -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
{
private static readonly Lazy LazyInstance =
new(() => new ConnectionManager(), LazyThreadSafetyMode.ExecutionAndPublication);
- private readonly object _syncRoot = new();
- private readonly Dictionary _connections = new(StringComparer.OrdinalIgnoreCase);
-
private ConnectionManager() { }
///
@@ -23,98 +20,15 @@ public static ConnectionManager GetInstance()
}
///
- /// 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.
///
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;
- }
- }
-
- ///
- /// Closes and disposes the connection associated with the provided alias.
- ///
- 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);
- }
- }
- }
-
- ///
- /// Closes and disposes all active SQLite connections managed by this instance.
- ///
- 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);
}
///
- /// Opens the provided SQLite connection when it is not already open.
- ///
- private static void EnsureOpen(DuckDBConnection connection)
- {
- if (connection.State != ConnectionState.Open)
- {
- connection.Open();
- }
- }
-
- ///
- /// Normalizes aliases to ensure consistent lookups inside the connection dictionary.
- ///
- private static string NormalizeAlias(string? alias)
- {
- return string.IsNullOrWhiteSpace(alias) ? "default" : alias.Trim();
- }
-
- ///
- /// Static helper that delegates to .
+ /// Static helper that delegates to .
///
public static void Close(string? alias = null)
{
@@ -128,7 +42,7 @@ public static void CloseAll()
{
GetInstance().CloseAllConnections();
}
- }
+}
public sealed class KeeperRegisterIdDataBase
{
@@ -182,11 +96,13 @@ public static void Register(string path, string idDataBase)
///
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);
+ }
}
}
diff --git a/DuckDB-Memory-Lib/DuckDB-Memory-Lib.csproj b/DuckDB-Memory-Lib/DuckDB-Memory-Lib.csproj
index 55d0e02..2729815 100644
--- a/DuckDB-Memory-Lib/DuckDB-Memory-Lib.csproj
+++ b/DuckDB-Memory-Lib/DuckDB-Memory-Lib.csproj
@@ -1,4 +1,4 @@
-
+
DuckDb_Memory_Lib
@@ -7,7 +7,11 @@
-
+
+
+
+
+
diff --git a/DuckDB-Memory-Lib/DuckTools.cs b/DuckDB-Memory-Lib/DuckTools.cs
index 59df907..1defec4 100644
--- a/DuckDB-Memory-Lib/DuckTools.cs
+++ b/DuckDB-Memory-Lib/DuckTools.cs
@@ -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;
@@ -128,8 +128,7 @@ public static EnumsDuckMemory.Output RegisterScalarFunction(
{
db.RegisterScalarFunction(
idFunction,
- func,
- isPureFunction: true);
+ func);
return EnumsDuckMemory.Output.SUCCESS;
}
@@ -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
diff --git a/DuckDB-Memory-Lib/DuckTypes.cs b/DuckDB-Memory-Lib/DuckTypes.cs
index 1ec8a1e..c118211 100644
--- a/DuckDB-Memory-Lib/DuckTypes.cs
+++ b/DuckDB-Memory-Lib/DuckTypes.cs
@@ -1,4 +1,5 @@
-using DuckDB.NET.Native;
+using System.Data;
+using DuckDB.NET.Native;
namespace DuckDb_Memory_Lib;
@@ -76,4 +77,61 @@ public static (object? Value, Type NetType) StrTryParse(string? value)
// Fallback → VARCHAR
return (value, typeof(string));
}
+
+ ///
+ /// Maps a .NET type to the closest ADO.NET DbType.
+ /// The resulting DbType can be used by DuckDB.NET to infer the native DuckDB type.
+ ///
+ public static DbType GetDbType(Type type)
+ {
+ type = Nullable.GetUnderlyingType(type) ?? type;
+
+ if (type == typeof(bool))
+ return DbType.Boolean;
+
+ if (type == typeof(byte))
+ return DbType.Byte;
+
+ if (type == typeof(short))
+ return DbType.Int16;
+
+ if (type == typeof(int))
+ return DbType.Int32;
+
+ if (type == typeof(long))
+ return DbType.Int64;
+
+ if (type == typeof(float))
+ return DbType.Single;
+
+ if (type == typeof(double))
+ return DbType.Double;
+
+ if (type == typeof(decimal))
+ return DbType.Decimal;
+
+ if (type == typeof(DateOnly))
+ return DbType.Date;
+
+ if (type == typeof(TimeOnly))
+ return DbType.Time;
+
+ if (type == typeof(DateTime))
+ return DbType.DateTime;
+
+ if (type == typeof(Guid))
+ return DbType.Guid;
+
+ if (type == typeof(byte[]))
+ return DbType.Binary;
+
+ if (type == typeof(string))
+ return DbType.String;
+
+ throw new NotSupportedException(
+ $"The .NET type '{type.FullName}' cannot be mapped to a DbType.");
+ }
}
+
+
+
diff --git a/DuckDB-Memory-Lib/QueryExecutor.cs b/DuckDB-Memory-Lib/QueryExecutor.cs
index c3c0693..46ce7b7 100644
--- a/DuckDB-Memory-Lib/QueryExecutor.cs
+++ b/DuckDB-Memory-Lib/QueryExecutor.cs
@@ -1,6 +1,7 @@
-using System.Diagnostics;
+using System.Diagnostics;
using DuckDB.NET.Data;
using System.IO;
+using DuckDB.NET.Native;
namespace DuckDb_Memory_Lib;
@@ -30,15 +31,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;
@@ -71,7 +73,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;
@@ -90,8 +92,8 @@ public static (EnumsDuckMemory.Output, List>) 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>();
@@ -104,7 +106,6 @@ public static (EnumsDuckMemory.Output, List>) Execute
}
}
- qryResult.Close();
return (EnumsDuckMemory.Output.SUCCESS, resultList);
}
catch (Exception ex)
@@ -121,14 +122,34 @@ public static (EnumsDuckMemory.Output, List>) 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)
+ {
+ var dbParameter = cmd.CreateParameter();
+ dbParameter.ParameterName = parameter.Key;
+ dbParameter.Value = parameter.Value;
+ dbParameter.DbType = NetTypeToDuckDbType.GetDbType(parameter.Value.GetType());
+ cmd.Parameters.Add(dbParameter);
+ }
+
+ using var qryResult = cmd.ExecuteReader();
+ var resultList = new List>();
+
+ 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, []);
}
}
}
\ No newline at end of file
diff --git a/DuckDB-Memory-Tests/T_DuckTools.cs b/DuckDB-Memory-Tests/T_DuckTools.cs
index 4ca40a9..9af0ca4 100644
--- a/DuckDB-Memory-Tests/T_DuckTools.cs
+++ b/DuckDB-Memory-Tests/T_DuckTools.cs
@@ -1,4 +1,4 @@
-using DuckDb_Memory_Lib;
+using DuckDb_Memory_Lib;
namespace DuckDB_Memory_Tests;
@@ -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()
{
diff --git a/DuckDB-Memory-Tests/T_Execute_Queries.cs b/DuckDB-Memory-Tests/T_Execute_Queries.cs
index 9158797..52e2937 100644
--- a/DuckDB-Memory-Tests/T_Execute_Queries.cs
+++ b/DuckDB-Memory-Tests/T_Execute_Queries.cs
@@ -1,4 +1,4 @@
-using System.Reflection;
+using System.Reflection;
using DuckDb_Memory_Lib;
namespace DuckDB_Memory_Tests;
@@ -41,4 +41,20 @@ 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 conn = ConnectionManager.GetInstance().GetConnection($"PARAM_DB_{Guid.NewGuid():N}");
+ var result = QueryExecutor.ExecuteQryReader(
+ conn,
+ "SELECT $value AS VALUE",
+ new Dictionary { ["value"] = "O'Reilly" });
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(result.Item1, Is.EqualTo(EnumsDuckMemory.Output.SUCCESS));
+ Assert.That(result.Item2.Single()["VALUE"], Is.EqualTo("O'Reilly"));
+ });
+ }
+
}
diff --git a/Shared/ConnectionManagerBase.cs b/Shared/ConnectionManagerBase.cs
new file mode 100644
index 0000000..d2fba45
--- /dev/null
+++ b/Shared/ConnectionManagerBase.cs
@@ -0,0 +1,98 @@
+using System.Data;
+using System.Data.Common;
+
+namespace MemoryDb_Lib.Shared;
+
+///
+/// Thread-safe base implementation for named database connection managers.
+///
+public abstract class ConnectionManagerBase where TConnection : DbConnection
+{
+ private readonly object _syncRoot = new();
+ private readonly Dictionary _connections = new(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Retrieves an open connection identified by the provided alias, creating it when necessary.
+ ///
+ protected TConnection GetConnectionCore(string? alias, string? path, Func connectionFactory)
+ {
+ var normalizedAlias = NormalizeAlias(alias);
+
+ lock (_syncRoot)
+ {
+ if (_connections.TryGetValue(normalizedAlias, out var existingConnection))
+ {
+ EnsureOpen(existingConnection);
+ return existingConnection;
+ }
+
+ var newConnection = connectionFactory(path);
+ EnsureOpen(newConnection);
+ _connections[normalizedAlias] = newConnection;
+
+ return newConnection;
+ }
+ }
+
+ ///
+ /// Closes and disposes the connection associated with the provided alias.
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// Closes and disposes all active connections managed by this instance.
+ ///
+ public void CloseAllConnections()
+ {
+ lock (_syncRoot)
+ {
+ foreach (var connection in _connections.Values)
+ {
+ try
+ {
+ connection.Close();
+ }
+ finally
+ {
+ connection.Dispose();
+ }
+ }
+
+ _connections.Clear();
+ }
+ }
+
+ private static void EnsureOpen(TConnection connection)
+ {
+ if (connection.State != ConnectionState.Open)
+ {
+ connection.Open();
+ }
+ }
+
+ private static string NormalizeAlias(string? alias)
+ {
+ return string.IsNullOrWhiteSpace(alias) ? "default" : alias.Trim();
+ }
+}
diff --git a/SqliteDB-Memory-Lib/ConnectionManager.cs b/SqliteDB-Memory-Lib/ConnectionManager.cs
index be61eeb..9fd0065 100644
--- a/SqliteDB-Memory-Lib/ConnectionManager.cs
+++ b/SqliteDB-Memory-Lib/ConnectionManager.cs
@@ -1,16 +1,13 @@
using Microsoft.Data.Sqlite;
-using System.Data;
+using MemoryDb_Lib.Shared;
namespace SqliteDB_Memory_Lib;
-public sealed class ConnectionManager
+public sealed class ConnectionManager : ConnectionManagerBase
{
private static readonly Lazy LazyInstance =
new(() => new ConnectionManager(), LazyThreadSafetyMode.ExecutionAndPublication);
- private readonly object _syncRoot = new();
- private readonly Dictionary _connections = new(StringComparer.OrdinalIgnoreCase);
-
private ConnectionManager() { }
///
@@ -26,94 +23,11 @@ public static ConnectionManager GetInstance()
///
public SqliteConnection 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 = SqLiteLiteTools.GetInstance(path);
- EnsureOpen(newConnection);
- _connections[normalizedAlias] = newConnection;
-
- return newConnection;
- }
- }
-
- ///
- /// Closes and disposes the connection associated with the provided alias.
- ///
- 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);
- }
- }
+ return GetConnectionCore(alias, path, SqLiteLiteTools.GetInstance);
}
///
- /// Closes and disposes all active SQLite connections managed by this instance.
- ///
- public void CloseAllConnections()
- {
- lock (_syncRoot)
- {
- foreach (var connection in _connections.Values)
- {
- try
- {
- connection.Close();
- }
- finally
- {
- connection.Dispose();
- }
- }
-
- _connections.Clear();
- }
- }
-
- ///
- /// Opens the provided SQLite connection when it is not already open.
- ///
- private static void EnsureOpen(SqliteConnection connection)
- {
- if (connection.State != ConnectionState.Open)
- {
- connection.Open();
- }
- }
-
- ///
- /// Normalizes aliases to ensure consistent lookups inside the connection dictionary.
- ///
- private static string NormalizeAlias(string? alias)
- {
- return string.IsNullOrWhiteSpace(alias) ? "default" : alias.Trim();
- }
-
- ///
- /// Static helper that delegates to .
+ /// Static helper that delegates to .
///
public static void Close(string? alias = null)
{
@@ -127,7 +41,7 @@ public static void CloseAll()
{
GetInstance().CloseAllConnections();
}
- }
+}
public sealed class KeeperRegisterIdDataBase
{
diff --git a/SqliteDB-Memory-Lib/SqliteDB-Memory-Lib.csproj b/SqliteDB-Memory-Lib/SqliteDB-Memory-Lib.csproj
index 457f1f0..afe9a85 100644
--- a/SqliteDB-Memory-Lib/SqliteDB-Memory-Lib.csproj
+++ b/SqliteDB-Memory-Lib/SqliteDB-Memory-Lib.csproj
@@ -9,4 +9,8 @@
+
+
+
+