From e9ec3638c5a6837dedd1ce499da1c907bad85947 Mon Sep 17 00:00:00 2001 From: JacobTech Date: Fri, 22 Mar 2024 12:11:19 -0400 Subject: [PATCH] ForeignKeys & CommandHandler --- Database/CommandHandler.cs | 91 ++++++++++++++++++ Database/Database.cs | 10 +- Database/ForeignKey.cs | 31 ++++++ Database/IBetterTable.cs | 8 ++ Database/ITable.cs | 12 --- Database/ServerDatabase.csproj | 7 +- Database/Table.cs | 171 +++++++++++++++++++++++++++------ Database/TableColumn.cs | 2 + 8 files changed, 277 insertions(+), 55 deletions(-) create mode 100644 Database/CommandHandler.cs create mode 100644 Database/ForeignKey.cs create mode 100644 Database/IBetterTable.cs delete mode 100644 Database/ITable.cs diff --git a/Database/CommandHandler.cs b/Database/CommandHandler.cs new file mode 100644 index 0000000..5c43ee4 --- /dev/null +++ b/Database/CommandHandler.cs @@ -0,0 +1,91 @@ +using Npgsql; +using ServerDatabase.Utils; + +namespace ServerDatabase; + +public class CommandHandler +{ + private IBetterTable _betterTable; + + internal CommandHandler(IBetterTable betterTable) + { + this._betterTable = betterTable; + } + + private List Parameters = new(); + private List Values = new(); + + private Order? Order = null; + + public CommandHandler WithFilter(Parameter p) + { + Parameters.Add(p); + return this; + } + public CommandHandler WithValue(Parameter p) + { + Values.Add(p); + return this; + } + + public CommandHandler WithFilter(TableColumn column, T value, string sign = "=") where T : notnull + { + return WithFilter(column.CreateParameter(value, sign)); + } + + public CommandHandler WithValue(TableColumn column, T value) where T : notnull + { + return WithValue(column.CreateParameter(value)); + } + + public CommandHandler AscendBy(TableColumn column) where T : notnull + { + Order = column.GetAssendingOrder(); + return this; + } + + public CommandHandler DescendBy(TableColumn column) where T : notnull + { + Order = column.GetDecendingOrder(); + return this; + } + + + public T Read(TableColumn column) where T : notnull + { + return _betterTable.DatabaseHandler.Read($"SELECT {column.Name} FROM {_betterTable.Name} WHERE", Order, Parameters.ToArray()); + } + + public void Insert() + { + _betterTable.Insert(Values.ToArray()); + } + + public void Update() + { + if (string.IsNullOrEmpty(_betterTable.DatabaseHandler.DB) || string.IsNullOrEmpty(_betterTable.DatabaseHandler.IP) || string.IsNullOrEmpty(_betterTable.DatabaseHandler.Uname) || string.IsNullOrEmpty(_betterTable.DatabaseHandler.PW)) throw new Exception("Database connection not fully defined"); + using NpgsqlConnection con = new(_betterTable.DatabaseHandler.ConectionString); + con.Open(); + using NpgsqlCommand cmd = new(); + cmd.Connection = con; + string values = ""; + foreach (Parameter param in Values) + { + values += $"{param.PGParameter.ParameterName} {param.Sign} @{param.PGParameter.ParameterName}, "; + cmd.Parameters.Add(param.PGParameter); + } + values = values.Remove(values.Length - 2, 2); + + string fils = ""; + foreach (Parameter param in Parameters) + { + fils += $"{param.PGParameter.ParameterName} {param.Sign} @{param.PGParameter.ParameterName} AND"; + cmd.Parameters.Add(param.PGParameter); + } + fils = fils.Remove(fils.Length - 4, 4); + cmd.CommandText = $"UPDATE {_betterTable.Name} SET {values} WHERE {fils};"; + cmd.Prepare(); + cmd.ExecuteNonQuery(); + con.Close(); + } +} \ No newline at end of file diff --git a/Database/Database.cs b/Database/Database.cs index 6feb755..59737d5 100755 --- a/Database/Database.cs +++ b/Database/Database.cs @@ -1,8 +1,4 @@ using Npgsql; -using NpgsqlTypes; -using System.Data; -using System.Runtime.InteropServices; -using System.Security.Cryptography; using ServerDatabase.Utils; namespace ServerDatabase; @@ -17,7 +13,7 @@ public sealed class Database } } - public List Tables { get; } = new() + public List Tables { get; } = new() { }; @@ -38,7 +34,7 @@ public sealed class Database public void RegisterTables() { - foreach (ITable Table in Tables) + foreach (IBetterTable Table in Tables) { foreach (ITableColumn Column in Table.Colums) { @@ -49,7 +45,7 @@ public sealed class Database } } VersionsTable.UpdateTable(); - foreach (ITable Table in Tables) + foreach (IBetterTable Table in Tables) { Table.UpdateTable(); } diff --git a/Database/ForeignKey.cs b/Database/ForeignKey.cs new file mode 100644 index 0000000..3567303 --- /dev/null +++ b/Database/ForeignKey.cs @@ -0,0 +1,31 @@ +using ServerDatabase.Utils; +using ServerDatabase.Utils.Enums; + +namespace ServerDatabase; + +public class ForeignKey : IForeignKey +{ + public ConstraintAction OnUpdate { get; init; } = ConstraintAction.None; + public ConstraintAction OnDelete { get; init; } = ConstraintAction.None; + public KeyMatchType MatchType { get; init; } = KeyMatchType.Simple; + + public required ITable Table { get; init; } + public required ITableColumn Column { get; init; } + + private bool d = false; + + public bool Deferrable { get; init; } = false; + + public bool Deferred + { + get + { + return d & Deferrable; + } + init + { + d = value; + } + } + public bool Validated { get; init; } = false; +} \ No newline at end of file diff --git a/Database/IBetterTable.cs b/Database/IBetterTable.cs new file mode 100644 index 0000000..fe8822c --- /dev/null +++ b/Database/IBetterTable.cs @@ -0,0 +1,8 @@ +using ServerDatabase.Utils; + +namespace ServerDatabase; + +public interface IBetterTable : ITable +{ + public Database DatabaseHandler { get; set; } +} \ No newline at end of file diff --git a/Database/ITable.cs b/Database/ITable.cs deleted file mode 100644 index e4441a9..0000000 --- a/Database/ITable.cs +++ /dev/null @@ -1,12 +0,0 @@ -using ServerDatabase.Utils; - -namespace ServerDatabase; - -public interface ITable -{ - public void UpdateTable(); - public IReadOnlyList Colums { get; } - public string Name { get; set; } - public Database DatabaseHandler { get; set; } - public void AddColumn(ITableColumn Column, string name); -} \ No newline at end of file diff --git a/Database/ServerDatabase.csproj b/Database/ServerDatabase.csproj index a361721..00f74a3 100755 --- a/Database/ServerDatabase.csproj +++ b/Database/ServerDatabase.csproj @@ -10,8 +10,8 @@ snupkg Postgresql;sql Server Database - 2.8.9 - 10 + 2.9.9 + 11 enable net8.0 @@ -22,8 +22,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + diff --git a/Database/Table.cs b/Database/Table.cs index 1d0209a..bbc88eb 100644 --- a/Database/Table.cs +++ b/Database/Table.cs @@ -1,13 +1,13 @@ using System.Data; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using Npgsql; using ServerDatabase.Utils; +using ServerDatabase.Utils.Enums; namespace ServerDatabase; -public class Table : ITable where TRow : class, new() +public class Table : IBetterTable where TRow : class, new() { public Table(string Name, Database DB) { @@ -22,6 +22,11 @@ public class Table : ITable where TRow : class, new() DatabaseHandler = DB; } + public CommandHandler CreateCommand() + { + return new CommandHandler(this); + } + public Table WithColumn(ITableColumn Column, string name) { Colums_.Add(Column); @@ -34,7 +39,6 @@ public class Table : ITable where TRow : class, new() colnamesraw.Add(Column.Name, name); Colums_.Add(Column); } - public IReadOnlyList GetMissingColumns(out IReadOnlyList UpdatedColumns, out string[] ExtraColumns) { @@ -222,25 +226,94 @@ public class Table : ITable where TRow : class, new() con.Open(); using NpgsqlCommand cmd = new(); cmd.Connection = con; - string command = $"CREATE TABLE public.{Name}("; - string key = ""; + string command = $"CREATE TABLE IF NOT EXISTS public.{Name}("; + List Keys = new(); + + List> FKeys = new(); foreach (ITableColumn column in Colums_) { command += $"{column.Name} {column.GetDatabaseTypeStr()}"; - if (column.IsPrimaryKey) key = $"PRIMARY KEY ({column.Name})"; + if (column.IsPrimaryKey) Keys.Add(column.Name); command += " NOT NULL, "; + if (column.ForeignKeys.Length > 0) + { + foreach (IForeignKey Fkey in column.ForeignKeys) + { + FKeys.Add(new(column, Fkey)); + } + } } - if (key == "") command = command.Remove(command.Length - 2, 2) + ");"; - command += $"{key});"; + + if (Keys.Count > 0) + { + string key = $"CONSTRAINT {Name}_pkey PRIMARY KEY ("; + foreach (var Key in Keys) + { + key += Key + ", "; + } + + key = key.Remove(key.Length - 2, 2); + key += "), "; + command += key; + } + + if (FKeys.Count > 0) + { + foreach (Tuple K in FKeys) + { + string key = $"CONSTRAINT {Name}_{K.Item1.Name}_{K.Item2.Table.Name}_{K.Item2.Column.Name}_fkey FOREIGN KEY ({K.Item1.Name}) REFERENCES public.{K.Item2.Table.Name} ({K.Item2.Column.Name}) MATCH "; + if (K.Item2.MatchType == KeyMatchType.Full) key += "FULL ON UPDATE "; + else key += "SIMPLE ON UPDATE "; + switch (K.Item2.OnUpdate) + { + case ConstraintAction.None: + key += "NO ACTION ON DELETE "; + break; + case ConstraintAction.Cascade: + key += "CASCADE ON DELETE "; + break; + case ConstraintAction.Restrict: + key += "RESTRICT ON DELETE "; + break; + case ConstraintAction.SetDefault: + key += "SET DEFAULT ON DELETE "; + break; + } + switch (K.Item2.OnDelete) + { + case ConstraintAction.None: + key += "NO ACTION "; + break; + case ConstraintAction.Cascade: + key += "CASCADE "; + break; + case ConstraintAction.Restrict: + key += "RESTRICT "; + break; + case ConstraintAction.SetDefault: + key += "SET DEFAULT "; + break; + } + + if (K.Item2.Deferrable) key += "DEFERRABLE "; + if (K.Item2.Deferred) key += "INITIALLY DEFERRED "; + if (!K.Item2.Validated) key += "NOT VALID "; + key = key.Remove(key.Length - 1, 1) + ", "; + command += key; + } + } + + command = command.Remove(command.Length - 2, 2); + + command += ");"; cmd.CommandText = command; cmd.Prepare(); cmd.ExecuteNonQuery(); con.Close(); - DatabaseHandler.ExecuteNonQuery($"ALTER TABLE IF EXISTS " + - $"public.{Name} OWNER to \"{DatabaseHandler.Uname}\";"); + DatabaseHandler.ExecuteNonQuery($"ALTER TABLE IF EXISTS public.{Name} OWNER to \"{DatabaseHandler.Uname}\";"); } - public bool TryRead(TableColumn column, Order order, [NotNullWhen(true)][MaybeNullWhen(false)]out T result, params Parameter[] Parameters) where T : notnull + public bool TryRead(TableColumn column, Order order, [NotNullWhen(true)]out T? result, params Parameter[] Parameters) where T : notnull { if (string.IsNullOrEmpty(DatabaseHandler.DB) || string.IsNullOrEmpty(DatabaseHandler.IP) || string.IsNullOrEmpty(DatabaseHandler.Uname) || string.IsNullOrEmpty(DatabaseHandler.PW)) throw new Exception("Database conection not fully defined"); using NpgsqlConnection con = new(DatabaseHandler.ConectionString); @@ -361,9 +434,9 @@ public class Table : ITable where TRow : class, new() return r; } - public bool TryReadRows(out TRow[] rows, params Parameter[] Parameters) => TryReadRows(null!, out rows, Parameters); + public bool TryReadRows([NotNullWhen(true)]out TRow[]? rows, params Parameter[] Parameters) => TryReadRows(null!, out rows, Parameters); - public bool TryReadRows(Order order, out TRow[] rows, params Parameter[] Parameters) + public bool TryReadRows(Order order, [NotNullWhen(true)]out TRow[]? rows, params Parameter[] Parameters) { if (string.IsNullOrEmpty(DatabaseHandler.DB) || string.IsNullOrEmpty(DatabaseHandler.IP) || string.IsNullOrEmpty(DatabaseHandler.Uname) || string.IsNullOrEmpty(DatabaseHandler.PW)) throw new Exception("Database conection not fully defined"); using NpgsqlConnection con = new(DatabaseHandler.ConectionString); @@ -389,9 +462,9 @@ public class Table : ITable where TRow : class, new() List Rows = new(); while (reader.Read()) { + TRow Row = new(); for (int i = 0; i < reader.FieldCount; i++) { - TRow Row = new(); string colname = reader.GetName(i); object? val = reader.GetValue(i); if (colnamesraw.ContainsKey(colname)) @@ -405,15 +478,15 @@ public class Table : ITable where TRow : class, new() } pi.SetValue(Row, val, null); } - Rows.Add(Row); } + Rows.Add(Row); } rows = Rows.ToArray(); return Rows.Any(); } - public bool TryRead(TableColumn column, [NotNullWhen(true)][MaybeNullWhen(false)]out T result, params Parameter[] Parameters) where T : notnull + public bool TryRead(TableColumn column, [NotNullWhen(true)]out T? result, params Parameter[] Parameters) where T : notnull { return TryRead(column, null!, out result, Parameters); } @@ -425,23 +498,12 @@ public class Table : ITable where TRow : class, new() public void Update(TableColumn condiction_column, Ttype condiction_value, string Sign, params Parameter[] Parameters) where Ttype : notnull { - if (string.IsNullOrEmpty(DatabaseHandler.DB) || string.IsNullOrEmpty(DatabaseHandler.IP) || string.IsNullOrEmpty(DatabaseHandler.Uname) || string.IsNullOrEmpty(DatabaseHandler.PW)) throw new Exception("Database conection not fully defined"); - using NpgsqlConnection con = new(DatabaseHandler.ConectionString); - con.Open(); - using NpgsqlCommand cmd = new(); - cmd.Connection = con; - string vals = ""; - foreach (Parameter param in Parameters) + CommandHandler command = CreateCommand().WithFilter(condiction_column, condiction_value, Sign); + foreach (var Parameter in Parameters) { - vals += $"{param.PGParameter.ParameterName} {param.Sign} @{param.PGParameter.ParameterName}, "; - cmd.Parameters.Add(param.PGParameter); + command.WithValue(Parameter); } - vals = vals.Remove(vals.Length - 2, 2); - cmd.Parameters.Add(condiction_column.CreateParameter(condiction_value).PGParameter); - cmd.CommandText = $"UPDATE {Name} SET {vals} WHERE {condiction_column.Name} {Sign} @{condiction_column.Name};"; - cmd.Prepare(); - cmd.ExecuteNonQuery(); - con.Close(); + command.Update(); } public TRow ReadRow(params Parameter[] Parameters) => ReadRow(null, Parameters); @@ -490,10 +552,54 @@ public class Table : ITable where TRow : class, new() return Row; } + + public TColumn[] ReadColumn(TableColumn column, params Parameter[] Parameters) where TColumn : notnull => ReadColumn(column, uint.MaxValue, null, Parameters); + + public TColumn[] ReadColumn(TableColumn column, uint NumRows, params Parameter[] Parameters) where TColumn : notnull => ReadColumn(column, NumRows, null, Parameters); + + public TColumn[] ReadColumn(TableColumn column, Order? Order, params Parameter[] Parameters) where TColumn : notnull => ReadColumn(column, uint.MaxValue, Order, Parameters); + + public TColumn[] ReadColumn(TableColumn column, uint NumRows, Order? order, params Parameter[] Parameters) where TColumn : notnull + { + if (string.IsNullOrEmpty(DatabaseHandler.DB) || string.IsNullOrEmpty(DatabaseHandler.IP) || string.IsNullOrEmpty(DatabaseHandler.Uname) || string.IsNullOrEmpty(DatabaseHandler.PW)) throw new Exception("Database conection not fully defined"); + using NpgsqlConnection con = new(DatabaseHandler.ConectionString); + string command = $"SELECT {column.Name} FROM {Name} WHERE"; + con.Open(); + using NpgsqlCommand cmd = new(); + cmd.Connection = con; + string vals = ""; + foreach (Parameter param in Parameters) + { + vals += $"{param.PGParameter.ParameterName} {param.Sign} @{param.PGParameter.ParameterName} AND "; + cmd.Parameters.Add(param.PGParameter); + } + if (command.EndsWith(";")) command = command.Remove(command.Length - 1, 1); + if (!string.IsNullOrWhiteSpace(vals)) vals = vals.Remove(vals.Length - 5, 5); + if (!string.IsNullOrWhiteSpace(vals) && command.EndsWith("WHERE")) command += $" {vals}"; + if (command.EndsWith("WHERE")) command = command.Remove(command.Length - 6, 6); + if (order is not null) command += $" ORDER BY {order.Type} "; + if (order is not null && order.Assending) command += "ASC"; + else { if (order is not null) command += "DESC"; } + command += $" LIMIT {NumRows};"; + cmd.CommandText = command; + NpgsqlDataReader reader = cmd.ExecuteReader(); + List Rows = new(); + for (int j = 0; j < NumRows; j++) + { + if (!reader.Read()) break; + Rows.Add((TColumn)reader.GetValue(0)); + } + + return Rows.ToArray(); + } + + public TRow[] ReadRows(params Parameter[] Parameters) => ReadRows(uint.MaxValue, null, Parameters); public TRow[] ReadRows(uint NumRows, params Parameter[] Parameters) => ReadRows(NumRows, null, Parameters); - public TRow[] ReadRows(uint NumRows, Order order, params Parameter[] Parameters) + public TRow[] ReadRows(Order? Order, params Parameter[] Parameters) => ReadRows(uint.MaxValue, Order, Parameters); + + public TRow[] ReadRows(uint NumRows, Order? order, params Parameter[] Parameters) { if (string.IsNullOrEmpty(DatabaseHandler.DB) || string.IsNullOrEmpty(DatabaseHandler.IP) || string.IsNullOrEmpty(DatabaseHandler.Uname) || string.IsNullOrEmpty(DatabaseHandler.PW)) throw new Exception("Database conection not fully defined"); using NpgsqlConnection con = new(DatabaseHandler.ConectionString); @@ -510,6 +616,7 @@ public class Table : ITable where TRow : class, new() if (command.EndsWith(";")) command = command.Remove(command.Length - 1, 1); if (!string.IsNullOrWhiteSpace(vals)) vals = vals.Remove(vals.Length - 5, 5); if (!string.IsNullOrWhiteSpace(vals) && command.EndsWith("WHERE")) command += $" {vals}"; + if (command.EndsWith("WHERE")) command = command.Remove(command.Length - 6, 6); if (order is not null) command += $" ORDER BY {order.Type} "; if (order is not null && order.Assending) command += "ASC"; else { if (order is not null) command += "DESC"; } diff --git a/Database/TableColumn.cs b/Database/TableColumn.cs index 14f007f..83e82dc 100644 --- a/Database/TableColumn.cs +++ b/Database/TableColumn.cs @@ -14,6 +14,8 @@ public class TableColumn : ITableColumn where Ttype : notnull this.IsPrimaryKey = IsPrimaryKey; } + public IForeignKey[] ForeignKeys { get; init; } = Array.Empty(); + public long ColumnVersion { get; set; } = 0; public Order GetDecendingOrder() => new Order() { Assending = false, Type = Name }; public Order GetAssendingOrder() => new Order() { Assending = true, Type = Name };