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