#include "ClientDatabase.h" #include #include #include #include #include #include #include #include namespace Noggit { // namespace Sql auto escapeSqlString = [](const QString& str) -> QString { QString result = str; result.replace("'", "''"); // double single quotes for SQL return "'" + result + "'"; }; void ClientDatabase::setDatabaseMode(DatabaseMode mode) { _database_mode = mode; } DatabaseMode ClientDatabase::_database_mode = DatabaseMode::ClientStorage; DatabaseMode ClientDatabase::databaseMode() { return _database_mode; } ClientDatabaseTable ClientDatabase::getTable(const std::string& tableName) { return ClientDatabaseTable(tableName); } bool ClientDatabaseTable::UploadDBCtoDB() { auto sql_table_name = getSqlTableName(); /* if (!verifySqlTableIntegrity()) { Log << "Table " << sql_table_name << "does not exist or has wrong structure."; qDebug() << "Table " << sql_table_name.c_str() << "does not exist or has wrong structure."; return false; }*/ qDebug() << "Populating empty Table " << sql_table_name.c_str(); // insert if fresh_table, otherwise replace? auto& row_definition = GetRecordDefinition(); auto sql_record_format = recordFormat(); auto client_table_iterator = getClientTable().Records(); // empty table, nothing to insert if (!client_table_iterator.HasRecords()) return false; QStringList column_names; for (auto& sql_column_format : sql_record_format) { column_names.append(sql_column_format.Name.c_str()); } int colCount = column_names.size(); auto& db_mgr = Noggit::Sql::SqlDatabaseManager::instance(); auto noggit_db = db_mgr.noggitDatabase(); QSqlQuery query(noggit_db); // Start bulk insert /////////////////////////////// QElapsedTimer timer; timer.start(); const int batchSize = 2000; int rowCount = 0; noggit_db.transaction(); // query.exec("SET UNIQUE_CHECKS=0;"); QStringList rowBuffer; // holds each row as a string rowBuffer.reserve(batchSize); while (client_table_iterator.HasRecords()) { auto& record = client_table_iterator.Next(); QStringList colValues; colValues.reserve(column_names.size()); for (auto& column_def : row_definition.ColumnDefinitions) { if (column_def.Type == "int" && column_def.isID) { colValues.append(QString::number(record.RecordId)); continue; } auto& rowColumn = record.Columns.at(column_def.Name); if (column_def.Type == "locstring") { for (int i = 0; i < 16; ++i) colValues.append(escapeSqlString(QString::fromStdString(rowColumn.Values[i]))); auto& flagValue = record.Columns.at(column_def.Name + "_flags").Value; colValues.append(QString::fromStdString(flagValue)); } else { int len = (column_def.arrLength > 1) ? column_def.arrLength : 1; for (int i = 0; i < len; ++i) { if (column_def.Type == "string") colValues.append(escapeSqlString((len > 1) ? QString::fromStdString(rowColumn.Values[i]) : QString::fromStdString(rowColumn.Value))); else // int/float colValues.append((len > 1) ? QString::fromStdString(rowColumn.Values[i]) : QString::fromStdString(rowColumn.Value)); } } } // append row as a single string rowBuffer.append("(" + colValues.join(",") + ")"); rowCount++; // flush batch if (rowCount % batchSize == 0) { int min_size = rowCount * ((column_names.size()*2) + 2); // 2 chars minimum per column (value and comma) // qDebug() << "min size" << min_size; QString sql; sql.reserve(min_size); sql = QString("INSERT INTO `%1` (%2) VALUES ") .arg(sql_table_name.c_str()) .arg(column_names.join(", ")); sql += rowBuffer.join(","); if (!query.exec(sql)) { qWarning() << "Batch insert failed:" << query.lastError().text(); // query.exec("SET UNIQUE_CHECKS=1;"); noggit_db.rollback(); return false; } rowBuffer.clear(); rowCount = 0; } } // flush remaining rows if (!rowBuffer.isEmpty()) { int min_size = rowCount * ((column_names.size() * 2) + 2); QString sql; sql.reserve(min_size); sql = QString("INSERT INTO `%1` (%2) VALUES ") .arg(sql_table_name.c_str()) .arg(column_names.join(", ")); sql += rowBuffer.join(","); if (!query.exec(sql)) { qWarning() << "Final batch insert failed:" << query.lastError().text(); // query.exec("SET UNIQUE_CHECKS=1;"); noggit_db.rollback(); return false; } } // query.exec("SET UNIQUE_CHECKS=1;"); noggit_db.commit(); // benchmark qint64 elapsedMs = timer.elapsed(); qDebug() << "Inserted" << getClientTable().RecordCount() << "rows in" << elapsedMs << "ms (" << (getClientTable().RecordCount() * 1000.0 / elapsedMs) << " rows/sec)"; Log << "Inserted " << getClientTable().RecordCount() << " rows in " << elapsedMs << "ms (" << (getClientTable().RecordCount() * 1000.0 / elapsedMs) << " rows/sec)" << std::endl; return true; } // executes query in client db and checks errors // use isActive to check if it properly ran, not isValid. QSqlQuery ClientDatabase::executeQuery(const QString& sql, bool forward_only) { auto db_mgr = Noggit::Sql::SqlDatabaseManager::instance().noggitDatabase(); QSqlQuery query(Noggit::Sql::SqlDatabaseManager::instance().noggitDatabase()); qDebug() << "Executing query : " << sql; QElapsedTimer timer; timer.start(); if (forward_only) // call this for browsing large data sets query.setForwardOnly(true); if (!query.exec(sql)) { LogError << "SQL query failed:" << query.lastError().text().toStdString(); LogError << "Query:" << sql.toStdString(); // throw SqlException("Query failed: " + query.lastError().text() + "\nQuery: " + sql); assert(false); return QSqlQuery(); // invalid query } qint64 elapsedMs = timer.elapsed(); qDebug() << "Executed query in " << elapsedMs << "ms"; return query; } bool ClientDatabaseTable::createSQLTableIfNotExist() { auto row_definition = GetRecordDefinition(); const std::string sql_table_name = getSqlTableName(); auto db_record_format = recordFormat(); assert(db_record_format.size() == getClientTable().ColumnCount()); std::string statement = std::format("CREATE TABLE IF NOT EXISTS `{}` (", sql_table_name); std::string primary_key_name; for (auto& db_column_format : db_record_format) { statement += std::format("`{}` {}", db_column_format.Name, db_column_format.Type); if (db_column_format.Type == "TEXT") { statement += " NULL"; // allow NULL by default } else { if (!db_column_format.isSigned && db_column_format.Type == "INT") { // assert(db_column_format.Type == "INT"); statement += " UNSIGNED"; // only allow int to be unsigned? } statement += " NOT NULL"; // allow text to be nulled statement += " DEFAULT 0"; } statement += ",\n"; if (db_column_format.isID) { assert(primary_key_name.empty()); // more than one key ? TODO primary_key_name = db_column_format.Name; } } if (!primary_key_name.empty()) statement += std::format("PRIMARY KEY (`{}`)", primary_key_name); // Add indexes for relations for (auto& db_column_format : db_record_format) { if (db_column_format.isRelation && !db_column_format.isID) { statement += std::format(",\nINDEX (`{}`)", db_column_format.Name); } } // statement += ")\n ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 DEFAULT COLLATE='utf8mb4_general_ci';"; statement += ")\n ENGINE = InnoDB;"; auto& db_mgr = Noggit::Sql::SqlDatabaseManager::instance(); bool valid_conn = db_mgr.testConnection(Noggit::Sql::SQLDbType::Noggit); if (!valid_conn) return false; auto noggit_db = db_mgr.noggitDatabase(); QSqlQuery query(noggit_db); bool success = query.exec(QString::fromStdString(statement)); if (!success) { qDebug() << "Failed to create table:" << query.lastError().text(); } else { qDebug() << "Table " << sql_table_name.c_str() << " created."; UploadDBCtoDB(); } return success; } std::vector ClientDatabaseTable::recordFormat() const { auto record_format = std::vector(); auto& row_definition = GetRecordDefinition(); for (int col_idx = 0; col_idx < row_definition.ColumnDefinitions.size(); col_idx++) { auto& column_def = row_definition.ColumnDefinitions[col_idx]; bool is_locstring = false; // convert dbd definition type names to real format // TODO : map types std::string sql_data_type = "INT"; if (BlizzardDatabaseLib::Extension::String::Compare(column_def.Type, "int")) { sql_data_type = "INT"; } else if (BlizzardDatabaseLib::Extension::String::Compare(column_def.Type, "float")) { sql_data_type = "FLOAT"; } else if (BlizzardDatabaseLib::Extension::String::Compare(column_def.Type, "string")) { sql_data_type = "TEXT"; } else if (BlizzardDatabaseLib::Extension::String::Compare(column_def.Type, "locstring")) { sql_data_type = "TEXT"; is_locstring = true; } else assert(false); int array_size = 1; if (column_def.arrLength > 1) { array_size = column_def.arrLength; } if (is_locstring) array_size = 16; for (int i = 0; i < array_size; i++) { DbColumnFormat db_col_format; std::string col_name = ""; if (array_size == 1) { col_name = column_def.Name; } else if (is_locstring) { col_name = std::format("{}_{}", column_def.Name, dbc_string_loc_names[i]); // {MapName_lang}_{enUS} } else if (array_size > 1) { col_name = std::format("{}_{}", column_def.Name, i); // {MapName}_{0} } db_col_format.Name = col_name; db_col_format.Type = sql_data_type; assert(!(column_def.isID && array_size > 1)); db_col_format.isID = column_def.isID; db_col_format.isRelation = column_def.isRelation; db_col_format.isSigned = column_def.isSigned; record_format.push_back(db_col_format); } if (is_locstring) // add lang mask column { DbColumnFormat db_col_format; db_col_format.Name = std::format("{}_flags", column_def.Name); db_col_format.Type = "INT"; db_col_format.isSigned = false; db_col_format.isID = false; db_col_format.isRelation = false; record_format.push_back(db_col_format); } } return record_format; } bool ClientDatabaseTable::verifySqlTableIntegrity() { auto& db_mgr = Noggit::Sql::SqlDatabaseManager::instance(); bool valid_conn = db_mgr.testConnection(Noggit::Sql::SQLDbType::Noggit); if (!valid_conn) return false; // check if table exists QString sql_table_name = getSqlTableName().c_str(); auto noggit_db = db_mgr.noggitDatabase(); // table integrity check bool table_is_valid = true; bool fresh_table = false; // noggit_db.tables().contains(sql_table_name) is bugged with current qt version and mysql 8 QSqlQuery query_show(noggit_db); if (!query_show.exec("SHOW TABLES")) { qWarning() << "Failed to list tables:" << query_show.lastError().text(); return false; } QStringList tables; while (query_show.next()) { tables << query_show.value(0).toString(); } if (tables.contains(sql_table_name)) { // this is also bugged... // QSqlRecord sql_rec = noggit_db.record(sql_table_name); // if (sql_rec.isEmpty()) // { // table_is_valid = false; // } // else // { // // TODO verify db structure, just column count for now // if (table.ColumnCount() != sql_rec.count()) // { // assert(false); // table_is_valid = false; // } // } } else // table doesn't exist { // create table table_is_valid = createSQLTableIfNotExist(); fresh_table = true; } return table_is_valid; } // Structures::BlizzardDatabaseRow ClientDatabaseTable::sqlRecordToDatabaseRow(const QSqlRecord& record) const Structures::BlizzardDatabaseRow ClientDatabaseTable::sqlRecordToDatabaseRow(QSqlQuery& record) const { auto& row_definition = GetRecordDefinition(); auto database_row = Structures::BlizzardDatabaseRow(-1); int Id = -1; int field_idx = 0; for (int column_def_idx = 0; column_def_idx < row_definition.ColumnDefinitions.size(); ++column_def_idx) { auto& column_def = row_definition.ColumnDefinitions[column_def_idx]; auto database_column = Structures::BlizzardDatabaseColumn(); if (column_def.Type == "locstring") { database_column.Values.resize(16); for (int loc_idx = 0; loc_idx < 16; loc_idx++) { database_column.Values[loc_idx] = (record.value(field_idx++).toString().toStdString()); } // currently loc mask is set to a separate column because wdbc reader does it. auto loc_mask_column = Structures::BlizzardDatabaseColumn(); loc_mask_column.Value = record.value(field_idx++).toString().toStdString(); database_row.Columns[column_def.Name + "_flags"] = loc_mask_column; } else // every other type than locstring { if (column_def.arrLength > 1) // array { database_column.Values.resize(column_def.arrLength); for (int i = 0; i < column_def.arrLength; i++) { database_column.Values[i] = (record.value(field_idx++).toString().toStdString()); } } else // single value { database_column.Value = record.value(field_idx++).toString().toStdString(); if (column_def.isID) Id = std::stoi(database_column.Value); } } database_row.Columns[column_def.Name] = std::move(database_column); } assert(Id != -1); // no id found database_row.RecordId = Id; return database_row; } ClientDatabaseTable::ClientDatabaseTable(std::string tableName) : _tableName(tableName), _qtTableName(tableName.c_str()) { if (ClientDatabase::databaseMode() == DatabaseMode::Sql) verifySqlTableIntegrity(); // verifySqlTableIntegrity()->createtableifnotexists()->UploadDBCtoDB() }; unsigned int ClientDatabaseTable::RecordCount() const { unsigned int client_count = getClientTable().RecordCount(); if (ClientDatabase::databaseMode() == DatabaseMode::Sql) { QString sql = QString("SELECT COUNT(*) FROM `%1`").arg(getSqlTableName().c_str()); QSqlQuery query = ClientDatabase::executeQuery(sql); if (query.isActive()) { if (query.next()) assert(query.value(0).toUInt() == client_count); } } return client_count; } int ClientDatabaseTable::ColumnCount() const { // get from parsed definition int def_column_count = recordFormat().size(); int client_count = getClientTable().ColumnCount(); assert(def_column_count == client_count); if (ClientDatabase::databaseMode() == DatabaseMode::Sql) { auto db = Noggit::Sql::SqlDatabaseManager::instance().noggitDatabase(); QSqlRecord rec = db.record(QString::fromStdString(getSqlTableName())); int db_count = rec.count(); assert(db_count == def_column_count); } return def_column_count; } std::optional ClientDatabaseTable::RecordById(unsigned int id) const { auto row = Structures::BlizzardDatabaseRow(-1); if (ClientDatabase::databaseMode() == DatabaseMode::Sql) row = sqlRowById(id); else row = clientRowById(id); if (row.RecordId == -1) return std::nullopt; else return row; } Noggit::DatabaseRecordCollection ClientDatabaseTable::Records() const { return Noggit::DatabaseRecordCollection(*this); }; /* std::optional ClientDatabaseTable::RecordByPosition(unsigned int positionId) const { // TODO auto row = Structures::BlizzardDatabaseRow(-1); if (ClientDatabase::databaseMode() == DatabaseMode::Sql) { // We shouldn't do this with SQL table } else row = getClientTable().RecordByPosition(positionId); return std::optional(); }*/ Structures::BlizzardDatabaseRowDefinition& ClientDatabaseTable::GetRecordDefinition() const { return Noggit::Project::CurrentProject::get()->ClientDatabase->TableRecordDefinition(_tableName); } BlizzardDatabaseLib::BlizzardDatabaseTable& ClientDatabaseTable::getClientTable() const { return Noggit::Project::CurrentProject::get()->ClientDatabase->LoadTable(_tableName, readFileAsIMemStream); } // get from local dbc data memory stream in BlizzardDatabaseLib::BlizzardDatabase Structures::BlizzardDatabaseRow ClientDatabaseTable::clientRowById(unsigned int id) const { auto record = getClientTable().RecordById(id); return record; } // get from SQL request to noggit db // never use this function for more than 1 rows, implement a new bulk function Structures::BlizzardDatabaseRow ClientDatabaseTable::sqlRowById(unsigned int id) const { QString sql_table_name = getSqlTableName().c_str(); QString sql = QString("SELECT * FROM `%1` WHERE ID = %2").arg(sql_table_name).arg(id); auto query = ClientDatabase::executeQuery(sql); if (!query.isActive()) return BlizzardDatabaseLib::Structures::BlizzardDatabaseRow(-1); // auto row_definition = GetRecordDefinition(); if (query.next()) { // QSqlRecord record = query.record(); // slow af auto database_row = sqlRecordToDatabaseRow(query); return database_row; } else { LogError << "SQL : No row found in" << sql_table_name.toStdString() << "for ID =" << id; qWarning() << "SQL : No row found in" << sql_table_name << "for ID =" << id; return BlizzardDatabaseLib::Structures::BlizzardDatabaseRow(); } } const std::string ClientDatabaseTable::getSqlTableName(unsigned int build_id) const { if (build_id == 0) build_id = Noggit::Project::CurrentProject::get()->buildId(); std::string table = std::format("db_{}_{}", _tableName, build_id); // convert to lowercase for compatibility with SQL std::transform(table.begin(), table.end(), table.begin(), [](unsigned char c) { return std::tolower(c); }); return table; } DatabaseRecordCollection::DatabaseRecordCollection(const ClientDatabaseTable& table) :_table(table), /*_mode(mode),*/ _client_iterator(_table.getClientTable().Records()) { if (ClientDatabase::databaseMode() == DatabaseMode::Sql) { QString sql = QString("SELECT * FROM `%1`").arg(_table.getSqlTableName().c_str()); // ORDER BY ID ? _query = ClientDatabase::executeQuery(sql, true); _querry_valid = _query.isActive(); // if query.exec ran properly querryAdvance(); } } bool DatabaseRecordCollection::HasRecords() { if (ClientDatabase::databaseMode() == DatabaseMode::ClientStorage) return _client_iterator.HasRecords(); else { return _querry_valid && _hasNext; } } Structures::BlizzardDatabaseRow DatabaseRecordCollection::Next() { if (ClientDatabase::databaseMode() == DatabaseMode::ClientStorage) return _client_iterator.Next(); else { if (!_querry_valid || !_hasNext) { assert(false); return Structures::BlizzardDatabaseRow(); // empty } // always store one row in advance to know if it's the last one // auto row = _table.sqlRecordToDatabaseRow(_nextRecord); assert(_nextRecord.RecordId != -1); querryAdvance(); return _nextRecord; } } void DatabaseRecordCollection::querryAdvance() { if (_query.next()) { // _nextRecord = _query.record(); _nextRecord = _table.sqlRecordToDatabaseRow(_query); _hasNext = true; } else { _hasNext = false; } } }