/* TShock, a server mod for Terraria Copyright (C) 2011-2019 Pryaxis & TShock Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ using MySql.Data.MySqlClient; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.Text; using TShockAPI.Extensions; namespace TShockAPI.DB { /// /// Interface for various SQL related utilities. /// public interface IQueryBuilder { /// /// Creates a table from a SqlTable object. /// /// The SqlTable to create the table from /// The sql query for the table creation. string CreateTable(SqlTable table); /// /// Alter a table from source to destination /// /// Must have name and column names. Column types are not required /// Must have column names and column types. /// The SQL Query string AlterTable(SqlTable from, SqlTable to); /// /// Converts the MySqlDbType enum to it's string representation. /// /// The MySqlDbType type /// The length of the datatype /// The string representation string DbTypeToString(MySqlDbType type, int? length); /// /// A UPDATE Query /// /// The table to update /// The values to change /// /// The SQL query string UpdateValue(string table, List values, List wheres); /// /// A INSERT query /// /// The table to insert to /// /// The SQL Query string InsertValues(string table, List values); /// /// A SELECT query to get all columns /// /// The table to select from /// /// The SQL query string ReadColumn(string table, List wheres); /// /// Deletes row(s). /// /// The table to delete the row from /// /// The SQL query string DeleteRow(string table, List wheres); /// /// Renames the given table. /// /// Old name of the table /// New name of the table /// The sql query for renaming the table. string RenameTable(string from, string to); } /// /// Query Creator for Sqlite /// public class SqliteQueryCreator : GenericQueryCreator, IQueryBuilder { /// /// Creates a table from a SqlTable object. /// /// The SqlTable to create the table from /// The sql query for the table creation. public override string CreateTable(SqlTable table) { ValidateSqlColumnType(table.Columns); var columns = table.Columns.Select( c => "'{0}' {1} {2} {3} {4} {5}".SFormat(c.Name, DbTypeToString(c.Type, c.Length), c.Primary ? "PRIMARY KEY" : "", c.AutoIncrement ? "AUTOINCREMENT" : "", c.NotNull ? "NOT NULL" : "", c.DefaultCurrentTimestamp ? "DEFAULT CURRENT_TIMESTAMP" : "")); var uniques = table.Columns.Where(c => c.Unique).Select(c => c.Name); return "CREATE TABLE {0} ({1} {2})".SFormat(EscapeTableName(table.Name), string.Join(", ", columns), uniques.Count() > 0 ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : ""); } /// /// Renames the given table. /// /// Old name of the table /// New name of the table /// The sql query for renaming the table. public override string RenameTable(string from, string to) { return "ALTER TABLE {0} RENAME TO {1}".SFormat(from, to); } private static readonly Dictionary TypesAsStrings = new Dictionary { { MySqlDbType.VarChar, "TEXT" }, { MySqlDbType.String, "TEXT" }, { MySqlDbType.Text, "TEXT" }, { MySqlDbType.TinyText, "TEXT" }, { MySqlDbType.MediumText, "TEXT" }, { MySqlDbType.LongText, "TEXT" }, { MySqlDbType.Float, "REAL" }, { MySqlDbType.Double, "REAL" }, { MySqlDbType.Int32, "INTEGER" }, { MySqlDbType.Blob, "BLOB" }, { MySqlDbType.Int64, "BIGINT"}, { MySqlDbType.DateTime, "DATETIME"}, }; /// /// Converts the MySqlDbType enum to it's string representation. /// /// The MySqlDbType type /// The length of the datatype /// The string representation public string DbTypeToString(MySqlDbType type, int? length) { string ret; if (TypesAsStrings.TryGetValue(type, out ret)) return ret; throw new NotImplementedException(Enum.GetName(typeof(MySqlDbType), type)); } /// /// Escapes the table name /// /// The name of the table to be escaped /// protected override string EscapeTableName(string table) { return $"\'{table}\'"; } } /// /// Query Creator for MySQL /// public class MysqlQueryCreator : GenericQueryCreator, IQueryBuilder { /// /// Creates a table from a SqlTable object. /// /// The SqlTable to create the table from /// The sql query for the table creation. public override string CreateTable(SqlTable table) { ValidateSqlColumnType(table.Columns); var columns = table.Columns.Select( c => "`{0}` {1} {2} {3} {4} {5}".SFormat(c.Name, DbTypeToString(c.Type, c.Length), c.Primary ? "PRIMARY KEY" : "", c.AutoIncrement ? "AUTO_INCREMENT" : "", c.NotNull ? "NOT NULL" : "", c.DefaultCurrentTimestamp ? "DEFAULT CURRENT_TIMESTAMP" : "")); var uniques = table.Columns.Where(c => c.Unique).Select(c => $"`{c.Name}`"); return "CREATE TABLE {0} ({1} {2})".SFormat(EscapeTableName(table.Name), string.Join(", ", columns), uniques.Count() > 0 ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : ""); } /// /// Renames the given table. /// /// Old name of the table /// New name of the table /// The sql query for renaming the table. public override string RenameTable(string from, string to) { return "RENAME TABLE {0} TO {1}".SFormat(from, to); } private static readonly Dictionary TypesAsStrings = new Dictionary { { MySqlDbType.VarChar, "VARCHAR" }, { MySqlDbType.String, "CHAR" }, { MySqlDbType.Text, "TEXT" }, { MySqlDbType.TinyText, "TINYTEXT" }, { MySqlDbType.MediumText, "MEDIUMTEXT" }, { MySqlDbType.LongText, "LONGTEXT" }, { MySqlDbType.Float, "FLOAT" }, { MySqlDbType.Double, "DOUBLE" }, { MySqlDbType.Int32, "INT" }, { MySqlDbType.Int64, "BIGINT"}, { MySqlDbType.DateTime, "DATETIME"}, }; /// /// Converts the MySqlDbType enum to it's string representation. /// /// The MySqlDbType type /// The length of the datatype /// The string representation public string DbTypeToString(MySqlDbType type, int? length) { string ret; if (TypesAsStrings.TryGetValue(type, out ret)) return ret + (length != null ? "({0})".SFormat((int)length) : ""); throw new NotImplementedException(Enum.GetName(typeof(MySqlDbType), type)); } /// /// Escapes the table name /// /// The name of the table to be escaped /// protected override string EscapeTableName(string table) { return table.SFormat("`{0}`", table); } } /// /// A Generic Query Creator (abstract) /// public abstract class GenericQueryCreator { protected static Random rand = new Random(); /// /// Escapes the table name /// /// The name of the table to be escaped /// protected abstract string EscapeTableName(string table); /// /// Creates a table from a SqlTable object. /// /// The SqlTable to create the table from /// The sql query for the table creation. public abstract string CreateTable(SqlTable table); /// /// Renames the given table. /// /// Old name of the table /// New name of the table /// The sql query for renaming the table. public abstract string RenameTable(string from, string to); /// /// Alter a table from source to destination /// /// Must have name and column names. Column types are not required /// Must have column names and column types. /// The SQL Query public string AlterTable(SqlTable from, SqlTable to) { var rstr = rand.NextString(20); var escapedTable = EscapeTableName(from.Name); var tmpTable = EscapeTableName("{0}_{1}".SFormat(rstr, from.Name)); var alter = RenameTable(escapedTable, tmpTable); var create = CreateTable(to); // combine all columns in the 'from' variable excluding ones that aren't in the 'to' variable. // exclude the ones that aren't in 'to' variable because if the column is deleted, why try to import the data? var columns = string.Join(", ", from.Columns.Where(c => to.Columns.Any(c2 => c2.Name == c.Name)).Select(c => $"`{c.Name}`")); var insert = "INSERT INTO {0} ({1}) SELECT {1} FROM {2}".SFormat(escapedTable, columns, tmpTable); var drop = "DROP TABLE {0}".SFormat(tmpTable); return "{0}; {1}; {2}; {3};".SFormat(alter, create, insert, drop); } /// /// Check for errors in the columns. /// /// /// public void ValidateSqlColumnType(List columns) { columns.ForEach(x => { if (x.DefaultCurrentTimestamp && x.Type != MySqlDbType.DateTime) { throw new SqlColumnException("Can't set to true SqlColumn.DefaultCurrentTimestamp " + "when the MySqlDbType is not DateTime"); } }); } /// /// Deletes row(s). /// /// The table to delete the row from /// /// The SQL query public string DeleteRow(string table, List wheres) { return "DELETE FROM {0} {1}".SFormat(EscapeTableName(table), BuildWhere(wheres)); } /// /// A UPDATE Query /// /// The table to update /// The values to change /// /// The SQL query public string UpdateValue(string table, List values, List wheres) { if (0 == values.Count) throw new ArgumentException("No values supplied"); return "UPDATE {0} SET {1} {2}".SFormat(EscapeTableName(table), string.Join(", ", values.Select(v => v.Name + " = " + v.Value)), BuildWhere(wheres)); } /// /// A SELECT query to get all columns /// /// The table to select from /// /// The SQL query public string ReadColumn(string table, List wheres) { return "SELECT * FROM {0} {1}".SFormat(EscapeTableName(table), BuildWhere(wheres)); } /// /// A INSERT query /// /// The table to insert to /// /// The SQL Query public string InsertValues(string table, List values) { var sbnames = new StringBuilder(); var sbvalues = new StringBuilder(); int count = 0; foreach (SqlValue value in values) { sbnames.Append(value.Name); sbvalues.Append(value.Value.ToString()); if (count != values.Count - 1) { sbnames.Append(", "); sbvalues.Append(", "); } count++; } return "INSERT INTO {0} ({1}) VALUES ({2})".SFormat(EscapeTableName(table), sbnames, sbvalues); } /// /// Builds the SQL WHERE clause /// /// /// protected static string BuildWhere(List wheres) { if (0 == wheres.Count) return string.Empty; return "WHERE {0}".SFormat(string.Join(", ", wheres.Select(v => $"{v.Name}" + " = " + v.Value))); } } }