diff --git a/Directory.Build.props b/Directory.Build.props
index 2986c46..90dab28 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,6 @@
- net8.0-windows
+ net10.0-windows
13.0
enable
enable
@@ -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..a71a0cc 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/QueryExecutor.cs b/DuckDB-Memory-Lib/QueryExecutor.cs
index c3c0693..9381a65 100644
--- a/DuckDB-Memory-Lib/QueryExecutor.cs
+++ b/DuckDB-Memory-Lib/QueryExecutor.cs
@@ -1,4 +1,4 @@
-using System.Diagnostics;
+using System.Diagnostics;
using DuckDB.NET.Data;
using System.IO;
@@ -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;
@@ -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;
@@ -90,8 +91,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 +105,6 @@ public static (EnumsDuckMemory.Output, List>) Execute
}
}
- qryResult.Close();
return (EnumsDuckMemory.Output.SUCCESS, resultList);
}
catch (Exception ex)
@@ -121,14 +121,37 @@ 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)
+ {
+ cmd.Parameters.Add(new DuckDBParameter(
+ NormalizeParameterName(parameter.Key),
+ parameter.Value));
+ }
+
+ 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, []);
}
}
+
+ private static string NormalizeParameterName(string parameterName)
+ {
+ return parameterName.TrimStart('@', '$', ':');
+ }
}
\ 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..3e7f8f2 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,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 { ["param"] = "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 @@
+
+
+
+
diff --git a/XLS-Memory-Lib/Properties/launchSettings.json b/XLS-Memory-Lib/Properties/launchSettings.json
new file mode 100644
index 0000000..8fec72d
--- /dev/null
+++ b/XLS-Memory-Lib/Properties/launchSettings.json
@@ -0,0 +1,9 @@
+{
+ "profiles": {
+ "Excel": {
+ "commandName": "Executable",
+ "executablePath": "C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE",
+ "commandLineArgs": "/x \"XLS-Memory-Lib.xll\""
+ }
+ }
+}
\ No newline at end of file
diff --git a/XLS-Memory-Lib/XLS-Memory-Lib.csproj b/XLS-Memory-Lib/XLS-Memory-Lib.csproj
index f271d45..0251e71 100644
--- a/XLS-Memory-Lib/XLS-Memory-Lib.csproj
+++ b/XLS-Memory-Lib/XLS-Memory-Lib.csproj
@@ -1,7 +1,7 @@
- net8.0-windows
+ net10.0-windows
v8.0