diff --git a/TShockAPI/Configuration/TShockConfig.cs b/TShockAPI/Configuration/TShockConfig.cs
index c91466fe..64fbf77c 100644
--- a/TShockAPI/Configuration/TShockConfig.cs
+++ b/TShockAPI/Configuration/TShockConfig.cs
@@ -529,7 +529,7 @@ namespace TShockAPI.Configuration
#region MySQL Settings
/// The type of database to use when storing data (either "sqlite" or "mysql").
- [Description("The type of database to use when storing data (either \"sqlite\" or \"mysql\").")]
+ [Description("The type of database to use when storing data (either \"sqlite\", \"mysql\" or \"postgres\").")]
public string StorageType = "sqlite";
/// The path of sqlite db.
@@ -552,6 +552,22 @@ namespace TShockAPI.Configuration
[Description("The password used when connecting to a MySQL database.")]
public string MySqlPassword = "";
+ ///The Postgres hostname and port to direct connections to.
+ [Description("The Postgres hostname and port to direct connections to.")]
+ public string PostgresHost = "";
+
+ /// The database name to connect to when using Postgres as the database type.
+ [Description("The database name to connect to when using Postgres as the database type.")]
+ public string PostgresDbName = "";
+
+ /// The username used when connecting to a Postgres database.
+ [Description("The username used when connecting to a Postgres database.")]
+ public string PostgresUsername = "";
+
+ /// The password used when connecting to a Postgres database.
+ [Description("The password used when connecting to a Postgres database.")]
+ public string PostgresPassword = "";
+
/// Whether or not to save logs to the SQL database instead of a text file.
[Description("Whether or not to save logs to the SQL database instead of a text file.\nDefault = false.")]
public bool UseSqlLogs = false;
diff --git a/TShockAPI/DB/BanManager.cs b/TShockAPI/DB/BanManager.cs
index 3e93a3b7..c56e3d0d 100644
--- a/TShockAPI/DB/BanManager.cs
+++ b/TShockAPI/DB/BanManager.cs
@@ -22,6 +22,7 @@ using System.Collections.Generic;
using System.Data;
using MySql.Data.MySqlClient;
using System.Collections.ObjectModel;
+using TShockAPI.DB.Queries;
namespace TShockAPI.DB
{
@@ -68,10 +69,9 @@ namespace TShockAPI.DB
new SqlColumn("Date", MySqlDbType.Int64),
new SqlColumn("Expiration", MySqlDbType.Int64)
);
- var creator = new SqlTableCreator(db,
- db.GetSqlType() == SqlType.Sqlite
- ? (IQueryBuilder)new SqliteQueryCreator()
- : new MysqlQueryCreator());
+
+ var creator = new SqlTableCreator(db, db.GetSqlQueryBuilder());
+
try
{
creator.EnsureTableStructure(table);
@@ -105,15 +105,12 @@ namespace TShockAPI.DB
///
public void TryConvertBans()
{
- int res;
- if (database.GetSqlType() == SqlType.Mysql)
+ int res = database.GetSqlType() switch
{
- res = database.QueryScalar("SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema = @0 and table_name = 'Bans'", TShock.Config.Settings.MySqlDbName);
- }
- else
- {
- res = database.QueryScalar("SELECT COUNT(name) FROM sqlite_master WHERE type='table' AND name = 'Bans'");
- }
+ SqlType.Mysql => database.QueryScalar("SELECT COUNT(table_name) FROM information_schema.tables WHERE table_schema = @0 and table_name = 'Bans'", TShock.Config.Settings.MySqlDbName),
+ SqlType.Sqlite => database.QueryScalar("SELECT COUNT(name) FROM sqlite_master WHERE type='table' AND name = 'Bans'"),
+ SqlType.Postgres => database.QueryScalar("SELECT COUNT(table_name) FROM information_schema.tables WHERE table_name = 'Bans'"),
+ };
if (res != 0)
{
@@ -300,16 +297,13 @@ namespace TShockAPI.DB
return new AddBanResult { Message = message };
}
- string query = "INSERT INTO PlayerBans (Identifier, Reason, BanningUser, Date, Expiration) VALUES (@0, @1, @2, @3, @4);";
-
- if (database.GetSqlType() == SqlType.Mysql)
+ string query = "INSERT INTO PlayerBans (Identifier, Reason, BanningUser, Date, Expiration) VALUES (@0, @1, @2, @3, @4)" + database.GetSqlType() switch
{
- query += "SELECT LAST_INSERT_ID();";
- }
- else
- {
- query += "SELECT CAST(last_insert_rowid() as INT);";
- }
+ SqlType.Mysql => /*lang=mysql*/"; SELECT LAST_INSERT_ID();",
+ SqlType.Sqlite => /*lang=sqlite*/"; SELECT last_insert_rowid();",
+ SqlType.Postgres => /*lang=postgresql*/"RETURNING \"Identifier\";",
+ _ => null
+ };
int ticketId = database.QueryScalar(query, args.Identifier, args.Reason, args.BanningUser, args.BanDateTime.Ticks, args.ExpirationDateTime.Ticks);
@@ -361,19 +355,18 @@ namespace TShockAPI.DB
return Bans[id];
}
- using (var reader = database.QueryReader("SELECT * FROM PlayerBans WHERE TicketNumber=@0", id))
- {
- if (reader.Read())
- {
- var ticketNumber = reader.Get("TicketNumber");
- var identifier = reader.Get("Identifier");
- var reason = reader.Get("Reason");
- var banningUser = reader.Get("BanningUser");
- var date = reader.Get("Date");
- var expiration = reader.Get("Expiration");
+ using var reader = database.QueryReader("SELECT * FROM PlayerBans WHERE TicketNumber=@0", id);
- return new Ban(ticketNumber, identifier, reason, banningUser, date, expiration);
- }
+ if (reader.Read())
+ {
+ var ticketNumber = reader.Get("TicketNumber");
+ var identifier = reader.Get("Identifier");
+ var reason = reader.Get("Reason");
+ var banningUser = reader.Get("BanningUser");
+ var date = reader.Get("Date");
+ var expiration = reader.Get("Expiration");
+
+ return new Ban(ticketNumber, identifier, reason, banningUser, date, expiration);
}
return null;
@@ -393,19 +386,18 @@ namespace TShockAPI.DB
query += $" AND Expiration > {DateTime.UtcNow.Ticks}";
}
- using (var reader = database.QueryReader(query, identifier))
- {
- while (reader.Read())
- {
- var ticketNumber = reader.Get("TicketNumber");
- var ident = reader.Get("Identifier");
- var reason = reader.Get("Reason");
- var banningUser = reader.Get("BanningUser");
- var date = reader.Get("Date");
- var expiration = reader.Get("Expiration");
+ using var reader = database.QueryReader(query, identifier);
- yield return new Ban(ticketNumber, ident, reason, banningUser, date, expiration);
- }
+ while (reader.Read())
+ {
+ var ticketNumber = reader.Get("TicketNumber");
+ var ident = reader.Get("Identifier");
+ var reason = reader.Get("Reason");
+ var banningUser = reader.Get("BanningUser");
+ var date = reader.Get("Date");
+ var expiration = reader.Get("Expiration");
+
+ yield return new Ban(ticketNumber, ident, reason, banningUser, date, expiration);
}
}
@@ -418,27 +410,27 @@ namespace TShockAPI.DB
public IEnumerable GetBansByIdentifiers(bool currentOnly = true, params string[] identifiers)
{
//Generate a sequence of '@0, @1, @2, ... etc'
- var parameters = string.Join(", ", Enumerable.Range(0, identifiers.Count()).Select(p => $"@{p}"));
+ var parameters = string.Join(", ", Enumerable.Range(0, identifiers.Length).Select(p => $"@{p}"));
string query = $"SELECT * FROM PlayerBans WHERE Identifier IN ({parameters})";
+
if (currentOnly)
{
query += $" AND Expiration > {DateTime.UtcNow.Ticks}";
}
- using (var reader = database.QueryReader(query, identifiers))
- {
- while (reader.Read())
- {
- var ticketNumber = reader.Get("TicketNumber");
- var identifier = reader.Get("Identifier");
- var reason = reader.Get("Reason");
- var banningUser = reader.Get("BanningUser");
- var date = reader.Get("Date");
- var expiration = reader.Get("Expiration");
+ using var reader = database.QueryReader(query, identifiers);
- yield return new Ban(ticketNumber, identifier, reason, banningUser, date, expiration);
- }
+ while (reader.Read())
+ {
+ var ticketNumber = reader.Get("TicketNumber");
+ var identifier = reader.Get("Identifier");
+ var reason = reader.Get("Reason");
+ var banningUser = reader.Get("BanningUser");
+ var date = reader.Get("Date");
+ var expiration = reader.Get("Expiration");
+
+ yield return new Ban(ticketNumber, identifier, reason, banningUser, date, expiration);
}
}
@@ -457,21 +449,19 @@ namespace TShockAPI.DB
List banlist = new List();
try
{
- var orderBy = SortToOrderByMap[sortMethod];
- using (var reader = database.QueryReader($"SELECT * FROM PlayerBans ORDER BY {orderBy}"))
- {
- while (reader.Read())
- {
- var ticketNumber = reader.Get("TicketNumber");
- var identifier = reader.Get("Identifier");
- var reason = reader.Get("Reason");
- var banningUser = reader.Get("BanningUser");
- var date = reader.Get("Date");
- var expiration = reader.Get("Expiration");
+ using var reader = database.QueryReader($"SELECT * FROM PlayerBans ORDER BY {SortToOrderByMap[sortMethod]}");
- var ban = new Ban(ticketNumber, identifier, reason, banningUser, date, expiration);
- banlist.Add(ban);
- }
+ while (reader.Read())
+ {
+ var ticketNumber = reader.Get("TicketNumber");
+ var identifier = reader.Get("Identifier");
+ var reason = reader.Get("Reason");
+ var banningUser = reader.Get("BanningUser");
+ var date = reader.Get("Date");
+ var expiration = reader.Get("Expiration");
+
+ var ban = new Ban(ticketNumber, identifier, reason, banningUser, date, expiration);
+ banlist.Add(ban);
}
}
catch (Exception ex)
@@ -500,7 +490,7 @@ namespace TShockAPI.DB
return false;
}
- internal Dictionary SortToOrderByMap = new Dictionary
+ private readonly Dictionary SortToOrderByMap = new()
{
{ BanSortMethod.AddedNewestToOldest, "Date DESC" },
{ BanSortMethod.AddedOldestToNewest, "Date ASC" },
diff --git a/TShockAPI/DB/CharacterManager.cs b/TShockAPI/DB/CharacterManager.cs
index c0551b65..3b9890a3 100644
--- a/TShockAPI/DB/CharacterManager.cs
+++ b/TShockAPI/DB/CharacterManager.cs
@@ -23,6 +23,7 @@ using System.Linq;
using System.Text;
using MySql.Data.MySqlClient;
using Terraria;
+using TShockAPI.DB.Queries;
namespace TShockAPI.DB
{
@@ -70,10 +71,8 @@ namespace TShockAPI.DB
new SqlColumn("unlockedSuperCart", MySqlDbType.Int32),
new SqlColumn("enabledSuperCart", MySqlDbType.Int32)
);
- var creator = new SqlTableCreator(db,
- db.GetSqlType() == SqlType.Sqlite
- ? (IQueryBuilder) new SqliteQueryCreator()
- : new MysqlQueryCreator());
+
+ SqlTableCreator creator = new(db, db.GetSqlQueryBuilder());
creator.EnsureTableStructure(table);
}
@@ -83,59 +82,57 @@ namespace TShockAPI.DB
try
{
- using (var reader = database.QueryReader("SELECT * FROM tsCharacter WHERE Account=@0", acctid))
+ using var reader = database.QueryReader("SELECT * FROM tsCharacter WHERE Account=@0", acctid);
+ if (reader.Read())
{
- if (reader.Read())
+ playerData.exists = true;
+ playerData.health = reader.Get("Health");
+ playerData.maxHealth = reader.Get("MaxHealth");
+ playerData.mana = reader.Get("Mana");
+ playerData.maxMana = reader.Get("MaxMana");
+ List inventory = reader.Get("Inventory").Split('~').Select(NetItem.Parse).ToList();
+ if (inventory.Count < NetItem.MaxInventory)
{
- playerData.exists = true;
- playerData.health = reader.Get("Health");
- playerData.maxHealth = reader.Get("MaxHealth");
- playerData.mana = reader.Get("Mana");
- playerData.maxMana = reader.Get("MaxMana");
- List inventory = reader.Get("Inventory").Split('~').Select(NetItem.Parse).ToList();
- if (inventory.Count < NetItem.MaxInventory)
- {
- //TODO: unhardcode this - stop using magic numbers and use NetItem numbers
- //Set new armour slots empty
- inventory.InsertRange(67, new NetItem[2]);
- //Set new vanity slots empty
- inventory.InsertRange(77, new NetItem[2]);
- //Set new dye slots empty
- inventory.InsertRange(87, new NetItem[2]);
- //Set the rest of the new slots empty
- inventory.AddRange(new NetItem[NetItem.MaxInventory - inventory.Count]);
- }
- playerData.inventory = inventory.ToArray();
- playerData.extraSlot = reader.Get("extraSlot");
- playerData.spawnX = reader.Get("spawnX");
- playerData.spawnY = reader.Get("spawnY");
- playerData.skinVariant = reader.Get("skinVariant");
- playerData.hair = reader.Get("hair");
- playerData.hairDye = (byte)reader.Get("hairDye");
- playerData.hairColor = TShock.Utils.DecodeColor(reader.Get("hairColor"));
- playerData.pantsColor = TShock.Utils.DecodeColor(reader.Get("pantsColor"));
- playerData.shirtColor = TShock.Utils.DecodeColor(reader.Get("shirtColor"));
- playerData.underShirtColor = TShock.Utils.DecodeColor(reader.Get("underShirtColor"));
- playerData.shoeColor = TShock.Utils.DecodeColor(reader.Get("shoeColor"));
- playerData.hideVisuals = TShock.Utils.DecodeBoolArray(reader.Get("hideVisuals"));
- playerData.skinColor = TShock.Utils.DecodeColor(reader.Get("skinColor"));
- playerData.eyeColor = TShock.Utils.DecodeColor(reader.Get("eyeColor"));
- playerData.questsCompleted = reader.Get("questsCompleted");
- playerData.usingBiomeTorches = reader.Get("usingBiomeTorches");
- playerData.happyFunTorchTime = reader.Get("happyFunTorchTime");
- playerData.unlockedBiomeTorches = reader.Get("unlockedBiomeTorches");
- playerData.currentLoadoutIndex = reader.Get("currentLoadoutIndex");
- playerData.ateArtisanBread = reader.Get("ateArtisanBread");
- playerData.usedAegisCrystal = reader.Get("usedAegisCrystal");
- playerData.usedAegisFruit = reader.Get("usedAegisFruit");
- playerData.usedArcaneCrystal = reader.Get("usedArcaneCrystal");
- playerData.usedGalaxyPearl = reader.Get("usedGalaxyPearl");
- playerData.usedGummyWorm = reader.Get("usedGummyWorm");
- playerData.usedAmbrosia = reader.Get("usedAmbrosia");
- playerData.unlockedSuperCart = reader.Get("unlockedSuperCart");
- playerData.enabledSuperCart = reader.Get("enabledSuperCart");
- return playerData;
+ //TODO: unhardcode this - stop using magic numbers and use NetItem numbers
+ //Set new armour slots empty
+ inventory.InsertRange(67, new NetItem[2]);
+ //Set new vanity slots empty
+ inventory.InsertRange(77, new NetItem[2]);
+ //Set new dye slots empty
+ inventory.InsertRange(87, new NetItem[2]);
+ //Set the rest of the new slots empty
+ inventory.AddRange(new NetItem[NetItem.MaxInventory - inventory.Count]);
}
+ playerData.inventory = inventory.ToArray();
+ playerData.extraSlot = reader.Get("extraSlot");
+ playerData.spawnX = reader.Get("spawnX");
+ playerData.spawnY = reader.Get("spawnY");
+ playerData.skinVariant = reader.Get("skinVariant");
+ playerData.hair = reader.Get("hair");
+ playerData.hairDye = (byte)reader.Get("hairDye");
+ playerData.hairColor = TShock.Utils.DecodeColor(reader.Get("hairColor"));
+ playerData.pantsColor = TShock.Utils.DecodeColor(reader.Get("pantsColor"));
+ playerData.shirtColor = TShock.Utils.DecodeColor(reader.Get("shirtColor"));
+ playerData.underShirtColor = TShock.Utils.DecodeColor(reader.Get("underShirtColor"));
+ playerData.shoeColor = TShock.Utils.DecodeColor(reader.Get("shoeColor"));
+ playerData.hideVisuals = TShock.Utils.DecodeBoolArray(reader.Get("hideVisuals"));
+ playerData.skinColor = TShock.Utils.DecodeColor(reader.Get("skinColor"));
+ playerData.eyeColor = TShock.Utils.DecodeColor(reader.Get("eyeColor"));
+ playerData.questsCompleted = reader.Get("questsCompleted");
+ playerData.usingBiomeTorches = reader.Get("usingBiomeTorches");
+ playerData.happyFunTorchTime = reader.Get("happyFunTorchTime");
+ playerData.unlockedBiomeTorches = reader.Get("unlockedBiomeTorches");
+ playerData.currentLoadoutIndex = reader.Get("currentLoadoutIndex");
+ playerData.ateArtisanBread = reader.Get("ateArtisanBread");
+ playerData.usedAegisCrystal = reader.Get("usedAegisCrystal");
+ playerData.usedAegisFruit = reader.Get("usedAegisFruit");
+ playerData.usedArcaneCrystal = reader.Get("usedArcaneCrystal");
+ playerData.usedGalaxyPearl = reader.Get("usedGalaxyPearl");
+ playerData.usedGummyWorm = reader.Get("usedGummyWorm");
+ playerData.usedAmbrosia = reader.Get("usedAmbrosia");
+ playerData.unlockedSuperCart = reader.Get("unlockedSuperCart");
+ playerData.enabledSuperCart = reader.Get("enabledSuperCart");
+ return playerData;
}
}
catch (Exception ex)
@@ -154,7 +151,7 @@ namespace TShockAPI.DB
if (items.Count < NetItem.MaxInventory)
items.AddRange(new NetItem[NetItem.MaxInventory - items.Count]);
- string initialItems = String.Join("~", items.Take(NetItem.MaxInventory));
+ string initialItems = string.Join("~", items.Take(NetItem.MaxInventory));
try
{
database.Query("INSERT INTO tsCharacter (Account, Health, MaxHealth, Mana, MaxMana, Inventory, spawnX, spawnY, questsCompleted) VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8);",
@@ -204,7 +201,7 @@ namespace TShockAPI.DB
{
database.Query(
"INSERT INTO tsCharacter (Account, Health, MaxHealth, Mana, MaxMana, Inventory, extraSlot, spawnX, spawnY, skinVariant, hair, hairDye, hairColor, pantsColor, shirtColor, underShirtColor, shoeColor, hideVisuals, skinColor, eyeColor, questsCompleted, usingBiomeTorches, happyFunTorchTime, unlockedBiomeTorches, currentLoadoutIndex,ateArtisanBread, usedAegisCrystal, usedAegisFruit, usedArcaneCrystal, usedGalaxyPearl, usedGummyWorm, usedAmbrosia, unlockedSuperCart, enabledSuperCart) VALUES (@0, @1, @2, @3, @4, @5, @6, @7, @8, @9, @10, @11, @12, @13, @14, @15, @16, @17, @18, @19, @20, @21, @22, @23, @24, @25, @26, @27, @28, @29, @30, @31, @32, @33);",
- player.Account.ID, playerData.health, playerData.maxHealth, playerData.mana, playerData.maxMana, String.Join("~", playerData.inventory), playerData.extraSlot, player.TPlayer.SpawnX, player.TPlayer.SpawnY, player.TPlayer.skinVariant, player.TPlayer.hair, player.TPlayer.hairDye, TShock.Utils.EncodeColor(player.TPlayer.hairColor), TShock.Utils.EncodeColor(player.TPlayer.pantsColor),TShock.Utils.EncodeColor(player.TPlayer.shirtColor), TShock.Utils.EncodeColor(player.TPlayer.underShirtColor), TShock.Utils.EncodeColor(player.TPlayer.shoeColor), TShock.Utils.EncodeBoolArray(player.TPlayer.hideVisibleAccessory), TShock.Utils.EncodeColor(player.TPlayer.skinColor),TShock.Utils.EncodeColor(player.TPlayer.eyeColor), player.TPlayer.anglerQuestsFinished, player.TPlayer.UsingBiomeTorches ? 1 : 0, player.TPlayer.happyFunTorchTime ? 1 : 0, player.TPlayer.unlockedBiomeTorches ? 1 : 0, player.TPlayer.CurrentLoadoutIndex, player.TPlayer.ateArtisanBread ? 1 : 0, player.TPlayer.usedAegisCrystal ? 1 : 0, player.TPlayer.usedAegisFruit ? 1 : 0, player.TPlayer.usedArcaneCrystal ? 1 : 0, player.TPlayer.usedGalaxyPearl ? 1 : 0, player.TPlayer.usedGummyWorm ? 1 : 0, player.TPlayer.usedAmbrosia ? 1 : 0, player.TPlayer.unlockedSuperCart ? 1 : 0, player.TPlayer.enabledSuperCart ? 1 : 0);
+ player.Account.ID, playerData.health, playerData.maxHealth, playerData.mana, playerData.maxMana, string.Join("~", playerData.inventory), playerData.extraSlot, player.TPlayer.SpawnX, player.TPlayer.SpawnY, player.TPlayer.skinVariant, player.TPlayer.hair, player.TPlayer.hairDye, TShock.Utils.EncodeColor(player.TPlayer.hairColor), TShock.Utils.EncodeColor(player.TPlayer.pantsColor),TShock.Utils.EncodeColor(player.TPlayer.shirtColor), TShock.Utils.EncodeColor(player.TPlayer.underShirtColor), TShock.Utils.EncodeColor(player.TPlayer.shoeColor), TShock.Utils.EncodeBoolArray(player.TPlayer.hideVisibleAccessory), TShock.Utils.EncodeColor(player.TPlayer.skinColor),TShock.Utils.EncodeColor(player.TPlayer.eyeColor), player.TPlayer.anglerQuestsFinished, player.TPlayer.UsingBiomeTorches ? 1 : 0, player.TPlayer.happyFunTorchTime ? 1 : 0, player.TPlayer.unlockedBiomeTorches ? 1 : 0, player.TPlayer.CurrentLoadoutIndex, player.TPlayer.ateArtisanBread ? 1 : 0, player.TPlayer.usedAegisCrystal ? 1 : 0, player.TPlayer.usedAegisFruit ? 1 : 0, player.TPlayer.usedArcaneCrystal ? 1 : 0, player.TPlayer.usedGalaxyPearl ? 1 : 0, player.TPlayer.usedGummyWorm ? 1 : 0, player.TPlayer.usedAmbrosia ? 1 : 0, player.TPlayer.unlockedSuperCart ? 1 : 0, player.TPlayer.enabledSuperCart ? 1 : 0);
return true;
}
catch (Exception ex)
@@ -218,7 +215,7 @@ namespace TShockAPI.DB
{
database.Query(
"UPDATE tsCharacter SET Health = @0, MaxHealth = @1, Mana = @2, MaxMana = @3, Inventory = @4, spawnX = @6, spawnY = @7, hair = @8, hairDye = @9, hairColor = @10, pantsColor = @11, shirtColor = @12, underShirtColor = @13, shoeColor = @14, hideVisuals = @15, skinColor = @16, eyeColor = @17, questsCompleted = @18, skinVariant = @19, extraSlot = @20, usingBiomeTorches = @21, happyFunTorchTime = @22, unlockedBiomeTorches = @23, currentLoadoutIndex = @24, ateArtisanBread = @25, usedAegisCrystal = @26, usedAegisFruit = @27, usedArcaneCrystal = @28, usedGalaxyPearl = @29, usedGummyWorm = @30, usedAmbrosia = @31, unlockedSuperCart = @32, enabledSuperCart = @33 WHERE Account = @5;",
- playerData.health, playerData.maxHealth, playerData.mana, playerData.maxMana, String.Join("~", playerData.inventory), player.Account.ID, player.TPlayer.SpawnX, player.TPlayer.SpawnY, player.TPlayer.hair, player.TPlayer.hairDye, TShock.Utils.EncodeColor(player.TPlayer.hairColor), TShock.Utils.EncodeColor(player.TPlayer.pantsColor), TShock.Utils.EncodeColor(player.TPlayer.shirtColor), TShock.Utils.EncodeColor(player.TPlayer.underShirtColor), TShock.Utils.EncodeColor(player.TPlayer.shoeColor), TShock.Utils.EncodeBoolArray(player.TPlayer.hideVisibleAccessory), TShock.Utils.EncodeColor(player.TPlayer.skinColor), TShock.Utils.EncodeColor(player.TPlayer.eyeColor), player.TPlayer.anglerQuestsFinished, player.TPlayer.skinVariant, player.TPlayer.extraAccessory ? 1 : 0, player.TPlayer.UsingBiomeTorches ? 1 : 0, player.TPlayer.happyFunTorchTime ? 1 : 0, player.TPlayer.unlockedBiomeTorches ? 1 : 0, player.TPlayer.CurrentLoadoutIndex, player.TPlayer.ateArtisanBread ? 1 : 0, player.TPlayer.usedAegisCrystal ? 1 : 0, player.TPlayer.usedAegisFruit ? 1 : 0, player.TPlayer.usedArcaneCrystal ? 1 : 0, player.TPlayer.usedGalaxyPearl ? 1 : 0, player.TPlayer.usedGummyWorm ? 1 : 0, player.TPlayer.usedAmbrosia ? 1 : 0, player.TPlayer.unlockedSuperCart ? 1 : 0, player.TPlayer.enabledSuperCart ? 1 : 0);
+ playerData.health, playerData.maxHealth, playerData.mana, playerData.maxMana, string.Join("~", playerData.inventory), player.Account.ID, player.TPlayer.SpawnX, player.TPlayer.SpawnY, player.TPlayer.hair, player.TPlayer.hairDye, TShock.Utils.EncodeColor(player.TPlayer.hairColor), TShock.Utils.EncodeColor(player.TPlayer.pantsColor), TShock.Utils.EncodeColor(player.TPlayer.shirtColor), TShock.Utils.EncodeColor(player.TPlayer.underShirtColor), TShock.Utils.EncodeColor(player.TPlayer.shoeColor), TShock.Utils.EncodeBoolArray(player.TPlayer.hideVisibleAccessory), TShock.Utils.EncodeColor(player.TPlayer.skinColor), TShock.Utils.EncodeColor(player.TPlayer.eyeColor), player.TPlayer.anglerQuestsFinished, player.TPlayer.skinVariant, player.TPlayer.extraAccessory ? 1 : 0, player.TPlayer.UsingBiomeTorches ? 1 : 0, player.TPlayer.happyFunTorchTime ? 1 : 0, player.TPlayer.unlockedBiomeTorches ? 1 : 0, player.TPlayer.CurrentLoadoutIndex, player.TPlayer.ateArtisanBread ? 1 : 0, player.TPlayer.usedAegisCrystal ? 1 : 0, player.TPlayer.usedAegisFruit ? 1 : 0, player.TPlayer.usedArcaneCrystal ? 1 : 0, player.TPlayer.usedGalaxyPearl ? 1 : 0, player.TPlayer.usedGummyWorm ? 1 : 0, player.TPlayer.usedAmbrosia ? 1 : 0, player.TPlayer.unlockedSuperCart ? 1 : 0, player.TPlayer.enabledSuperCart ? 1 : 0);
return true;
}
catch (Exception ex)
@@ -279,7 +276,7 @@ namespace TShockAPI.DB
playerData.maxHealth,
playerData.mana,
playerData.maxMana,
- String.Join("~", playerData.inventory),
+ string.Join("~", playerData.inventory),
playerData.extraSlot,
playerData.spawnX,
playerData.spawnX,
@@ -325,7 +322,7 @@ namespace TShockAPI.DB
playerData.maxHealth,
playerData.mana,
playerData.maxMana,
- String.Join("~", playerData.inventory),
+ string.Join("~", playerData.inventory),
player.Account.ID,
playerData.spawnX,
playerData.spawnX,
diff --git a/TShockAPI/DB/DbBuilder.cs b/TShockAPI/DB/DbBuilder.cs
new file mode 100644
index 00000000..dc6e20df
--- /dev/null
+++ b/TShockAPI/DB/DbBuilder.cs
@@ -0,0 +1,115 @@
+using System.Data;
+using System.Diagnostics;
+using System.IO;
+using Microsoft.Data.Sqlite;
+using MySql.Data.MySqlClient;
+using Npgsql;
+using TerrariaApi.Server;
+using TShockAPI.Configuration;
+
+namespace TShockAPI.DB;
+
+///
+/// Provides logic to build a DB connection.
+///
+public sealed class DbBuilder
+{
+ private readonly TShock _caller;
+ private readonly TShockConfig _config;
+ private readonly string _savePath;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The TShock instance calling this DbBuilder.
+ /// The TShock configuration, supplied by at init.
+ /// The savePath registered by TShock. See .
+ public DbBuilder(TShock caller, TShockConfig config, string savePath)
+ {
+ _caller = caller;
+ _config = config;
+ _savePath = savePath;
+ }
+
+ ///
+ /// Builds a DB connection based on the provided configuration.
+ ///
+ /// The TShock configuration.
+ ///
+ /// Default settings will result in a local sqlite database file named "tshock.db" in the current directory to be used as server DB.
+ ///
+ public IDbConnection BuildDbConnection()
+ {
+ string dbType = _config.Settings.StorageType.ToLowerInvariant();
+
+ return dbType switch
+ {
+ "sqlite" => BuildSqliteConnection(),
+ "mysql" => BuildMySqlConnection(),
+ "postgres" => BuildPostgresConnection(),
+ _ => throw new("Invalid storage type")
+ };
+ }
+
+ private SqliteConnection BuildSqliteConnection()
+ {
+ string dbFilePath = Path.Combine(_savePath, _config.Settings.SqliteDBPath);
+
+ if (Path.GetDirectoryName(dbFilePath) is not { } dbDirPath)
+ {
+ throw new DirectoryNotFoundException($"The SQLite database path '{dbFilePath}' could not be found.");
+ }
+
+ Directory.CreateDirectory(dbDirPath);
+
+ return new($"Data Source={dbFilePath}");
+ }
+
+ private MySqlConnection BuildMySqlConnection()
+ {
+ try
+ {
+ string[] hostport = _config.Settings.MySqlHost.Split(':');
+
+ MySqlConnectionStringBuilder connStrBuilder = new()
+ {
+ Server = hostport[0],
+ Port = hostport.Length > 1 ? uint.Parse(hostport[1]) : 3306,
+ Database = _config.Settings.MySqlDbName,
+ UserID = _config.Settings.MySqlUsername,
+ Password = _config.Settings.MySqlPassword
+ };
+
+ return new(connStrBuilder.ToString());
+ }
+ catch (MySqlException e)
+ {
+ ServerApi.LogWriter.PluginWriteLine(_caller, e.ToString(), TraceLevel.Error);
+ throw new("MySql not setup correctly", e);
+ }
+ }
+
+ private NpgsqlConnection BuildPostgresConnection()
+ {
+ try
+ {
+ string[] hostport = _config.Settings.PostgresHost.Split(':');
+
+ NpgsqlConnectionStringBuilder connStrBuilder = new()
+ {
+ Host = hostport[0],
+ Port = hostport.Length > 1 ? int.Parse(hostport[1]) : 5432,
+ Database = _config.Settings.PostgresDbName,
+ Username = _config.Settings.PostgresUsername,
+ Password = _config.Settings.PostgresPassword
+ };
+
+ return new(connStrBuilder.ToString());
+ }
+ catch (NpgsqlException e)
+ {
+ ServerApi.LogWriter.PluginWriteLine(_caller, e.ToString(), TraceLevel.Error);
+ throw new("Postgres not setup correctly", e);
+ }
+ }
+}
diff --git a/TShockAPI/DB/GroupManager.cs b/TShockAPI/DB/GroupManager.cs
index 44f0c994..351399bf 100644
--- a/TShockAPI/DB/GroupManager.cs
+++ b/TShockAPI/DB/GroupManager.cs
@@ -23,6 +23,7 @@ using System.Data;
using System.Diagnostics;
using System.Linq;
using MySql.Data.MySqlClient;
+using TShockAPI.DB.Queries;
namespace TShockAPI.DB
{
@@ -50,10 +51,9 @@ namespace TShockAPI.DB
new SqlColumn("Prefix", MySqlDbType.Text),
new SqlColumn("Suffix", MySqlDbType.Text)
);
- var creator = new SqlTableCreator(db,
- db.GetSqlType() == SqlType.Sqlite
- ? (IQueryBuilder)new SqliteQueryCreator()
- : new MysqlQueryCreator());
+
+ SqlTableCreator creator = new(db, db.GetSqlQueryBuilder());
+
if (creator.EnsureTableStructure(table))
{
// Add default groups if they don't exist
@@ -294,7 +294,7 @@ namespace TShockAPI.DB
/// parent of group
/// permissions
/// chatcolor
- public void AddGroup(String name, string parentname, String permissions, String chatcolor)
+ public void AddGroup(string name, string parentname, string permissions, string chatcolor)
{
if (GroupExists(name))
{
@@ -315,15 +315,22 @@ namespace TShockAPI.DB
group.Parent = parent;
}
- string query = (TShock.Config.Settings.StorageType.ToLower() == "sqlite")
- ? "INSERT OR IGNORE INTO GroupList (GroupName, Parent, Commands, ChatColor) VALUES (@0, @1, @2, @3);"
- : "INSERT IGNORE INTO GroupList SET GroupName=@0, Parent=@1, Commands=@2, ChatColor=@3";
- if (database.Query(query, name, parentname, permissions, chatcolor) == 1)
+ string query = database.GetSqlType() switch
+ {
+ SqlType.Sqlite => "INSERT OR IGNORE INTO GroupList (GroupName, Parent, Commands, ChatColor) VALUES (@0, @1, @2, @3);",
+ SqlType.Mysql => "INSERT IGNORE INTO GroupList SET GroupName=@0, Parent=@1, Commands=@2, ChatColor=@3",
+ SqlType.Postgres => "INSERT INTO GroupList (GroupName, Parent, Commands, ChatColor) VALUES (@0, @1, @2, @3) ON CONFLICT (GroupName) DO NOTHING",
+ _ => throw new NotSupportedException(GetString("Unsupported database type."))
+ };
+
+ if (database.Query(query, name, parentname, permissions, chatcolor) is 1)
{
groups.Add(group);
}
else
+ {
throw new GroupManagerException(GetString($"Failed to add group {name}."));
+ }
}
///
@@ -383,7 +390,7 @@ namespace TShockAPI.DB
/// The group's name.
/// The new name.
/// The result from the operation to be sent back to the user.
- public String RenameGroup(string name, string newName)
+ public string RenameGroup(string name, string newName)
{
if (!GroupExists(name))
{
@@ -395,87 +402,83 @@ namespace TShockAPI.DB
throw new GroupExistsException(newName);
}
- using (var db = database.CloneEx())
+ using var db = database.CloneEx();
+ db.Open();
+ using var transaction = db.BeginTransaction();
+ try
{
- db.Open();
- using (var transaction = db.BeginTransaction())
+ using (var command = db.CreateCommand())
{
- try
- {
- using (var command = db.CreateCommand())
- {
- command.CommandText = "UPDATE GroupList SET GroupName = @0 WHERE GroupName = @1";
- command.AddParameter("@0", newName);
- command.AddParameter("@1", name);
- command.ExecuteNonQuery();
- }
+ command.CommandText = "UPDATE GroupList SET GroupName = @0 WHERE GroupName = @1";
+ command.AddParameter("@0", newName);
+ command.AddParameter("@1", name);
+ command.ExecuteNonQuery();
+ }
- var oldGroup = GetGroupByName(name);
- var newGroup = new Group(newName, oldGroup.Parent, oldGroup.ChatColor, oldGroup.Permissions)
- {
- Prefix = oldGroup.Prefix,
- Suffix = oldGroup.Suffix
- };
- groups.Remove(oldGroup);
- groups.Add(newGroup);
+ var oldGroup = GetGroupByName(name);
+ var newGroup = new Group(newName, oldGroup.Parent, oldGroup.ChatColor, oldGroup.Permissions)
+ {
+ Prefix = oldGroup.Prefix,
+ Suffix = oldGroup.Suffix
+ };
+ groups.Remove(oldGroup);
+ groups.Add(newGroup);
- // We need to check if the old group has been referenced as a parent and update those references accordingly
- using (var command = db.CreateCommand())
- {
- command.CommandText = "UPDATE GroupList SET Parent = @0 WHERE Parent = @1";
- command.AddParameter("@0", newName);
- command.AddParameter("@1", name);
- command.ExecuteNonQuery();
- }
- foreach (var group in groups.Where(g => g.Parent != null && g.Parent == oldGroup))
- {
- group.Parent = newGroup;
- }
+ // We need to check if the old group has been referenced as a parent and update those references accordingly
+ using (var command = db.CreateCommand())
+ {
+ command.CommandText = "UPDATE GroupList SET Parent = @0 WHERE Parent = @1";
+ command.AddParameter("@0", newName);
+ command.AddParameter("@1", name);
+ command.ExecuteNonQuery();
+ }
+ foreach (var group in groups.Where(g => g.Parent != null && g.Parent == oldGroup))
+ {
+ group.Parent = newGroup;
+ }
- // Read the config file to prevent the possible loss of any unsaved changes
- TShock.Config.Read(FileTools.ConfigPath, out bool writeConfig);
- if (TShock.Config.Settings.DefaultGuestGroupName == oldGroup.Name)
- {
- TShock.Config.Settings.DefaultGuestGroupName = newGroup.Name;
- Group.DefaultGroup = newGroup;
- }
- if (TShock.Config.Settings.DefaultRegistrationGroupName == oldGroup.Name)
- {
- TShock.Config.Settings.DefaultRegistrationGroupName = newGroup.Name;
- }
- if (writeConfig)
- {
- TShock.Config.Write(FileTools.ConfigPath);
- }
+ // Read the config file to prevent the possible loss of any unsaved changes
+ TShock.Config.Read(FileTools.ConfigPath, out bool writeConfig);
+ if (TShock.Config.Settings.DefaultGuestGroupName == oldGroup.Name)
+ {
+ TShock.Config.Settings.DefaultGuestGroupName = newGroup.Name;
+ Group.DefaultGroup = newGroup;
+ }
+ if (TShock.Config.Settings.DefaultRegistrationGroupName == oldGroup.Name)
+ {
+ TShock.Config.Settings.DefaultRegistrationGroupName = newGroup.Name;
+ }
+ if (writeConfig)
+ {
+ TShock.Config.Write(FileTools.ConfigPath);
+ }
- // We also need to check if any users belong to the old group and automatically apply changes
- using (var command = db.CreateCommand())
- {
- command.CommandText = "UPDATE Users SET Usergroup = @0 WHERE Usergroup = @1";
- command.AddParameter("@0", newName);
- command.AddParameter("@1", name);
- command.ExecuteNonQuery();
- }
- foreach (var player in TShock.Players.Where(p => p?.Group == oldGroup))
- {
- player.Group = newGroup;
- }
+ // We also need to check if any users belong to the old group and automatically apply changes
+ using (var command = db.CreateCommand())
+ {
+ command.CommandText = "UPDATE Users SET Usergroup = @0 WHERE Usergroup = @1";
+ command.AddParameter("@0", newName);
+ command.AddParameter("@1", name);
+ command.ExecuteNonQuery();
+ }
+ foreach (var player in TShock.Players.Where(p => p?.Group == oldGroup))
+ {
+ player.Group = newGroup;
+ }
- transaction.Commit();
- return GetString($"Group {name} has been renamed to {newName}.");
- }
- catch (Exception ex)
- {
- TShock.Log.Error(GetString($"An exception has occurred during database transaction: {ex.Message}"));
- try
- {
- transaction.Rollback();
- }
- catch (Exception rollbackEx)
- {
- TShock.Log.Error(GetString($"An exception has occurred during database rollback: {rollbackEx.Message}"));
- }
- }
+ transaction.Commit();
+ return GetString($"Group {name} has been renamed to {newName}.");
+ }
+ catch (Exception ex)
+ {
+ TShock.Log.Error(GetString($"An exception has occurred during database transaction: {ex.Message}"));
+ try
+ {
+ transaction.Rollback();
+ }
+ catch (Exception rollbackEx)
+ {
+ TShock.Log.Error(GetString($"An exception has occurred during database rollback: {rollbackEx.Message}"));
}
}
@@ -488,7 +491,7 @@ namespace TShockAPI.DB
/// The group's name.
/// Whether exceptions will be thrown in case something goes wrong.
/// The result from the operation to be sent back to the user.
- public String DeleteGroup(String name, bool exceptions = false)
+ public string DeleteGroup(string name, bool exceptions = false)
{
if (!GroupExists(name))
{
@@ -521,7 +524,7 @@ namespace TShockAPI.DB
/// The group name.
/// The permission list.
/// The result from the operation to be sent back to the user.
- public String AddPermissions(String name, List permissions)
+ public string AddPermissions(string name, List permissions)
{
if (!GroupExists(name))
return GetString($"Group {name} doesn't exist.");
@@ -544,7 +547,7 @@ namespace TShockAPI.DB
/// The group name.
/// The permission list.
/// The result from the operation to be sent back to the user.
- public String DeletePermissions(String name, List permissions)
+ public string DeletePermissions(string name, List permissions)
{
if (!GroupExists(name))
return GetString($"Group {name} doesn't exist.");
diff --git a/TShockAPI/DB/IQueryBuilder.cs b/TShockAPI/DB/IQueryBuilder.cs
deleted file mode 100644
index c2be131d..00000000
--- a/TShockAPI/DB/IQueryBuilder.cs
+++ /dev/null
@@ -1,401 +0,0 @@
-/*
-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(GetString("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(GetString("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)));
- }
- }
-}
diff --git a/TShockAPI/DB/ItemManager.cs b/TShockAPI/DB/ItemManager.cs
index 012132dc..e533d7c0 100644
--- a/TShockAPI/DB/ItemManager.cs
+++ b/TShockAPI/DB/ItemManager.cs
@@ -21,6 +21,7 @@ using System.Collections.Generic;
using System.Data;
using System.Linq;
using MySql.Data.MySqlClient;
+using TShockAPI.DB.Queries;
using TShockAPI.Hooks;
namespace TShockAPI.DB
@@ -38,10 +39,9 @@ namespace TShockAPI.DB
new SqlColumn("ItemName", MySqlDbType.VarChar, 50) {Primary = true},
new SqlColumn("AllowedGroups", MySqlDbType.Text)
);
- var creator = new SqlTableCreator(db,
- db.GetSqlType() == SqlType.Sqlite
- ? (IQueryBuilder) new SqliteQueryCreator()
- : new MysqlQueryCreator());
+
+ SqlTableCreator creator = new(db, db.GetSqlQueryBuilder());
+
creator.EnsureTableStructure(table);
UpdateItemBans();
}
@@ -50,14 +50,13 @@ namespace TShockAPI.DB
{
ItemBans.Clear();
- using (var reader = database.QueryReader("SELECT * FROM ItemBans"))
+ using var reader = database.QueryReader("SELECT * FROM ItemBans");
+
+ while (reader != null && reader.Read())
{
- while (reader != null && reader.Read())
- {
- ItemBan ban = new ItemBan(reader.Get("ItemName"));
- ban.SetAllowedGroups(reader.Get("AllowedGroups"));
- ItemBans.Add(ban);
- }
+ ItemBan ban = new ItemBan(reader.Get("ItemName"));
+ ban.SetAllowedGroups(reader.Get("AllowedGroups"));
+ ItemBans.Add(ban);
}
}
@@ -91,14 +90,7 @@ namespace TShockAPI.DB
}
}
- public bool ItemIsBanned(string name)
- {
- if (ItemBans.Contains(new ItemBan(name)))
- {
- return true;
- }
- return false;
- }
+ public bool ItemIsBanned(string name) => ItemBans.Contains(new(name));
public bool ItemIsBanned(string name, TSPlayer ply)
{
@@ -108,13 +100,12 @@ namespace TShockAPI.DB
public bool AllowGroup(string item, string name)
{
- string groupsNew = "";
ItemBan b = GetItemBanByName(item);
if (b != null)
{
try
{
- groupsNew = String.Join(",", b.AllowedGroups);
+ string groupsNew = string.Join(",", b.AllowedGroups);
if (groupsNew.Length > 0)
groupsNew += ",";
groupsNew += name;
@@ -122,7 +113,7 @@ namespace TShockAPI.DB
int q = database.Query("UPDATE ItemBans SET AllowedGroups=@0 WHERE ItemName=@1", groupsNew,
item);
-
+
return q > 0;
}
catch (Exception ex)
@@ -140,12 +131,12 @@ namespace TShockAPI.DB
if (b != null)
{
try
- {
+ {
b.RemoveGroup(group);
string groups = string.Join(",", b.AllowedGroups);
int q = database.Query("UPDATE ItemBans SET AllowedGroups=@0 WHERE ItemName=@1", groups,
item);
-
+
if (q > 0)
return true;
}
@@ -157,7 +148,7 @@ namespace TShockAPI.DB
return false;
}
- public ItemBan GetItemBanByName(String name)
+ public ItemBan GetItemBanByName(string name)
{
for (int i = 0; i < ItemBans.Count; i++)
{
@@ -188,10 +179,7 @@ namespace TShockAPI.DB
AllowedGroups = new List();
}
- public bool Equals(ItemBan other)
- {
- return Name == other.Name;
- }
+ public bool Equals(ItemBan other) => Name == other.Name;
public bool HasPermissionToUseItem(TSPlayer ply)
{
@@ -224,12 +212,12 @@ namespace TShockAPI.DB
// could add in the other permissions in this class instead of a giant if switch.
}
- public void SetAllowedGroups(String groups)
+ public void SetAllowedGroups(string groups)
{
// prevent null pointer exceptions
if (!string.IsNullOrEmpty(groups))
{
- List groupArr = groups.Split(',').ToList();
+ List groupArr = groups.Split(',').ToList();
for (int i = 0; i < groupArr.Count; i++)
{
@@ -244,10 +232,10 @@ namespace TShockAPI.DB
{
return AllowedGroups.Remove(groupName);
}
-
+
public override string ToString()
{
- return Name + (AllowedGroups.Count > 0 ? " (" + String.Join(",", AllowedGroups) + ")" : "");
+ return Name + (AllowedGroups.Count > 0 ? " (" + string.Join(",", AllowedGroups) + ")" : "");
}
}
-}
\ No newline at end of file
+}
diff --git a/TShockAPI/DB/ProjectileManager.cs b/TShockAPI/DB/ProjectileManager.cs
index 0e6cabe0..f9372f57 100644
--- a/TShockAPI/DB/ProjectileManager.cs
+++ b/TShockAPI/DB/ProjectileManager.cs
@@ -21,6 +21,7 @@ using System.Collections.Generic;
using System.Data;
using System.Linq;
using MySql.Data.MySqlClient;
+using TShockAPI.DB.Queries;
using TShockAPI.Hooks;
namespace TShockAPI.DB
@@ -38,10 +39,8 @@ namespace TShockAPI.DB
new SqlColumn("ProjectileID", MySqlDbType.Int32) {Primary = true},
new SqlColumn("AllowedGroups", MySqlDbType.Text)
);
- var creator = new SqlTableCreator(db,
- db.GetSqlType() == SqlType.Sqlite
- ? (IQueryBuilder) new SqliteQueryCreator()
- : new MysqlQueryCreator());
+
+ SqlTableCreator creator = new(db, db.GetSqlQueryBuilder());
creator.EnsureTableStructure(table);
UpdateBans();
}
@@ -50,14 +49,13 @@ namespace TShockAPI.DB
{
ProjectileBans.Clear();
- using (var reader = database.QueryReader("SELECT * FROM ProjectileBans"))
+ using var reader = database.QueryReader("SELECT * FROM ProjectileBans");
+
+ while (reader != null && reader.Read())
{
- while (reader != null && reader.Read())
- {
- ProjectileBan ban = new ProjectileBan((short) reader.Get("ProjectileID"));
- ban.SetAllowedGroups(reader.Get("AllowedGroups"));
- ProjectileBans.Add(ban);
- }
+ ProjectileBan ban = new ProjectileBan((short) reader.Get("ProjectileID"));
+ ban.SetAllowedGroups(reader.Get("AllowedGroups"));
+ ProjectileBans.Add(ban);
}
}
@@ -92,14 +90,7 @@ namespace TShockAPI.DB
}
}
- public bool ProjectileIsBanned(short id)
- {
- if (ProjectileBans.Contains(new ProjectileBan(id)))
- {
- return true;
- }
- return false;
- }
+ public bool ProjectileIsBanned(short id) => ProjectileBans.Contains(new(id));
public bool ProjectileIsBanned(short id, TSPlayer ply)
{
@@ -113,13 +104,12 @@ namespace TShockAPI.DB
public bool AllowGroup(short id, string name)
{
- string groupsNew = "";
ProjectileBan b = GetBanById(id);
if (b != null)
{
- try
+ try
{
- groupsNew = String.Join(",", b.AllowedGroups);
+ string groupsNew = string.Join(",", b.AllowedGroups);
if (groupsNew.Length > 0)
groupsNew += ",";
groupsNew += name;
@@ -193,10 +183,7 @@ namespace TShockAPI.DB
AllowedGroups = new List();
}
- public bool Equals(ProjectileBan other)
- {
- return ID == other.ID;
- }
+ public bool Equals(ProjectileBan other) => ID == other.ID;
public bool HasPermissionToCreateProjectile(TSPlayer ply)
{
@@ -229,12 +216,12 @@ namespace TShockAPI.DB
// could add in the other permissions in this class instead of a giant if switch.
}
- public void SetAllowedGroups(String groups)
+ public void SetAllowedGroups(string groups)
{
// prevent null pointer exceptions
if (!string.IsNullOrEmpty(groups))
{
- List groupArr = groups.Split(',').ToList();
+ List groupArr = groups.Split(',').ToList();
for (int i = 0; i < groupArr.Count; i++)
{
@@ -245,14 +232,8 @@ namespace TShockAPI.DB
}
}
- public bool RemoveGroup(string groupName)
- {
- return AllowedGroups.Remove(groupName);
- }
+ public bool RemoveGroup(string groupName) => AllowedGroups.Remove(groupName);
- public override string ToString()
- {
- return ID + (AllowedGroups.Count > 0 ? " (" + String.Join(",", AllowedGroups) + ")" : "");
- }
+ public override string ToString() => ID + (AllowedGroups.Count > 0 ? $" ({string.Join(",", AllowedGroups)})" : "");
}
}
diff --git a/TShockAPI/DB/Queries/GenericQueryBuilder.cs b/TShockAPI/DB/Queries/GenericQueryBuilder.cs
new file mode 100644
index 00000000..b62d3925
--- /dev/null
+++ b/TShockAPI/DB/Queries/GenericQueryBuilder.cs
@@ -0,0 +1,136 @@
+/*
+TShock, a server mod for Terraria
+Copyright (C) 2011-2025 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using MySql.Data.MySqlClient;
+using TShockAPI.Extensions;
+
+namespace TShockAPI.DB.Queries;
+
+///
+/// A Generic Query Creator (abstract)
+///
+public abstract class GenericQueryBuilder : IQueryBuilder
+{
+ protected static Random rand = new Random();
+
+ ///
+ /// Escapes the table name
+ ///
+ /// The name of the table to be escaped
+ ///
+ protected abstract string EscapeTableName(string table);
+
+ ///
+ public abstract string CreateTable(SqlTable table);
+
+ ///
+ public abstract string RenameTable(string from, string to);
+
+ ///
+ 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);
+ }
+
+ ///
+ public abstract string DbTypeToString(MySqlDbType type, int? length);
+
+ ///
+ /// Check for errors in the columns.
+ ///
+ ///
+ ///
+ protected static void ValidateSqlColumnType(List columns)
+ {
+ columns.ForEach(x =>
+ {
+ if (x.DefaultCurrentTimestamp && x.Type != MySqlDbType.DateTime)
+ {
+ throw new SqlColumnException(GetString("Can't set to true SqlColumn.DefaultCurrentTimestamp when the MySqlDbType is not DateTime"));
+ }
+ });
+ }
+
+
+ ///
+ public string DeleteRow(string table, List wheres)
+ {
+ return "DELETE FROM {0} {1}".SFormat(EscapeTableName(table), BuildWhere(wheres));
+ }
+
+ ///
+ public string UpdateValue(string table, List values, List wheres)
+ {
+ if (0 == values.Count)
+ throw new ArgumentException(GetString("No values supplied"));
+
+ return "UPDATE {0} SET {1} {2}".SFormat(EscapeTableName(table), string.Join(", ", values.Select(v => v.Name + " = " + v.Value)), BuildWhere(wheres));
+ }
+
+
+ ///
+ public string ReadColumn(string table, List wheres)
+ {
+ return "SELECT * FROM {0} {1}".SFormat(EscapeTableName(table), BuildWhere(wheres));
+ }
+
+
+ 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) => wheres.Count > 0
+ ? string.Empty
+ : "WHERE {0}".SFormat(string.Join(", ", wheres.Select(v => $"{v.Name} = {v.Value}")));
+}
diff --git a/TShockAPI/DB/Queries/IQueryBuilder.cs b/TShockAPI/DB/Queries/IQueryBuilder.cs
new file mode 100644
index 00000000..fe2d6b53
--- /dev/null
+++ b/TShockAPI/DB/Queries/IQueryBuilder.cs
@@ -0,0 +1,92 @@
+/*
+TShock, a server mod for Terraria
+Copyright (C) 2011-2025 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 System.Collections.Generic;
+using MySql.Data.MySqlClient;
+
+namespace TShockAPI.DB.Queries;
+
+///
+/// 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);
+}
diff --git a/TShockAPI/DB/Queries/MysqlQueryBuilder.cs b/TShockAPI/DB/Queries/MysqlQueryBuilder.cs
new file mode 100644
index 00000000..85185960
--- /dev/null
+++ b/TShockAPI/DB/Queries/MysqlQueryBuilder.cs
@@ -0,0 +1,88 @@
+/*
+TShock, a server mod for Terraria
+Copyright (C) 2011-2025 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using MySql.Data.MySqlClient;
+
+namespace TShockAPI.DB.Queries;
+
+///
+/// Query Creator for MySQL
+///
+public class MysqlQueryBuilder : GenericQueryBuilder, 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.Any()
+ ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques))
+ : "");
+ }
+
+
+ ///
+ public override string RenameTable(string from, string to) => /*lang=mysql*/"RENAME TABLE {0} TO {1}".SFormat(from, to);
+
+ private static readonly Dictionary TypesAsStrings = new()
+ {
+ { 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"},
+ };
+
+ ///
+ public override string DbTypeToString(MySqlDbType type, int? length)
+ {
+ if (TypesAsStrings.TryGetValue(type, out string ret))
+ {
+ return ret + (length is not null ? "({0})".SFormat((int)length) : "");
+ }
+
+ throw new NotImplementedException(Enum.GetName(typeof(MySqlDbType), type));
+ }
+
+ ///
+ protected override string EscapeTableName(string table) => table.SFormat("`{0}`", table);
+}
diff --git a/TShockAPI/DB/Queries/PostgresQueryBuilder.cs b/TShockAPI/DB/Queries/PostgresQueryBuilder.cs
new file mode 100644
index 00000000..dd92121e
--- /dev/null
+++ b/TShockAPI/DB/Queries/PostgresQueryBuilder.cs
@@ -0,0 +1,87 @@
+/*
+TShock, a server mod for Terraria
+Copyright (C) 2011-2025 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using MySql.Data.MySqlClient;
+
+namespace TShockAPI.DB.Queries;
+
+///
+/// Query Creator for PostgreSQL
+///
+public class PostgresQueryBuilder : GenericQueryBuilder
+{
+ ///
+ public override string DbTypeToString(MySqlDbType type, int? length) => type switch
+ {
+ MySqlDbType.VarChar when length is not null => "VARCHAR({0})".SFormat(length),
+ MySqlDbType.String when length is not null => "CHAR({0})".SFormat(length),
+ MySqlDbType.Text => "TEXT",
+ MySqlDbType.TinyText => "TEXT",
+ MySqlDbType.MediumText => "TEXT",
+ MySqlDbType.LongText => "TEXT",
+ MySqlDbType.Float => "REAL",
+ MySqlDbType.Double => "DOUBLE PRECISION",
+ MySqlDbType.Int32 => "INT",
+ MySqlDbType.Int64 => "BIGINT",
+ MySqlDbType.DateTime => "TIMESTAMP",
+
+ _ => throw new NotImplementedException(Enum.GetName(typeof(MySqlDbType), type))
+ };
+
+ ///
+ protected override string EscapeTableName(string table) => table.SFormat("\"{0}\"", table);
+
+ ///
+ public override string CreateTable(SqlTable table)
+ {
+ ValidateSqlColumnType(table.Columns);
+
+ IEnumerable columns = table.Columns.Select(c =>
+ {
+ // Handle PostgreSQL-specific auto-increment using SERIAL/BIGSERIAL
+ string dataType;
+
+ if (c.AutoIncrement)
+ {
+ dataType = c.Type is MySqlDbType.Int32 ? "SERIAL" : "BIGSERIAL";
+ }
+ else
+ {
+ dataType = DbTypeToString(c.Type, c.Length);
+ }
+
+ return "{0} {1} {2} {3} {4}".SFormat(c.Name,
+ dataType,
+ c.Primary ? "PRIMARY KEY" : "",
+ c.NotNull && !c.AutoIncrement ? "NOT NULL" : "", // SERIAL implies NOT NULL
+ c.DefaultCurrentTimestamp ? "DEFAULT CURRENT_TIMESTAMP" : "");
+ });
+
+ string[] uniques = table.Columns
+ .Where(c => c.Unique).Select(c => c.Name)
+ .ToArray(); // No re-enumeration
+
+ return $"CREATE TABLE {EscapeTableName(table.Name)} ({string.Join(", ", columns)} {(uniques.Any() ? ", UNIQUE({0})".SFormat(string.Join(", ", uniques)) : "")})";
+ }
+
+ ///
+ public override string RenameTable(string from, string to) => "ALTER TABLE {0} RENAME TO {1}".SFormat(from, to);
+}
diff --git a/TShockAPI/DB/Queries/SqliteQueryBuilder.cs b/TShockAPI/DB/Queries/SqliteQueryBuilder.cs
new file mode 100644
index 00000000..72de17d5
--- /dev/null
+++ b/TShockAPI/DB/Queries/SqliteQueryBuilder.cs
@@ -0,0 +1,103 @@
+/*
+TShock, a server mod for Terraria
+Copyright (C) 2011-2025 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 System;
+using System.Collections.Generic;
+using System.Linq;
+using MySql.Data.MySqlClient;
+
+namespace TShockAPI.DB.Queries;
+
+///
+/// Query Creator for Sqlite
+///
+public class SqliteQueryBuilder : GenericQueryBuilder, 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 override string DbTypeToString(MySqlDbType type, int? length)
+ {
+ if (TypesAsStrings.TryGetValue(type, out string 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) => $"\'{table}\'";
+}
diff --git a/TShockAPI/DB/RegionManager.cs b/TShockAPI/DB/RegionManager.cs
index 1a284ff3..d657980a 100644
--- a/TShockAPI/DB/RegionManager.cs
+++ b/TShockAPI/DB/RegionManager.cs
@@ -1,4 +1,4 @@
-/*
+/*
TShock, a server mod for Terraria
Copyright (C) 2011-2019 Pryaxis & TShock Contributors
@@ -23,6 +23,7 @@ using System.Linq;
using MySql.Data.MySqlClient;
using Terraria;
using Microsoft.Xna.Framework;
+using TShockAPI.DB.Queries;
namespace TShockAPI.DB
{
@@ -55,10 +56,7 @@ namespace TShockAPI.DB
new SqlColumn("Owner", MySqlDbType.VarChar, 50),
new SqlColumn("Z", MySqlDbType.Int32){ DefaultValue = "0" }
);
- var creator = new SqlTableCreator(db,
- db.GetSqlType() == SqlType.Sqlite
- ? (IQueryBuilder) new SqliteQueryCreator()
- : new MysqlQueryCreator());
+ SqlTableCreator creator = new(db, db.GetSqlQueryBuilder());
creator.EnsureTableStructure(table);
}
@@ -69,49 +67,46 @@ namespace TShockAPI.DB
{
try
{
- using (var reader = database.QueryReader("SELECT * FROM Regions WHERE WorldID=@0", Main.worldID.ToString()))
+ using var reader = database.QueryReader("SELECT * FROM Regions WHERE WorldID=@0", Main.worldID.ToString());
+
+ Regions.Clear();
+ while (reader.Read())
{
- Regions.Clear();
- while (reader.Read())
+ int id = reader.Get("Id");
+ int X1 = reader.Get("X1");
+ int Y1 = reader.Get("Y1");
+ int height = reader.Get("height");
+ int width = reader.Get("width");
+ int Protected = reader.Get("Protected");
+ string mergedids = reader.Get("UserIds");
+ string name = reader.Get("RegionName");
+ string owner = reader.Get("Owner");
+ string groups = reader.Get("Groups");
+ int z = reader.Get("Z");
+
+ string[] splitids = mergedids.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
+
+ Region r = new Region(id, new Rectangle(X1, Y1, width, height), name, owner, Protected != 0, Main.worldID.ToString(), z);
+ r.SetAllowedGroups(groups);
+ try
{
- int id = reader.Get("Id");
- int X1 = reader.Get("X1");
- int Y1 = reader.Get("Y1");
- int height = reader.Get("height");
- int width = reader.Get("width");
- int Protected = reader.Get("Protected");
- string mergedids = reader.Get("UserIds");
- string name = reader.Get("RegionName");
- string owner = reader.Get("Owner");
- string groups = reader.Get("Groups");
- int z = reader.Get("Z");
-
- string[] splitids = mergedids.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
-
- Region r = new Region(id, new Rectangle(X1, Y1, width, height), name, owner, Protected != 0, Main.worldID.ToString(), z);
- r.SetAllowedGroups(groups);
- try
+ foreach (string t in splitids)
{
- for (int i = 0; i < splitids.Length; i++)
- {
- int userid;
-
- if (Int32.TryParse(splitids[i], out userid)) // if unparsable, it's not an int, so silently skip
- r.AllowedIDs.Add(userid);
- else
- TShock.Log.Warn(GetString($"One of your UserIDs is not a usable integer: {splitids[i]}"));
- }
+ if (int.TryParse(t, out int userid)) // if unparsable, it's not an int, so silently skip
+ r.AllowedIDs.Add(userid);
+ else
+ TShock.Log.Warn(GetString($"One of your UserIDs is not a usable integer: {t}"));
}
- catch (Exception e)
- {
- TShock.Log.Error(GetString("Your database contains invalid UserIDs (they should be integers)."));
- TShock.Log.Error(GetString("A lot of things will fail because of this. You must manually delete and re-create the allowed field."));
- TShock.Log.Error(e.ToString());
- TShock.Log.Error(e.StackTrace);
- }
-
- Regions.Add(r);
}
+ catch (Exception e)
+ {
+ TShock.Log.Error(GetString("Your database contains invalid UserIDs (they should be integers)."));
+ TShock.Log.Error(GetString("A lot of things will fail because of this. You must manually delete and re-create the allowed field."));
+ TShock.Log.Error(e.ToString());
+ TShock.Log.Error(e.StackTrace);
+ }
+
+ Regions.Add(r);
}
}
catch (Exception ex)
@@ -295,10 +290,7 @@ namespace TShockAPI.DB
/// X coordinate
/// Y coordinate
/// Whether any regions exist at the given (x, y) coordinate
- public bool InArea(int x, int y)
- {
- return Regions.Any(r => r.InArea(x, y));
- }
+ public bool InArea(int x, int y) => Regions.Any(r => r.InArea(x, y));
///
/// Checks if any regions exist at the given (x, y) coordinate
@@ -307,10 +299,7 @@ namespace TShockAPI.DB
/// X coordinate
/// Y coordinate
/// The names of any regions that exist at the given (x, y) coordinate
- public IEnumerable InAreaRegionName(int x, int y)
- {
- return Regions.Where(r => r.InArea(x, y)).Select(r => r.Name);
- }
+ public IEnumerable InAreaRegionName(int x, int y) => Regions.Where(r => r.InArea(x, y)).Select(r => r.Name);
///
/// Checks if any regions exist at the given (x, y) coordinate
@@ -319,10 +308,7 @@ namespace TShockAPI.DB
/// X coordinate
/// Y coordinate
/// The IDs of any regions that exist at the given (x, y) coordinate
- public IEnumerable InAreaRegionID(int x, int y)
- {
- return Regions.Where(r => r.InArea(x, y)).Select(r => r.ID);
- }
+ public IEnumerable InAreaRegionID(int x, int y) => Regions.Where(r => r.InArea(x, y)).Select(r => r.ID);
///
/// Checks if any regions exist at the given (x, y) coordinate
@@ -331,10 +317,7 @@ namespace TShockAPI.DB
/// X coordinate
/// Y coordinate
/// The objects of any regions that exist at the given (x, y) coordinate
- public IEnumerable InAreaRegion(int x, int y)
- {
- return Regions.Where(r => r.InArea(x, y));
- }
+ public IEnumerable InAreaRegion(int x, int y) => Regions.Where(r => r.InArea(x, y));
///
/// Changes the size of a given region
@@ -413,7 +396,6 @@ namespace TShockAPI.DB
/// true if renamed successfully, false otherwise
public bool RenameRegion(string oldName, string newName)
{
- Region region = null;
string worldID = Main.worldID.ToString();
bool result = false;
@@ -425,7 +407,7 @@ namespace TShockAPI.DB
if (q > 0)
{
- region = Regions.First(r => r.Name == oldName && r.WorldID == worldID);
+ Region region = Regions.First(r => r.Name == oldName && r.WorldID == worldID);
region.Name = newName;
Hooks.RegionHooks.OnRegionRenamed(region, oldName, newName);
result = true;
@@ -522,7 +504,7 @@ namespace TShockAPI.DB
{
try
{
- Region region = Regions.First(r => String.Equals(regionName, r.Name, StringComparison.OrdinalIgnoreCase));
+ Region region = Regions.First(r => string.Equals(regionName, r.Name, StringComparison.OrdinalIgnoreCase));
region.Area = new Rectangle(x, y, width, height);
if (database.Query("UPDATE Regions SET X1 = @0, Y1 = @1, width = @2, height = @3 WHERE RegionName = @4 AND WorldID = @5",
@@ -546,11 +528,9 @@ namespace TShockAPI.DB
var regions = new List();
try
{
- using (var reader = database.QueryReader("SELECT RegionName FROM Regions WHERE WorldID=@0", worldid))
- {
- while (reader.Read())
- regions.Add(new Region {Name = reader.Get("RegionName")});
- }
+ using var reader = database.QueryReader("SELECT RegionName FROM Regions WHERE WorldID=@0", worldid);
+ while (reader.Read())
+ regions.Add(new Region {Name = reader.Get("RegionName")});
}
catch (Exception ex)
{
@@ -564,20 +544,14 @@ namespace TShockAPI.DB
///
/// Region name
/// The region with the given name, or null if not found
- public Region GetRegionByName(String name)
- {
- return Regions.FirstOrDefault(r => r.Name.Equals(name) && r.WorldID == Main.worldID.ToString());
- }
+ public Region GetRegionByName(string name) => Regions.FirstOrDefault(r => r.Name.Equals(name) && r.WorldID == Main.worldID.ToString());
///
/// Returns a region with the given ID
///
/// Region ID
/// The region with the given ID, or null if not found
- public Region GetRegionByID(int id)
- {
- return Regions.FirstOrDefault(r => r.ID == id && r.WorldID == Main.worldID.ToString());
- }
+ public Region GetRegionByID(int id) => Regions.FirstOrDefault(r => r.ID == id && r.WorldID == Main.worldID.ToString());
///
/// Changes the owner of the region with the given name
@@ -798,12 +772,12 @@ namespace TShockAPI.DB
/// Sets the user IDs which are allowed to use the region
///
/// String of IDs to set
- public void SetAllowedIDs(String ids)
+ public void SetAllowedIDs(string ids)
{
- String[] idArr = ids.Split(',');
+ string[] idArr = ids.Split(',');
List idList = new List();
- foreach (String id in idArr)
+ foreach (string id in idArr)
{
int i = 0;
if (int.TryParse(id, out i) && i != 0)
@@ -818,12 +792,12 @@ namespace TShockAPI.DB
/// Sets the group names which are allowed to use the region
///
/// String of group names to set
- public void SetAllowedGroups(String groups)
+ public void SetAllowedGroups(string groups)
{
// prevent null pointer exceptions
if (!string.IsNullOrEmpty(groups))
{
- List groupList = groups.Split(',').ToList();
+ List groupList = groups.Split(',').ToList();
for (int i = 0; i < groupList.Count; i++)
{
diff --git a/TShockAPI/DB/RememberedPosManager.cs b/TShockAPI/DB/RememberedPosManager.cs
index c49c5912..8d2f6986 100644
--- a/TShockAPI/DB/RememberedPosManager.cs
+++ b/TShockAPI/DB/RememberedPosManager.cs
@@ -21,6 +21,7 @@ using System.Data;
using MySql.Data.MySqlClient;
using Terraria;
using Microsoft.Xna.Framework;
+using TShockAPI.DB.Queries;
namespace TShockAPI.DB
{
@@ -39,10 +40,7 @@ namespace TShockAPI.DB
new SqlColumn("Y", MySqlDbType.Int32),
new SqlColumn("WorldID", MySqlDbType.Text)
);
- var creator = new SqlTableCreator(db,
- db.GetSqlType() == SqlType.Sqlite
- ? (IQueryBuilder) new SqliteQueryCreator()
- : new MysqlQueryCreator());
+ SqlTableCreator creator = new(db, db.GetSqlQueryBuilder());
creator.EnsureTableStructure(table);
}
@@ -50,19 +48,17 @@ namespace TShockAPI.DB
{
try
{
- using (var reader = database.QueryReader("SELECT * FROM RememberedPos WHERE Name=@0", name))
+ using var reader = database.QueryReader("SELECT * FROM RememberedPos WHERE Name=@0", name);
+ if (reader.Read())
{
- if (reader.Read())
- {
- int checkX=reader.Get("X");
- int checkY=reader.Get("Y");
- //fix leftover inconsistencies
- if (checkX==0)
- checkX++;
- if (checkY==0)
- checkY++;
- return new Vector2(checkX, checkY);
- }
+ int checkX=reader.Get("X");
+ int checkY=reader.Get("Y");
+ //fix leftover inconsistencies
+ if (checkX==0)
+ checkX++;
+ if (checkY==0)
+ checkY++;
+ return new Vector2(checkX, checkY);
}
}
catch (Exception ex)
@@ -79,12 +75,10 @@ namespace TShockAPI.DB
{
try
{
- using (var reader = database.QueryReader("SELECT * FROM RememberedPos WHERE Name=@0 AND IP=@1 AND WorldID=@2", name, IP, Main.worldID.ToString()))
+ using var reader = database.QueryReader("SELECT * FROM RememberedPos WHERE Name=@0 AND IP=@1 AND WorldID=@2", name, IP, Main.worldID.ToString());
+ if (reader.Read())
{
- if (reader.Read())
- {
- return new Vector2(reader.Get("X"), reader.Get("Y"));
- }
+ return new Vector2(reader.Get("X"), reader.Get("Y"));
}
}
catch (Exception ex)
diff --git a/TShockAPI/DB/ResearchDatastore.cs b/TShockAPI/DB/ResearchDatastore.cs
index 0d2429d7..6f318a91 100644
--- a/TShockAPI/DB/ResearchDatastore.cs
+++ b/TShockAPI/DB/ResearchDatastore.cs
@@ -7,6 +7,7 @@ using System.Text;
using System.Threading.Tasks;
using Terraria;
using Terraria.ID;
+using TShockAPI.DB.Queries;
namespace TShockAPI.DB
{
@@ -40,10 +41,9 @@ namespace TShockAPI.DB
new SqlColumn("AmountSacrificed", MySqlDbType.Int32),
new SqlColumn("TimeSacrificed", MySqlDbType.DateTime)
);
- var creator = new SqlTableCreator(db,
- db.GetSqlType() == SqlType.Sqlite
- ? (IQueryBuilder)new SqliteQueryCreator()
- : new MysqlQueryCreator());
+
+ SqlTableCreator creator = new(db, db.GetSqlQueryBuilder());
+
try
{
creator.EnsureTableStructure(table);
@@ -84,15 +84,14 @@ namespace TShockAPI.DB
where WorldId = @0
group by itemId";
- try {
- using (var reader = database.QueryReader(sql, Main.worldID))
+ try
+ {
+ using var reader = database.QueryReader(sql, Main.worldID);
+ while (reader.Read())
{
- while (reader.Read())
- {
- var itemId = reader.Get("itemId");
- var amount = reader.Get("totalSacrificed");
- sacrificedItems[itemId] = amount;
- }
+ var itemId = reader.Get("itemId");
+ var amount = reader.Get("totalSacrificed");
+ sacrificedItems[itemId] = amount;
}
}
catch (Exception ex)
diff --git a/TShockAPI/DB/SqlTable.cs b/TShockAPI/DB/SqlTable.cs
index 1f0c4593..22591ff8 100644
--- a/TShockAPI/DB/SqlTable.cs
+++ b/TShockAPI/DB/SqlTable.cs
@@ -21,6 +21,7 @@ using System.Collections.Generic;
using System.Data;
using System.Linq;
using MySql.Data.MySqlClient;
+using TShockAPI.DB.Queries;
namespace TShockAPI.DB
{
@@ -58,7 +59,9 @@ namespace TShockAPI.DB
var columns = GetColumns(table);
if (columns.Count > 0)
{
- if (!table.Columns.All(c => columns.Contains(c.Name)) || !columns.All(c => table.Columns.Any(c2 => c2.Name == c)))
+ // Use OrdinalIgnoreCase to account for pgsql automatically lowering cases.
+ if (!table.Columns.All(c => columns.Contains(c.Name, StringComparer.OrdinalIgnoreCase))
+ || !columns.All(c => table.Columns.Any(c2 => c2.Name.Equals(c, StringComparison.OrdinalIgnoreCase))))
{
var from = new SqlTable(table.Name, columns.Select(s => new SqlColumn(s, MySqlDbType.String)).ToList());
database.Query(creator.AlterTable(from, table));
@@ -69,36 +72,50 @@ namespace TShockAPI.DB
database.Query(creator.CreateTable(table));
return true;
}
+
return false;
}
public List GetColumns(SqlTable table)
{
- var ret = new List();
- var name = database.GetSqlType();
- if (name == SqlType.Sqlite)
+ List ret = new();
+ switch (database.GetSqlType())
{
- using (var reader = database.QueryReader("PRAGMA table_info({0})".SFormat(table.Name)))
+ case SqlType.Sqlite:
{
+ using QueryResult reader = database.QueryReader("PRAGMA table_info({0})".SFormat(table.Name));
while (reader.Read())
+ {
ret.Add(reader.Get("name"));
+ }
+
+ break;
}
- }
- else if (name == SqlType.Mysql)
- {
- using (
- var reader =
- database.QueryReader(
- "SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME=@0 AND TABLE_SCHEMA=@1", table.Name,
- database.Database))
+ case SqlType.Mysql:
{
+ using QueryResult reader =
+ database.QueryReader("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME=@0 AND TABLE_SCHEMA=@1", table.Name, database.Database);
+
while (reader.Read())
+ {
ret.Add(reader.Get("COLUMN_NAME"));
+ }
+
+ break;
}
- }
- else
- {
- throw new NotSupportedException();
+ case SqlType.Postgres:
+ {
+ // HACK: Using "ilike" op to ignore case, due to weird case issues adapting for pgsql
+ using QueryResult reader = database.QueryReader("SELECT column_name FROM information_schema.columns WHERE table_schema=current_schema() AND table_name ILIKE @0", table.Name);
+
+ while (reader.Read())
+ {
+ ret.Add(reader.Get("column_name"));
+ }
+
+ break;
+ }
+ default: throw new NotSupportedException();
}
return ret;
@@ -109,4 +126,4 @@ namespace TShockAPI.DB
database.Query(creator.DeleteRow(table, wheres));
}
}
-}
\ No newline at end of file
+}
diff --git a/TShockAPI/DB/SqlValue.cs b/TShockAPI/DB/SqlValue.cs
index aa70e2d6..eaeaa106 100644
--- a/TShockAPI/DB/SqlValue.cs
+++ b/TShockAPI/DB/SqlValue.cs
@@ -18,6 +18,7 @@ along with this program. If not, see .
using System.Collections.Generic;
using System.Data;
+using TShockAPI.DB.Queries;
namespace TShockAPI.DB
{
@@ -58,11 +59,9 @@ namespace TShockAPI.DB
{
List