24 bool EndsWithSqlExtension(std::string
const& name)
26 if (name.length() < 4)
29 std::string extension = name.substr(name.length() - 4);
30 std::transform(extension.begin(), extension.end(), extension.begin(),
31 [](
unsigned char c) { return char(std::tolower(c)); });
33 return extension ==
".sql";
36 std::string BuildSqlPath(std::string
const& directory, std::string
const& name)
38 if (directory.empty())
41 char last = directory[directory.length() - 1];
42 if (last ==
'/' || last ==
'\\')
43 return directory + name;
45 char separator = directory.find(
'\\') != std::string::npos && directory.find(
'/') == std::string::npos ?
'\\' :
'/';
46 return directory + separator + name;
49 std::string Trim(std::string
const& text)
51 std::string::size_type begin = 0;
52 while (begin < text.length() && std::isspace(
static_cast<unsigned char>(text[begin])))
55 std::string::size_type end = text.length();
56 while (end > begin && std::isspace(
static_cast<unsigned char>(text[end - 1])))
59 return text.substr(begin, end - begin);
62 bool ReadTextFile(std::filesystem::path
const& path, std::string& contents)
64 std::ifstream file(path, std::ios::in | std::ios::binary);
68 std::ostringstream stream;
69 stream << file.rdbuf();
70 contents = stream.str();
74 bool StartsWithCaseInsensitive(std::string
const& text, std::string
const& prefix)
76 if (text.length() < prefix.length())
79 for (std::string::size_type i = 0; i < prefix.length(); ++i)
81 if (std::tolower(
static_cast<unsigned char>(text[i])) !=
82 std::tolower(
static_cast<unsigned char>(prefix[i])))
89 bool TryReadDelimiterCommand(std::string
const& line, std::string& delimiter)
91 std::string trimmed = Trim(line);
92 if (!StartsWithCaseInsensitive(trimmed,
"delimiter"))
95 if (trimmed.length() != 9 && !std::isspace(
static_cast<unsigned char>(trimmed[9])))
98 delimiter = Trim(trimmed.substr(9));
99 return !delimiter.empty();
102 std::string::size_type FindDelimiterOutsideQuotedText(std::string
const& sql, std::string
const& delimiter)
104 bool inSingleQuote =
false;
105 bool inDoubleQuote =
false;
106 bool inBacktick =
false;
107 bool escaped =
false;
109 for (std::string::size_type i = 0; i < sql.length(); ++i)
120 inSingleQuote =
false;
132 inDoubleQuote =
false;
145 if (c ==
'-' && i + 1 < sql.length() && sql[i + 1] ==
'-' &&
146 (i + 2 == sql.length() || std::isspace(
static_cast<unsigned char>(sql[i + 2]))))
149 while (i < sql.length() && sql[i] !=
'\n')
157 while (i < sql.length() && sql[i] !=
'\n')
163 if (c ==
'/' && i + 1 < sql.length() && sql[i + 1] ==
'*')
166 while (i + 1 < sql.length() && !(sql[i] ==
'*' && sql[i + 1] ==
'/'))
169 if (i + 1 < sql.length())
175 if (!delimiter.empty() && sql.compare(i, delimiter.length(), delimiter) == 0)
179 inSingleQuote =
true;
181 inDoubleQuote =
true;
186 return std::string::npos;
192 return Error.empty();
207 options.
SqlPath = std::move(sqlPath);
226 options.
Domain =
"characters";
227 options.
SqlPath = std::move(sqlPath);
236 std::string externalBaseFile)
239 std::move(externalBaseFile));
243 std::string externalBaseFile)
250 options.
SqlPath = std::move(sqlPath);
261 std::vector<SqlUpdateFile> updates;
262 updates.reserve(names.size());
264 for (std::string
const& name : names)
266 if (!EndsWithSqlExtension(name))
269 updates.push_back({ name, BuildSqlPath(directory, name) });
274 return left.Name < right.Name;
285 auto discoverFromDirectory = [](std::filesystem::path directory) -> std::vector<SqlUpdateFile>
287 directory.make_preferred();
288 if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory))
291 std::vector<std::string> names;
292 for (std::filesystem::directory_entry
const& entry : std::filesystem::directory_iterator(directory))
294 if (entry.is_regular_file())
295 names.push_back(entry.path().filename().string());
301 std::string contents;
302 if (ReadTextFile(update.Path, contents))
309 std::vector<SqlUpdateFile> updates =
315 std::vector<SqlUpdateFile> pendingUpdates =
317 if (pendingUpdates.empty())
320 std::set<std::string> knownNames;
322 knownNames.insert(update.Name);
326 if (!knownNames.insert(update.Name).second)
329 updates.push_back(std::move(update));
334 return left.Name < right.Name;
342 std::vector<std::string> statements;
344 std::string delimiter =
";";
345 std::istringstream input(sql);
348 while (std::getline(input, line))
350 std::string newDelimiter;
351 if (Trim(current).empty() && TryReadDelimiterCommand(line, newDelimiter))
354 delimiter = newDelimiter;
359 current.push_back(
'\n');
362 std::string::size_type delimiterPosition = FindDelimiterOutsideQuotedText(current, delimiter);
363 if (delimiterPosition == std::string::npos)
366 std::string statement = Trim(current.substr(0, delimiterPosition));
367 if (!statement.empty())
368 statements.push_back(statement);
370 current.erase(0, delimiterPosition + delimiter.length());
374 std::string statement = Trim(current);
375 if (!statement.empty())
376 statements.push_back(statement);
381 bool ExecuteSqlScript(std::string
const& sql, std::function<
bool(std::string
const&)>
const& executor)
383 std::istringstream input(sql);
387 return executor(statement);
395 std::string delimiter =
";";
400 while (std::getline(input, line))
404 std::string newDelimiter;
405 if (Trim(current).empty() && TryReadDelimiterCommand(line, newDelimiter))
408 delimiter = newDelimiter;
413 current.push_back(
'\n');
416 std::string::size_type delimiterPosition = FindDelimiterOutsideQuotedText(current, delimiter);
417 if (delimiterPosition == std::string::npos)
420 std::string statement = Trim(current.substr(0, delimiterPosition));
421 if (!statement.empty())
424 if (!executor(statement, context))
428 current.erase(0, delimiterPosition + delimiter.length());
432 std::string statement = Trim(current);
433 if (!statement.empty())
436 if (!executor(statement, context))
445 std::uint64_t hash = 14695981039346656037ull;
447 for (
unsigned char c : sql)
450 hash *= 1099511628211ull;
453 std::ostringstream stream;
454 stream << std::hex << std::setfill(
'0') << std::setw(16) << hash;
461 escaped.reserve(value.length());
465 if (c ==
'\'' || c ==
'\\')
466 escaped.push_back(
'\\');
468 escaped.push_back(c);
477 escaped.reserve(identifier.length());
479 for (
char c : identifier)
482 escaped.push_back(
'`');
484 escaped.push_back(c);
493 "` DEFAULT CHARACTER SET utf8";
498 return "CREATE TABLE IF NOT EXISTS `skyfire_db_updates` ("
499 "`domain` varchar(32) NOT NULL,"
500 "`filename` varchar(255) NOT NULL,"
501 "`hash` varchar(64) NOT NULL,"
502 "`applied_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,"
503 "PRIMARY KEY (`domain`,`filename`)"
504 ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3";
508 std::string
const& hash)
510 return "INSERT INTO `skyfire_db_updates` (`domain`, `filename`, `hash`, `applied_at`) VALUES ('" +
517 return "CREATE TABLE IF NOT EXISTS `db_update` ("
518 "`date` date NOT NULL,"
519 "`time` time NOT NULL,"
520 "`filename` varchar(255) NOT NULL"
521 ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3";
526 return "INSERT INTO `db_update` (`date`, `time`, `filename`) VALUES (CURDATE(), CURTIME(), '" +
531 std::size_t discoveredUpdateCount,
bool appliesRequiredSql)
533 char const* mode =
"check-only";
535 mode =
"install-base";
539 mode =
"apply-updates";
541 std::ostringstream stream;
542 stream << databaseName <<
" database setup plan: mode=" << mode
543 <<
", discovered updates=" << discoveredUpdateCount
548 <<
", required SQL=" << (appliesRequiredSql ?
"yes" :
"no")
555 std::vector<SqlUpdateFile>
const& updates)
563 plan.
Error = options.
Domain +
" database does not exist and auto setup is disabled.";
569 plan.
Error = options.
Domain +
" database does not exist and auto create is disabled.";
580 plan.
Error = options.
Domain +
" database is empty and auto setup is disabled.";
589 plan.
Error = options.
Domain +
" database base SQL file was not found.";
597 plan.
Error = options.
Domain +
" database update tracking table is missing on a non-empty schema.";
605 if (update.
Hash.empty())
609 plan.
Error = options.
Domain +
" database update `" + update.
Name +
"` has no content hash for baseline.";
625 std::map<std::string, std::string>::const_iterator appliedHash = state.
AppliedUpdateHashes.find(update.Name);
627 !update.Hash.empty() && appliedHash->second != update.Hash)
635 plan.
Error = options.
Domain +
" database update `" + update.Name +
"` was already applied with a different hash.";
645 std::vector<SqlUpdateFile>
const& updates)
651 std::vector<SqlUpdateFile>
const& updates)
657 bool requiredBaseSqlExists, std::vector<SqlUpdateFile>
const& updates)
665 plan.
Error =
"world database external base SQL file was not found.";
671 plan.
Error =
"world database required base SQL file was not found.";
std::vector< SqlUpdateFile > DiscoverSqlUpdates(SetupOptions const &options)
SetupPlan BuildWorldDatabaseSetupPlan(SetupOptions const &options, SetupState const &state, bool externalBaseSqlExists, bool requiredBaseSqlExists, std::vector< SqlUpdateFile > const &updates)
std::vector< std::string > SplitSqlStatements(std::string const &sql)
std::string EscapeSqlString(std::string const &value)
std::string BuildUpdateTrackingInsertSql(std::string const &domain, std::string const &filename, std::string const &hash)
SetupPlan BuildDatabaseSetupPlan(SetupOptions const &options, SetupState const &state, bool baseSqlExists, std::vector< SqlUpdateFile > const &updates)
std::string BuildDbUpdateAuditTableSql()
bool ExecuteSqlScript(std::string const &sql, std::function< bool(std::string const &)> const &executor)
SetupOptions MakeWorldDatabaseSetupOptions(bool autoSetup, bool autoCreate, std::string sqlPath, std::string externalBaseFile)
bool ExecuteSqlStream(std::istream &input, std::uintmax_t totalBytes, std::function< bool(std::string const &, SqlStatementContext const &)> const &executor)
SetupOptions MakeCharacterDatabaseSetupOptions(bool autoSetup, bool autoCreate, std::string sqlPath)
SetupPlan BuildCharacterDatabaseSetupPlan(SetupOptions const &options, SetupState const &state, bool baseSqlExists, std::vector< SqlUpdateFile > const &updates)
std::string BuildCreateDatabaseSql(std::string const &databaseName)
std::vector< SqlUpdateFile > BuildSortedSqlUpdateList(std::vector< std::string > const &names, std::string const &directory)
std::string BuildDbUpdateAuditInsertSql(std::string const &filename)
SetupPlan BuildAuthDatabaseSetupPlan(SetupOptions const &options, SetupState const &state, bool baseSqlExists, std::vector< SqlUpdateFile > const &updates)
std::string EscapeSqlIdentifier(std::string const &identifier)
std::string CalculateStableSqlHash(std::string const &sql)
SetupOptions MakeAuthDatabaseSetupOptions(bool autoSetup, bool autoCreate, std::string sqlPath)
std::string BuildSetupPlanSummary(std::string const &databaseName, SetupPlan const &plan, std::size_t discoveredUpdateCount, bool appliesRequiredSql)
std::string BuildUpdateTrackingTableSql()
std::vector< std::string > RequiredBaseFileNames
std::string PendingUpdatesDirectory
std::string ExternalBaseFile
bool AllowUpdateHashMismatch
bool ImportPendingUpdates
std::string UpdatesDirectory
std::vector< SqlUpdateFile > PendingUpdates
std::vector< SqlUpdateFile > BaselineUpdates
bool ShouldBaselineUpdates
std::vector< SqlUpdateFile > HashMismatchedUpdates
bool ShouldCreateDatabase
bool UpdateTrackingExists
std::set< std::string > AppliedUpdates
std::map< std::string, std::string > AppliedUpdateHashes
unsigned int SchemaTableCount
std::uintmax_t TotalBytes
std::size_t StatementCount