From 9fe6a8ee36ea80cbfe8a603aaf27e40c13ac165a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 26 Feb 2026 06:08:27 -0500 Subject: [PATCH] feat(porttracker): add DB access layer and init command Add Database.cs with SQLite connection management and helper methods (Execute, ExecuteScalar, Query), Schema.cs for schema initialization, and replace default Program.cs with System.CommandLine v3 CLI featuring global --db/--schema options and an init command. --- tools/NatsNet.PortTracker/Data/Database.cs | 77 ++++++++++++++++++++++ tools/NatsNet.PortTracker/Data/Schema.cs | 10 +++ tools/NatsNet.PortTracker/Program.cs | 38 ++++++++++- 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 tools/NatsNet.PortTracker/Data/Database.cs create mode 100644 tools/NatsNet.PortTracker/Data/Schema.cs diff --git a/tools/NatsNet.PortTracker/Data/Database.cs b/tools/NatsNet.PortTracker/Data/Database.cs new file mode 100644 index 0000000..fb4bc43 --- /dev/null +++ b/tools/NatsNet.PortTracker/Data/Database.cs @@ -0,0 +1,77 @@ +using Microsoft.Data.Sqlite; + +namespace NatsNet.PortTracker.Data; + +public sealed class Database : IDisposable +{ + private readonly SqliteConnection _connection; + + public Database(string dbPath) + { + var connectionString = new SqliteConnectionStringBuilder + { + DataSource = dbPath, + Mode = SqliteOpenMode.ReadWriteCreate, + ForeignKeys = true + }.ToString(); + + _connection = new SqliteConnection(connectionString); + _connection.Open(); + + using var cmd = _connection.CreateCommand(); + cmd.CommandText = "PRAGMA journal_mode=WAL;"; + cmd.ExecuteNonQuery(); + } + + public SqliteConnection Connection => _connection; + + public SqliteCommand CreateCommand(string sql) + { + var cmd = _connection.CreateCommand(); + cmd.CommandText = sql; + return cmd; + } + + public int Execute(string sql, params (string name, object? value)[] parameters) + { + using var cmd = CreateCommand(sql); + foreach (var (name, value) in parameters) + cmd.Parameters.AddWithValue(name, value ?? DBNull.Value); + return cmd.ExecuteNonQuery(); + } + + public T? ExecuteScalar(string sql, params (string name, object? value)[] parameters) + { + using var cmd = CreateCommand(sql); + foreach (var (name, value) in parameters) + cmd.Parameters.AddWithValue(name, value ?? DBNull.Value); + var result = cmd.ExecuteScalar(); + if (result is null or DBNull) return default; + return (T)Convert.ChangeType(result, typeof(T)); + } + + public List> Query(string sql, params (string name, object? value)[] parameters) + { + using var cmd = CreateCommand(sql); + foreach (var (name, value) in parameters) + cmd.Parameters.AddWithValue(name, value ?? DBNull.Value); + + var results = new List>(); + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + var row = new Dictionary(); + for (int i = 0; i < reader.FieldCount; i++) + { + row[reader.GetName(i)] = reader.IsDBNull(i) ? null : reader.GetValue(i); + } + results.Add(row); + } + return results; + } + + public void Dispose() + { + _connection.Dispose(); + } +} diff --git a/tools/NatsNet.PortTracker/Data/Schema.cs b/tools/NatsNet.PortTracker/Data/Schema.cs new file mode 100644 index 0000000..87ba610 --- /dev/null +++ b/tools/NatsNet.PortTracker/Data/Schema.cs @@ -0,0 +1,10 @@ +namespace NatsNet.PortTracker.Data; + +public static class Schema +{ + public static void Initialize(Database db, string schemaPath) + { + var sql = File.ReadAllText(schemaPath); + db.Execute(sql); + } +} diff --git a/tools/NatsNet.PortTracker/Program.cs b/tools/NatsNet.PortTracker/Program.cs index 3751555..a69eaef 100644 --- a/tools/NatsNet.PortTracker/Program.cs +++ b/tools/NatsNet.PortTracker/Program.cs @@ -1,2 +1,36 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using System.CommandLine; +using NatsNet.PortTracker.Data; + +var dbOption = new Option("--db") +{ + Description = "Path to the SQLite database file", + DefaultValueFactory = _ => Path.Combine(Directory.GetCurrentDirectory(), "porting.db"), + Recursive = true +}; + +var schemaOption = new Option("--schema") +{ + Description = "Path to the SQL schema file", + DefaultValueFactory = _ => Path.Combine(Directory.GetCurrentDirectory(), "porting-schema.sql"), + Recursive = true +}; + +var rootCommand = new RootCommand("NATS .NET Porting Tracker"); +rootCommand.Add(dbOption); +rootCommand.Add(schemaOption); + +// init command +var initCommand = new Command("init", "Create or reset the database schema"); +initCommand.SetAction(parseResult => +{ + var dbPath = parseResult.GetValue(dbOption)!; + var schemaPath = parseResult.GetValue(schemaOption)!; + using var db = new Database(dbPath); + Schema.Initialize(db, schemaPath); + Console.WriteLine($"Database initialized at {dbPath}"); +}); + +rootCommand.Add(initCommand); + +var parseResult = rootCommand.Parse(args); +return await parseResult.InvokeAsync();