Project SkyFire Core
SkyFire 5.4.8 server core API documentation
Loading...
Searching...
No Matches
DatabaseSetup.cpp
Go to the documentation of this file.
1/*
2* This file is part of Project SkyFire https://www.projectskyfire.org.
3* See LICENSE.md file for Copyright information
4*/
5
6#include "DatabaseSetup.h"
7
8#include <algorithm>
9#include <cctype>
10#include <cstdint>
11#include <filesystem>
12#include <fstream>
13#include <iomanip>
14#include <set>
15#include <sstream>
16#include <utility>
17
18namespace Skyfire
19{
20namespace Database
21{
22 namespace
23 {
24 bool EndsWithSqlExtension(std::string const& name)
25 {
26 if (name.length() < 4)
27 return false;
28
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)); });
32
33 return extension == ".sql";
34 }
35
36 std::string BuildSqlPath(std::string const& directory, std::string const& name)
37 {
38 if (directory.empty())
39 return name;
40
41 char last = directory[directory.length() - 1];
42 if (last == '/' || last == '\\')
43 return directory + name;
44
45 char separator = directory.find('\\') != std::string::npos && directory.find('/') == std::string::npos ? '\\' : '/';
46 return directory + separator + name;
47 }
48
49 std::string Trim(std::string const& text)
50 {
51 std::string::size_type begin = 0;
52 while (begin < text.length() && std::isspace(static_cast<unsigned char>(text[begin])))
53 ++begin;
54
55 std::string::size_type end = text.length();
56 while (end > begin && std::isspace(static_cast<unsigned char>(text[end - 1])))
57 --end;
58
59 return text.substr(begin, end - begin);
60 }
61
62 bool ReadTextFile(std::filesystem::path const& path, std::string& contents)
63 {
64 std::ifstream file(path, std::ios::in | std::ios::binary);
65 if (!file)
66 return false;
67
68 std::ostringstream stream;
69 stream << file.rdbuf();
70 contents = stream.str();
71 return true;
72 }
73
74 bool StartsWithCaseInsensitive(std::string const& text, std::string const& prefix)
75 {
76 if (text.length() < prefix.length())
77 return false;
78
79 for (std::string::size_type i = 0; i < prefix.length(); ++i)
80 {
81 if (std::tolower(static_cast<unsigned char>(text[i])) !=
82 std::tolower(static_cast<unsigned char>(prefix[i])))
83 return false;
84 }
85
86 return true;
87 }
88
89 bool TryReadDelimiterCommand(std::string const& line, std::string& delimiter)
90 {
91 std::string trimmed = Trim(line);
92 if (!StartsWithCaseInsensitive(trimmed, "delimiter"))
93 return false;
94
95 if (trimmed.length() != 9 && !std::isspace(static_cast<unsigned char>(trimmed[9])))
96 return false;
97
98 delimiter = Trim(trimmed.substr(9));
99 return !delimiter.empty();
100 }
101
102 std::string::size_type FindDelimiterOutsideQuotedText(std::string const& sql, std::string const& delimiter)
103 {
104 bool inSingleQuote = false;
105 bool inDoubleQuote = false;
106 bool inBacktick = false;
107 bool escaped = false;
108
109 for (std::string::size_type i = 0; i < sql.length(); ++i)
110 {
111 char c = sql[i];
112
113 if (inSingleQuote)
114 {
115 if (escaped)
116 escaped = false;
117 else if (c == '\\')
118 escaped = true;
119 else if (c == '\'')
120 inSingleQuote = false;
121
122 continue;
123 }
124
125 if (inDoubleQuote)
126 {
127 if (escaped)
128 escaped = false;
129 else if (c == '\\')
130 escaped = true;
131 else if (c == '"')
132 inDoubleQuote = false;
133
134 continue;
135 }
136
137 if (inBacktick)
138 {
139 if (c == '`')
140 inBacktick = false;
141
142 continue;
143 }
144
145 if (c == '-' && i + 1 < sql.length() && sql[i + 1] == '-' &&
146 (i + 2 == sql.length() || std::isspace(static_cast<unsigned char>(sql[i + 2]))))
147 {
148 i += 2;
149 while (i < sql.length() && sql[i] != '\n')
150 ++i;
151
152 continue;
153 }
154
155 if (c == '#')
156 {
157 while (i < sql.length() && sql[i] != '\n')
158 ++i;
159
160 continue;
161 }
162
163 if (c == '/' && i + 1 < sql.length() && sql[i + 1] == '*')
164 {
165 i += 2;
166 while (i + 1 < sql.length() && !(sql[i] == '*' && sql[i + 1] == '/'))
167 ++i;
168
169 if (i + 1 < sql.length())
170 ++i;
171
172 continue;
173 }
174
175 if (!delimiter.empty() && sql.compare(i, delimiter.length(), delimiter) == 0)
176 return i;
177
178 if (c == '\'')
179 inSingleQuote = true;
180 else if (c == '"')
181 inDoubleQuote = true;
182 else if (c == '`')
183 inBacktick = true;
184 }
185
186 return std::string::npos;
187 }
188 }
189
191 {
192 return Error.empty();
193 }
194
195 SetupOptions MakeAuthDatabaseSetupOptions(bool autoSetup, bool autoCreate, std::string sqlPath)
196 {
197 return MakeAuthDatabaseSetupOptions(autoSetup, autoCreate, false, std::move(sqlPath));
198 }
199
200 SetupOptions MakeAuthDatabaseSetupOptions(bool autoSetup, bool autoCreate, bool autoBaseline, std::string sqlPath)
201 {
202 SetupOptions options;
203 options.AutoSetup = autoSetup;
204 options.AutoCreate = autoCreate;
205 options.AutoBaseline = autoBaseline;
206 options.Domain = "auth";
207 options.SqlPath = std::move(sqlPath);
208 options.BaseFileName = "auth_database.sql";
209 options.UpdatesDirectory = "updates/auth";
210 options.PendingUpdatesDirectory = "pending_updates/auth";
211
212 return options;
213 }
214
215 SetupOptions MakeCharacterDatabaseSetupOptions(bool autoSetup, bool autoCreate, std::string sqlPath)
216 {
217 return MakeCharacterDatabaseSetupOptions(autoSetup, autoCreate, false, std::move(sqlPath));
218 }
219
220 SetupOptions MakeCharacterDatabaseSetupOptions(bool autoSetup, bool autoCreate, bool autoBaseline, std::string sqlPath)
221 {
222 SetupOptions options;
223 options.AutoSetup = autoSetup;
224 options.AutoCreate = autoCreate;
225 options.AutoBaseline = autoBaseline;
226 options.Domain = "characters";
227 options.SqlPath = std::move(sqlPath);
228 options.BaseFileName = "characters_database.sql";
229 options.UpdatesDirectory = "updates/characters";
230 options.PendingUpdatesDirectory = "pending_updates/characters";
231
232 return options;
233 }
234
235 SetupOptions MakeWorldDatabaseSetupOptions(bool autoSetup, bool autoCreate, std::string sqlPath,
236 std::string externalBaseFile)
237 {
238 return MakeWorldDatabaseSetupOptions(autoSetup, autoCreate, false, std::move(sqlPath),
239 std::move(externalBaseFile));
240 }
241
242 SetupOptions MakeWorldDatabaseSetupOptions(bool autoSetup, bool autoCreate, bool autoBaseline, std::string sqlPath,
243 std::string externalBaseFile)
244 {
245 SetupOptions options;
246 options.AutoSetup = autoSetup;
247 options.AutoCreate = autoCreate;
248 options.AutoBaseline = autoBaseline;
249 options.Domain = "world";
250 options.SqlPath = std::move(sqlPath);
251 options.ExternalBaseFile = std::move(externalBaseFile);
252 options.RequiredBaseFileNames.push_back("stored_procs.sql");
253 options.UpdatesDirectory = "updates/world";
254 options.PendingUpdatesDirectory = "pending_updates/world";
255
256 return options;
257 }
258
259 std::vector<SqlUpdateFile> BuildSortedSqlUpdateList(std::vector<std::string> const& names, std::string const& directory)
260 {
261 std::vector<SqlUpdateFile> updates;
262 updates.reserve(names.size());
263
264 for (std::string const& name : names)
265 {
266 if (!EndsWithSqlExtension(name))
267 continue;
268
269 updates.push_back({ name, BuildSqlPath(directory, name) });
270 }
271
272 std::sort(updates.begin(), updates.end(), [](SqlUpdateFile const& left, SqlUpdateFile const& right)
273 {
274 return left.Name < right.Name;
275 });
276
277 return updates;
278 }
279
280 std::vector<SqlUpdateFile> DiscoverSqlUpdates(SetupOptions const& options)
281 {
282 if (options.SqlPath.empty())
283 return {};
284
285 auto discoverFromDirectory = [](std::filesystem::path directory) -> std::vector<SqlUpdateFile>
286 {
287 directory.make_preferred();
288 if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory))
289 return {};
290
291 std::vector<std::string> names;
292 for (std::filesystem::directory_entry const& entry : std::filesystem::directory_iterator(directory))
293 {
294 if (entry.is_regular_file())
295 names.push_back(entry.path().filename().string());
296 }
297
298 std::vector<SqlUpdateFile> updates = BuildSortedSqlUpdateList(names, directory.string());
299 for (SqlUpdateFile& update : updates)
300 {
301 std::string contents;
302 if (ReadTextFile(update.Path, contents))
303 update.Hash = CalculateStableSqlHash(contents);
304 }
305
306 return updates;
307 };
308
309 std::vector<SqlUpdateFile> updates =
310 discoverFromDirectory(std::filesystem::path(options.SqlPath) / options.UpdatesDirectory);
311
312 if (!options.ImportPendingUpdates || options.PendingUpdatesDirectory.empty())
313 return updates;
314
315 std::vector<SqlUpdateFile> pendingUpdates =
316 discoverFromDirectory(std::filesystem::path(options.SqlPath) / options.PendingUpdatesDirectory);
317 if (pendingUpdates.empty())
318 return updates;
319
320 std::set<std::string> knownNames;
321 for (SqlUpdateFile const& update : updates)
322 knownNames.insert(update.Name);
323
324 for (SqlUpdateFile& update : pendingUpdates)
325 {
326 if (!knownNames.insert(update.Name).second)
327 continue;
328
329 updates.push_back(std::move(update));
330 }
331
332 std::sort(updates.begin(), updates.end(), [](SqlUpdateFile const& left, SqlUpdateFile const& right)
333 {
334 return left.Name < right.Name;
335 });
336
337 return updates;
338 }
339
340 std::vector<std::string> SplitSqlStatements(std::string const& sql)
341 {
342 std::vector<std::string> statements;
343 std::string current;
344 std::string delimiter = ";";
345 std::istringstream input(sql);
346 std::string line;
347
348 while (std::getline(input, line))
349 {
350 std::string newDelimiter;
351 if (Trim(current).empty() && TryReadDelimiterCommand(line, newDelimiter))
352 {
353 current.clear();
354 delimiter = newDelimiter;
355 continue;
356 }
357
358 current += line;
359 current.push_back('\n');
360 while (true)
361 {
362 std::string::size_type delimiterPosition = FindDelimiterOutsideQuotedText(current, delimiter);
363 if (delimiterPosition == std::string::npos)
364 break;
365
366 std::string statement = Trim(current.substr(0, delimiterPosition));
367 if (!statement.empty())
368 statements.push_back(statement);
369
370 current.erase(0, delimiterPosition + delimiter.length());
371 }
372 }
373
374 std::string statement = Trim(current);
375 if (!statement.empty())
376 statements.push_back(statement);
377
378 return statements;
379 }
380
381 bool ExecuteSqlScript(std::string const& sql, std::function<bool(std::string const&)> const& executor)
382 {
383 std::istringstream input(sql);
384 return ExecuteSqlStream(input, sql.length(),
385 [&executor](std::string const& statement, SqlStatementContext const&)
386 {
387 return executor(statement);
388 });
389 }
390
391 bool ExecuteSqlStream(std::istream& input, std::uintmax_t totalBytes,
392 std::function<bool(std::string const&, SqlStatementContext const&)> const& executor)
393 {
394 std::string current;
395 std::string delimiter = ";";
396 std::string line;
397 SqlStatementContext context;
398 context.TotalBytes = totalBytes;
399
400 while (std::getline(input, line))
401 {
402 context.BytesRead += line.length() + 1;
403
404 std::string newDelimiter;
405 if (Trim(current).empty() && TryReadDelimiterCommand(line, newDelimiter))
406 {
407 current.clear();
408 delimiter = newDelimiter;
409 continue;
410 }
411
412 current += line;
413 current.push_back('\n');
414 while (true)
415 {
416 std::string::size_type delimiterPosition = FindDelimiterOutsideQuotedText(current, delimiter);
417 if (delimiterPosition == std::string::npos)
418 break;
419
420 std::string statement = Trim(current.substr(0, delimiterPosition));
421 if (!statement.empty())
422 {
423 ++context.StatementCount;
424 if (!executor(statement, context))
425 return false;
426 }
427
428 current.erase(0, delimiterPosition + delimiter.length());
429 }
430 }
431
432 std::string statement = Trim(current);
433 if (!statement.empty())
434 {
435 ++context.StatementCount;
436 if (!executor(statement, context))
437 return false;
438 }
439
440 return true;
441 }
442
443 std::string CalculateStableSqlHash(std::string const& sql)
444 {
445 std::uint64_t hash = 14695981039346656037ull;
446
447 for (unsigned char c : sql)
448 {
449 hash ^= c;
450 hash *= 1099511628211ull;
451 }
452
453 std::ostringstream stream;
454 stream << std::hex << std::setfill('0') << std::setw(16) << hash;
455 return stream.str();
456 }
457
458 std::string EscapeSqlString(std::string const& value)
459 {
460 std::string escaped;
461 escaped.reserve(value.length());
462
463 for (char c : value)
464 {
465 if (c == '\'' || c == '\\')
466 escaped.push_back('\\');
467
468 escaped.push_back(c);
469 }
470
471 return escaped;
472 }
473
474 std::string EscapeSqlIdentifier(std::string const& identifier)
475 {
476 std::string escaped;
477 escaped.reserve(identifier.length());
478
479 for (char c : identifier)
480 {
481 if (c == '`')
482 escaped.push_back('`');
483
484 escaped.push_back(c);
485 }
486
487 return escaped;
488 }
489
490 std::string BuildCreateDatabaseSql(std::string const& databaseName)
491 {
492 return "CREATE DATABASE IF NOT EXISTS `" + EscapeSqlIdentifier(databaseName) +
493 "` DEFAULT CHARACTER SET utf8";
494 }
495
497 {
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";
505 }
506
507 std::string BuildUpdateTrackingInsertSql(std::string const& domain, std::string const& filename,
508 std::string const& hash)
509 {
510 return "INSERT INTO `skyfire_db_updates` (`domain`, `filename`, `hash`, `applied_at`) VALUES ('" +
511 EscapeSqlString(domain) + "', '" + EscapeSqlString(filename) + "', '" + EscapeSqlString(hash) +
512 "', NOW())";
513 }
514
516 {
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";
522 }
523
524 std::string BuildDbUpdateAuditInsertSql(std::string const& filename)
525 {
526 return "INSERT INTO `db_update` (`date`, `time`, `filename`) VALUES (CURDATE(), CURTIME(), '" +
527 EscapeSqlString(filename) + "')";
528 }
529
530 std::string BuildSetupPlanSummary(std::string const& databaseName, SetupPlan const& plan,
531 std::size_t discoveredUpdateCount, bool appliesRequiredSql)
532 {
533 char const* mode = "check-only";
534 if (plan.ShouldInstallBase)
535 mode = "install-base";
536 else if (plan.ShouldBaselineUpdates)
537 mode = "baseline";
538 else if (!plan.PendingUpdates.empty())
539 mode = "apply-updates";
540
541 std::ostringstream stream;
542 stream << databaseName << " database setup plan: mode=" << mode
543 << ", discovered updates=" << discoveredUpdateCount
544 << ", pending updates=" << plan.PendingUpdates.size()
545 << ", baseline updates=" << plan.BaselineUpdates.size()
546 << ", hash mismatch bypasses=" << plan.HashMismatchedUpdates.size()
547 << ", install base=" << (plan.ShouldInstallBase ? "yes" : "no")
548 << ", required SQL=" << (appliesRequiredSql ? "yes" : "no")
549 << ".";
550
551 return stream.str();
552 }
553
554 SetupPlan BuildDatabaseSetupPlan(SetupOptions const& options, SetupState const& state, bool baseSqlExists,
555 std::vector<SqlUpdateFile> const& updates)
556 {
557 SetupPlan plan;
558
559 if (!state.DatabaseExists)
560 {
561 if (!options.AutoSetup)
562 {
563 plan.Error = options.Domain + " database does not exist and auto setup is disabled.";
564 return plan;
565 }
566
567 if (!options.AutoCreate)
568 {
569 plan.Error = options.Domain + " database does not exist and auto create is disabled.";
570 return plan;
571 }
572
573 plan.ShouldCreateDatabase = true;
574 plan.ShouldInstallBase = true;
575 }
576 else if (state.SchemaTableCount == 0)
577 {
578 if (!options.AutoSetup)
579 {
580 plan.Error = options.Domain + " database is empty and auto setup is disabled.";
581 return plan;
582 }
583
584 plan.ShouldInstallBase = true;
585 }
586
587 if (plan.ShouldInstallBase && !baseSqlExists)
588 {
589 plan.Error = options.Domain + " database base SQL file was not found.";
590 return plan;
591 }
592
593 if (!plan.ShouldInstallBase && !state.UpdateTrackingExists && !updates.empty())
594 {
595 if (!options.AutoBaseline)
596 {
597 plan.Error = options.Domain + " database update tracking table is missing on a non-empty schema.";
598 return plan;
599 }
600
601 plan.ShouldBaselineUpdates = true;
602 plan.BaselineUpdates = updates;
603 for (SqlUpdateFile const& update : plan.BaselineUpdates)
604 {
605 if (update.Hash.empty())
606 {
607 plan.ShouldBaselineUpdates = false;
608 plan.BaselineUpdates.clear();
609 plan.Error = options.Domain + " database update `" + update.Name + "` has no content hash for baseline.";
610 return plan;
611 }
612 }
613
614 return plan;
615 }
616
617 for (SqlUpdateFile const& update : updates)
618 {
619 if (state.AppliedUpdates.find(update.Name) == state.AppliedUpdates.end())
620 {
621 plan.PendingUpdates.push_back(update);
622 continue;
623 }
624
625 std::map<std::string, std::string>::const_iterator appliedHash = state.AppliedUpdateHashes.find(update.Name);
626 if (appliedHash != state.AppliedUpdateHashes.end() && !appliedHash->second.empty() &&
627 !update.Hash.empty() && appliedHash->second != update.Hash)
628 {
629 if (options.AllowUpdateHashMismatch)
630 {
631 plan.HashMismatchedUpdates.push_back(update);
632 continue;
633 }
634
635 plan.Error = options.Domain + " database update `" + update.Name + "` was already applied with a different hash.";
636 plan.PendingUpdates.clear();
637 return plan;
638 }
639 }
640
641 return plan;
642 }
643
644 SetupPlan BuildAuthDatabaseSetupPlan(SetupOptions const& options, SetupState const& state, bool baseSqlExists,
645 std::vector<SqlUpdateFile> const& updates)
646 {
647 return BuildDatabaseSetupPlan(options, state, baseSqlExists, updates);
648 }
649
650 SetupPlan BuildCharacterDatabaseSetupPlan(SetupOptions const& options, SetupState const& state, bool baseSqlExists,
651 std::vector<SqlUpdateFile> const& updates)
652 {
653 return BuildDatabaseSetupPlan(options, state, baseSqlExists, updates);
654 }
655
656 SetupPlan BuildWorldDatabaseSetupPlan(SetupOptions const& options, SetupState const& state, bool externalBaseSqlExists,
657 bool requiredBaseSqlExists, std::vector<SqlUpdateFile> const& updates)
658 {
659 SetupPlan plan = BuildDatabaseSetupPlan(options, state, true, updates);
660 if (!plan.IsValid())
661 return plan;
662
663 if (plan.ShouldInstallBase && (options.ExternalBaseFile.empty() || !externalBaseSqlExists))
664 {
665 plan.Error = "world database external base SQL file was not found.";
666 return plan;
667 }
668
669 if (plan.ShouldInstallBase && !requiredBaseSqlExists)
670 {
671 plan.Error = "world database required base SQL file was not found.";
672 return plan;
673 }
674
675 return plan;
676 }
677}
678}
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::vector< SqlUpdateFile > PendingUpdates
std::vector< SqlUpdateFile > BaselineUpdates
std::vector< SqlUpdateFile > HashMismatchedUpdates
std::set< std::string > AppliedUpdates
std::map< std::string, std::string > AppliedUpdateHashes