TShock/TShockAPI/DB/SqlTable.cs
Sakura Akeno Isayeki 2d839e3609
fix(db/postgres): Resolve SQL identifier casing issues
Improves SQL query execution by ensuring proper casing for identifiers across various database types, particularly for Postgres.

Enhances security and compatibility by using an identifier escaping method, preventing potential errors due to case sensitivity in SQL queries.

Addresses potential issues with existing queries for better reliability and consistency.
2025-04-29 00:47:19 +02:00

129 lines
3.4 KiB
C#

/*
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 <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using MySql.Data.MySqlClient;
using TShockAPI.DB.Queries;
namespace TShockAPI.DB
{
public class SqlTable
{
public List<SqlColumn> Columns { get; protected set; }
public string Name { get; protected set; }
public SqlTable(string name, params SqlColumn[] columns)
: this(name, new List<SqlColumn>(columns))
{
}
public SqlTable(string name, List<SqlColumn> columns)
{
Name = name;
Columns = columns;
}
}
public class SqlTableCreator
{
private IDbConnection database;
private IQueryBuilder creator;
public SqlTableCreator(IDbConnection db, IQueryBuilder provider)
{
database = db;
creator = provider;
}
// Returns true if the table was created; false if it was not.
public bool EnsureTableStructure(SqlTable table)
{
var columns = GetColumns(table);
if (columns.Count > 0)
{
// 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));
}
}
else
{
database.Query(creator.CreateTable(table));
return true;
}
return false;
}
public List<string> GetColumns(SqlTable table)
{
List<string> ret = new();
switch (database.GetSqlType())
{
case SqlType.Sqlite:
{
using QueryResult reader = database.QueryReader("PRAGMA table_info({0})".SFormat(table.Name));
while (reader.Read())
{
ret.Add(reader.Get<string>("name"));
}
break;
}
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<string>("COLUMN_NAME"));
}
break;
}
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_name ILIKE @0", table.Name);
while (reader.Read())
{
ret.Add(reader.Get<string>("column_name"));
}
break;
}
default: throw new NotSupportedException();
}
return ret;
}
public void DeleteRow(string table, List<SqlValue> wheres)
{
database.Query(creator.DeleteRow(table, wheres));
}
}
}