24#include <system_error>
33 bool StartsWithCaseInsensitive(std::string
const& text,
char const* prefix)
35 std::size_t prefixLength = std::strlen(prefix);
36 if (text.length() < prefixLength)
39 for (std::size_t i = 0; i < prefixLength; ++i)
41 if (std::tolower(
static_cast<unsigned char>(text[i])) !=
42 std::tolower(
static_cast<unsigned char>(prefix[i])))
49 std::string ExtractSqlTableName(std::string
const& statement)
51 std::string::size_type begin = 0;
52 while (begin < statement.length() && std::isspace(
static_cast<unsigned char>(statement[begin])))
55 std::string trimmed = statement.substr(begin);
56 if (!StartsWithCaseInsensitive(trimmed,
"DROP TABLE") &&
57 !StartsWithCaseInsensitive(trimmed,
"CREATE TABLE") &&
58 !StartsWithCaseInsensitive(trimmed,
"INSERT INTO") &&
59 !StartsWithCaseInsensitive(trimmed,
"DELETE FROM") &&
60 !StartsWithCaseInsensitive(trimmed,
"ALTER TABLE"))
63 std::string::size_type tableBegin = trimmed.find(
'`');
64 if (tableBegin == std::string::npos)
67 std::string::size_type tableEnd = trimmed.find(
'`', tableBegin + 1);
68 if (tableEnd == std::string::npos)
71 return trimmed.substr(tableBegin + 1, tableEnd - tableBegin - 1);
76 if (!sqlContext.TotalBytes)
79 std::uintmax_t percent = sqlContext.BytesRead * 100 / sqlContext.TotalBytes;
80 return uint32(percent > 100 ? 100 : percent);
83 std::string TrimCopy(std::string
const& text)
85 std::string::size_type begin = 0;
86 while (begin < text.length() && std::isspace(
static_cast<unsigned char>(text[begin])))
89 std::string::size_type end = text.length();
90 while (end > begin && std::isspace(
static_cast<unsigned char>(text[end - 1])))
93 return text.substr(begin, end - begin);
96 std::string::size_type FindKeywordOutsideQuotedText(std::string
const& sql,
char const* keyword)
98 bool inSingleQuote =
false;
99 bool inDoubleQuote =
false;
100 bool inBacktick =
false;
101 bool escaped =
false;
102 std::size_t keywordLength = std::strlen(keyword);
104 for (std::string::size_type i = 0; i < sql.length(); ++i)
115 inSingleQuote =
false;
127 inDoubleQuote =
false;
142 inSingleQuote =
true;
148 inDoubleQuote =
true;
158 if (i + keywordLength <= sql.length() &&
159 StartsWithCaseInsensitive(sql.substr(i, keywordLength), keyword))
161 bool beforeBoundary = i == 0 ||
162 !std::isalnum(
static_cast<unsigned char>(sql[i - 1]));
163 bool afterBoundary = i + keywordLength == sql.length() ||
164 !std::isalnum(
static_cast<unsigned char>(sql[i + keywordLength]));
166 if (beforeBoundary && afterBoundary)
171 return std::string::npos;
174 std::vector<std::string> SplitInsertRows(std::string
const& values)
176 std::vector<std::string> rows;
177 bool inSingleQuote =
false;
178 bool inDoubleQuote =
false;
179 bool escaped =
false;
181 std::string::size_type rowBegin = 0;
183 for (std::string::size_type i = 0; i < values.length(); ++i)
194 inSingleQuote =
false;
206 inDoubleQuote =
false;
213 inSingleQuote =
true;
219 inDoubleQuote =
true;
230 else if (c ==
',' && depth == 0)
232 std::string row = TrimCopy(values.substr(rowBegin, i - rowBegin));
240 std::string row = TrimCopy(values.substr(rowBegin));
247 bool ExecuteSetupQuery(MYSQL* setupConnection, std::string
const& sql,
char const* queryContext,
250 if (mysql_query(setupConnection, sql.c_str()))
252 SF_LOG_ERROR(context.LogFilter,
"%s: %s", queryContext, mysql_error(setupConnection));
258 MYSQL_RES* result = mysql_store_result(setupConnection);
260 mysql_free_result(result);
261 else if (mysql_field_count(setupConnection) != 0)
263 SF_LOG_ERROR(context.LogFilter,
"%s: %s", queryContext, mysql_error(setupConnection));
267 int nextResult = mysql_next_result(setupConnection);
270 SF_LOG_ERROR(context.LogFilter,
"%s: %s", queryContext, mysql_error(setupConnection));
281 bool ExecuteSetupQueryWithInsertChunks(MYSQL* setupConnection, std::string
const& sql,
282 char const* queryContext,
SetupRuntimeContext const& context, std::string
const& tableName,
285 constexpr std::size_t InsertChunkRows = 250;
286 constexpr std::size_t LargeInsertThreshold = 1024 * 1024;
288 std::string trimmed = TrimCopy(sql);
289 if (trimmed.length() < LargeInsertThreshold || !StartsWithCaseInsensitive(trimmed,
"INSERT INTO"))
290 return ExecuteSetupQuery(setupConnection, sql, queryContext, context);
292 std::string::size_type valuesPosition = FindKeywordOutsideQuotedText(trimmed,
"VALUES");
293 if (valuesPosition == std::string::npos)
294 return ExecuteSetupQuery(setupConnection, sql, queryContext, context);
296 std::string header = TrimCopy(trimmed.substr(0, valuesPosition + 6));
297 std::string values = TrimCopy(trimmed.substr(valuesPosition + 6));
298 std::vector<std::string> rows = SplitInsertRows(values);
300 if (rows.size() <= InsertChunkRows)
301 return ExecuteSetupQuery(setupConnection, sql, queryContext, context);
303 std::size_t chunkCount = (rows.size() + InsertChunkRows - 1) / InsertChunkRows;
304 SF_LOG_INFO(context.LogFilter,
"Large INSERT for %s database table `%s` has %u rows; executing %u chunks.",
305 context.DatabaseName, tableName.empty() ?
"unknown" : tableName.c_str(),
uint32(rows.size()),
308 std::uintmax_t statementEndByte = sqlContext.BytesRead;
309 std::uintmax_t statementStartByte = statementEndByte > sql.length() ? statementEndByte - sql.length() : 0;
310 std::uintmax_t statementBytes = statementEndByte > statementStartByte ? statementEndByte - statementStartByte : 0;
312 for (std::size_t chunkIndex = 0; chunkIndex < chunkCount; ++chunkIndex)
314 std::size_t begin = chunkIndex * InsertChunkRows;
315 std::size_t end = std::min(begin + InsertChunkRows, rows.size());
317 std::ostringstream chunk;
318 chunk << header <<
' ';
319 for (std::size_t rowIndex = begin; rowIndex < end; ++rowIndex)
321 if (rowIndex != begin)
324 chunk << rows[rowIndex];
327 std::ostringstream chunkContext;
328 chunkContext << queryContext <<
" chunk " << (chunkIndex + 1) <<
"/" << chunkCount;
330 if (!ExecuteSetupQuery(setupConnection, chunk.str(), chunkContext.str().c_str(), context))
333 std::uintmax_t chunkBytesRead = statementEndByte;
334 if (sqlContext.TotalBytes && statementBytes)
335 chunkBytesRead = statementStartByte + statementBytes * (chunkIndex + 1) / chunkCount;
338 if (sqlContext.TotalBytes)
340 std::uintmax_t percentValue = chunkBytesRead * 100 / sqlContext.TotalBytes;
341 percent =
uint32(percentValue > 100 ? 100 : percentValue);
344 SF_LOG_INFO(context.LogFilter,
"Import progress: %u%% - %s database table `%s` - chunk %u/%u.",
345 percent, context.DatabaseName, tableName.empty() ?
"unknown" : tableName.c_str(),
352 bool QuerySetupUInt32(MYSQL* setupConnection,
char const* sql,
uint32& value,
char const* queryContext,
355 if (mysql_query(setupConnection, sql))
357 SF_LOG_ERROR(context.LogFilter,
"%s: %s", queryContext, mysql_error(setupConnection));
361 MYSQL_RES* result = mysql_store_result(setupConnection);
364 SF_LOG_ERROR(context.LogFilter,
"%s: %s", queryContext, mysql_error(setupConnection));
368 std::unique_ptr<MYSQL_RES,
decltype(&mysql_free_result)> resultGuard(result, mysql_free_result);
369 MYSQL_ROW row = mysql_fetch_row(result);
372 SF_LOG_ERROR(context.LogFilter,
"%s returned no value.", queryContext);
376 value =
uint32(std::strtoul(row[0], NULL, 10));
385 SF_LOG_ERROR(context.LogFilter,
"%s database update %s has no content hash.",
386 context.DatabaseNameTitle, update.Name.c_str());
390 std::string queryContext =
"Could not record " + std::string(context.DatabaseName) +
" database update";
391 return ExecuteSetupQuery(setupConnection,
398 if (!RecordUpdateMetadata(setupConnection, options, update, hash, context))
401 std::string queryContext =
"Could not record " + std::string(context.DatabaseName) +
402 " database update audit row";
415 std::filesystem::path path = std::filesystem::path(options.
SqlPath) /
"base" / baseFileName;
416 path.make_preferred();
423 MYSQL* mysqlInit = mysql_init(NULL);
430 mysql_options(mysqlInit, MYSQL_SET_CHARSET_NAME,
"utf8");
433 char const* unixSocket = NULL;
434 std::string host = connectionInfo._host;
439 unsigned int protocol = MYSQL_PROTOCOL_PIPE;
440 mysql_options(mysqlInit, MYSQL_OPT_PROTOCOL,
reinterpret_cast<char const*
>(&protocol));
443 port = atoi(connectionInfo._port_or_socket.c_str());
447 unsigned int protocol = MYSQL_PROTOCOL_SOCKET;
448 mysql_options(mysqlInit, MYSQL_OPT_PROTOCOL,
reinterpret_cast<char const*
>(&protocol));
450 unixSocket = connectionInfo._port_or_socket.c_str();
453 port = atoi(connectionInfo._port_or_socket.c_str());
456 handle = mysql_real_connect(mysqlInit, host.c_str(), connectionInfo._user.c_str(),
457 connectionInfo._password.c_str(), databaseName, port, unixSocket, 0);
462 mysql_error(mysqlInit));
463 mysql_close(mysqlInit);
476 if (connectionInfo._database.empty())
483 MYSQL* setupConnection = NULL;
490 context.
DatabaseName, connectionInfo._database.c_str(), mysql_error(setupConnection));
491 mysql_close(setupConnection);
497 mysql_close(setupConnection);
506 std::string tableCountContext =
"Could not inspect " + std::string(context.
DatabaseName) +
507 " database table count";
508 if (!QuerySetupUInt32(setupConnection,
509 "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE()",
513 std::string trackingTableContext =
"Could not inspect " + std::string(context.
DatabaseName) +
514 " database update tracking table";
515 uint32 updateTrackingTableCount = 0;
516 if (!QuerySetupUInt32(setupConnection,
517 "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() "
518 "AND table_name = 'skyfire_db_updates'",
519 updateTrackingTableCount, trackingTableContext.c_str(), context))
526 std::string appliedQuery =
"SELECT `filename`, `hash` FROM `skyfire_db_updates` WHERE `domain` = '" +
528 if (mysql_query(setupConnection, appliedQuery.c_str()))
535 MYSQL_RES* result = mysql_store_result(setupConnection);
543 std::unique_ptr<MYSQL_RES,
decltype(&mysql_free_result)> resultGuard(result, mysql_free_result);
544 while (MYSQL_ROW row = mysql_fetch_row(result))
557 bool ExecuteSqlFile(MYSQL* setupConnection, std::filesystem::path
const& path, std::string& contents,
562 std::ifstream file(path, std::ios::in | std::ios::binary);
569 std::uintmax_t totalBytes = 0;
570 std::error_code fileSizeError;
571 totalBytes = std::filesystem::file_size(path, fileSizeError);
575 std::string fileName = path.filename().string();
576 std::string currentTable;
577 uint32 lastLoggedPercent = 0;
580 path.string().c_str(),
static_cast<unsigned long long>(totalBytes));
583 [setupConnection, &context, ¤tTable, &fileName, &lastLoggedPercent]
586 std::string detectedTable = ExtractSqlTableName(statement);
587 if (!detectedTable.empty() && detectedTable != currentTable)
589 currentTable = detectedTable;
591 context.
DatabaseName, currentTable.c_str(), fileName.c_str());
594 uint32 percent = CalculateSqlProgressPercent(sqlContext);
596 percent >= lastLoggedPercent + 5 || percent == 100)
598 lastLoggedPercent = percent;
600 "Import progress: %u%% - %s database table `%s` - %u statements.",
601 percent, context.
DatabaseName, currentTable.empty() ?
"unknown" : currentTable.c_str(),
605 std::ostringstream queryContext;
607 <<
" near byte " <<
static_cast<unsigned long long>(sqlContext.
BytesRead);
608 if (!currentTable.empty())
609 queryContext <<
" while importing table `" << currentTable <<
"`";
611 return ExecuteSetupQueryWithInsertChunks(setupConnection, statement, queryContext.str().c_str(),
612 context, currentTable, sqlContext);
636 "%s database update %s was already applied with a different hash; %s.AllowUpdateHashMismatch is enabled, skipping reapply.",
643 std::string trackingContext =
"Could not create " + std::string(context.
DatabaseName) +
644 " database update tracking table";
651 std::string auditContext =
"Could not create " + std::string(context.
DatabaseName) +
652 " database update audit table";
669 "%s.AutoBaseline is enabled. Recording %u %s updates as already applied without executing them.",
672 "Disable %s.AutoBaseline after this startup to keep future update checks strict.",
677 if (!RecordUpdateMetadata(setupConnection, options, update, update.
Hash, context))
693 std::string updateSql;
699 if (!RecordAppliedUpdate(setupConnection, options, update, update.
Hash, context))
#define SF_LOG_WARN(filterType__,...)
#define SF_LOG_ERROR(filterType__,...)
#define SF_LOG_INFO(filterType__,...)
bool EnsureDatabaseExists(MySQLConnectionInfo const &connectionInfo, SetupOptions const &options, SetupRuntimeContext const &context)
std::string EscapeSqlString(std::string const &value)
std::string BuildUpdateTrackingInsertSql(std::string const &domain, std::string const &filename, std::string const &hash)
bool LoadDatabaseSetupState(MYSQL *setupConnection, SetupOptions const &options, SetupState &state, SetupRuntimeContext const &context)
bool ConnectToMySQLServer(MySQLConnectionInfo const &connectionInfo, char const *databaseName, MYSQL *&handle, SetupRuntimeContext const &context)
std::string BuildDbUpdateAuditTableSql()
bool ExecuteSqlFile(MYSQL *setupConnection, std::filesystem::path const &path, std::string &contents, SetupRuntimeContext const &context)
bool ExecuteSqlStream(std::istream &input, std::uintmax_t totalBytes, std::function< bool(std::string const &, SqlStatementContext const &)> const &executor)
std::string BuildCreateDatabaseSql(std::string const &databaseName)
bool BaselineSetupUpdates(MYSQL *setupConnection, SetupOptions const &options, SetupPlan const &plan, SetupRuntimeContext const &context)
std::filesystem::path GetDatabaseBaseSqlPath(SetupOptions const &options)
void LogSetupPlan(SetupPlan const &plan, std::size_t discoveredUpdateCount, bool appliesRequiredSql, SetupRuntimeContext const &context)
std::string BuildDbUpdateAuditInsertSql(std::string const &filename)
bool EnsureSetupTrackingTables(MYSQL *setupConnection, SetupRuntimeContext const &context)
bool ApplyPendingSetupUpdates(MYSQL *setupConnection, SetupOptions const &options, SetupPlan const &plan, SetupRuntimeContext const &context)
std::string BuildSetupPlanSummary(std::string const &databaseName, SetupPlan const &plan, std::size_t discoveredUpdateCount, bool appliesRequiredSql)
std::string BuildUpdateTrackingTableSql()
std::vector< SqlUpdateFile > PendingUpdates
std::vector< SqlUpdateFile > BaselineUpdates
bool ShouldBaselineUpdates
std::vector< SqlUpdateFile > HashMismatchedUpdates
char const * DatabaseNameWithArticle
char const * DatabaseName
char const * DatabaseNameTitle
char const * ConfigPrefix
char const * SqlExecutionContext
bool UpdateTrackingExists
std::set< std::string > AppliedUpdates
std::map< std::string, std::string > AppliedUpdateHashes
unsigned int SchemaTableCount
std::size_t StatementCount