metaforce/hecl/lib/Project.cpp

506 lines
15 KiB
C++
Raw Normal View History

2015-05-21 02:33:05 +00:00
#include <sys/stat.h>
#include <algorithm>
2017-12-29 07:56:31 +00:00
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <string>
2015-05-21 02:33:05 +00:00
#include <system_error>
2015-06-10 23:34:14 +00:00
#if _WIN32
#else
#include <unistd.h>
#endif
#include "hecl/ClientProcess.hpp"
2016-03-04 23:02:44 +00:00
#include "hecl/Database.hpp"
2017-12-29 07:56:31 +00:00
#include "hecl/Blender/Connection.hpp"
2018-03-23 21:40:12 +00:00
#include "hecl/MultiProgressPrinter.hpp"
2015-05-21 02:33:05 +00:00
#include <logvisor/logvisor.hpp>
2018-12-08 05:18:42 +00:00
namespace hecl::Database {
2015-05-21 02:33:05 +00:00
2016-09-25 01:57:43 +00:00
logvisor::Module LogModule("hecl::Database");
constexpr hecl::FourCC HECLfcc("HECL");
2015-06-09 23:21:45 +00:00
/**********************************************
* Project::ConfigFile
**********************************************/
2019-07-20 04:22:58 +00:00
static bool CheckNewLineAdvance(std::string::const_iterator& it) {
2018-12-08 05:18:42 +00:00
if (*it == '\n') {
it += 1;
return true;
} else if (*it == '\r') {
if (*(it + 1) == '\n') {
it += 2;
return true;
2015-06-09 22:19:59 +00:00
}
2018-12-08 05:18:42 +00:00
it += 1;
return true;
}
return false;
2015-06-09 22:19:59 +00:00
}
2018-12-08 05:18:42 +00:00
Project::ConfigFile::ConfigFile(const Project& project, SystemStringView name, SystemStringView subdir) {
m_filepath = SystemString(project.m_rootPath.getAbsolutePath()) + subdir.data() + name.data();
2015-06-09 22:57:21 +00:00
}
2015-06-09 22:19:59 +00:00
2018-12-08 05:18:42 +00:00
std::vector<std::string>& Project::ConfigFile::lockAndRead() {
if (m_lockedFile != nullptr) {
2015-06-10 23:34:14 +00:00
return m_lines;
}
2018-12-08 05:18:42 +00:00
m_lockedFile = hecl::FopenUnique(m_filepath.c_str(), _SYS_STR("a+"), FileLockType::Write);
hecl::FSeek(m_lockedFile.get(), 0, SEEK_SET);
2018-12-08 05:18:42 +00:00
std::string mainString;
char readBuf[1024];
size_t readSz;
while ((readSz = std::fread(readBuf, 1, sizeof(readBuf), m_lockedFile.get()))) {
2018-12-08 05:18:42 +00:00
mainString += std::string(readBuf, readSz);
}
2018-12-08 05:18:42 +00:00
auto begin = mainString.cbegin();
auto end = mainString.cbegin();
2018-12-08 05:18:42 +00:00
m_lines.clear();
while (end != mainString.end()) {
auto origEnd = end;
if (*end == '\0') {
2018-12-08 05:18:42 +00:00
break;
}
if (CheckNewLineAdvance(end)) {
if (begin != origEnd) {
m_lines.emplace_back(begin, origEnd);
}
2018-12-08 05:18:42 +00:00
begin = end;
continue;
}
++end;
}
if (begin != end) {
m_lines.emplace_back(begin, end);
}
2018-12-08 05:18:42 +00:00
return m_lines;
2015-06-09 22:57:21 +00:00
}
2015-06-09 22:19:59 +00:00
2018-12-08 05:18:42 +00:00
void Project::ConfigFile::addLine(std::string_view line) {
if (!checkForLine(line))
m_lines.emplace_back(line);
2015-06-09 22:57:21 +00:00
}
2015-05-21 02:33:05 +00:00
2018-12-08 05:18:42 +00:00
void Project::ConfigFile::removeLine(std::string_view refLine) {
if (!m_lockedFile) {
2019-07-20 04:22:58 +00:00
LogModule.reportSource(logvisor::Fatal, __FILE__, __LINE__, fmt("Project::ConfigFile::lockAndRead not yet called"));
2018-12-08 05:18:42 +00:00
return;
}
2015-05-21 02:33:05 +00:00
2018-12-08 05:18:42 +00:00
for (auto it = m_lines.begin(); it != m_lines.end();) {
2019-10-01 07:23:35 +00:00
if (*it == refLine) {
2018-12-08 05:18:42 +00:00
it = m_lines.erase(it);
continue;
2015-05-21 02:33:05 +00:00
}
2018-12-08 05:18:42 +00:00
++it;
}
2015-06-09 22:57:21 +00:00
}
2015-05-21 02:33:05 +00:00
bool Project::ConfigFile::checkForLine(std::string_view refLine) const {
2018-12-08 05:18:42 +00:00
if (!m_lockedFile) {
2019-07-20 04:22:58 +00:00
LogModule.reportSource(logvisor::Fatal, __FILE__, __LINE__, fmt("Project::ConfigFile::lockAndRead not yet called"));
2015-06-09 22:57:21 +00:00
return false;
2018-12-08 05:18:42 +00:00
}
return std::any_of(m_lines.cbegin(), m_lines.cend(), [&refLine](const auto& line) { return line == refLine; });
2015-06-09 22:57:21 +00:00
}
2015-05-21 02:33:05 +00:00
2018-12-08 05:18:42 +00:00
void Project::ConfigFile::unlockAndDiscard() {
if (m_lockedFile == nullptr) {
2019-07-20 04:22:58 +00:00
LogModule.reportSource(logvisor::Fatal, __FILE__, __LINE__, fmt("Project::ConfigFile::lockAndRead not yet called"));
2018-12-08 05:18:42 +00:00
return;
}
2015-06-10 23:34:14 +00:00
2018-12-08 05:18:42 +00:00
m_lines.clear();
m_lockedFile.reset();
2015-06-10 23:34:14 +00:00
}
2018-12-08 05:18:42 +00:00
bool Project::ConfigFile::unlockAndCommit() {
if (!m_lockedFile) {
2019-07-20 04:22:58 +00:00
LogModule.reportSource(logvisor::Fatal, __FILE__, __LINE__, fmt("Project::ConfigFile::lockAndRead not yet called"));
2018-12-08 05:18:42 +00:00
return false;
}
const SystemString newPath = m_filepath + _SYS_STR(".part");
auto newFile = hecl::FopenUnique(newPath.c_str(), _SYS_STR("w"), FileLockType::Write);
2018-12-08 05:18:42 +00:00
bool fail = false;
for (const std::string& line : m_lines) {
if (std::fwrite(line.c_str(), 1, line.size(), newFile.get()) != line.size()) {
2018-12-08 05:18:42 +00:00
fail = true;
break;
}
if (std::fputc('\n', newFile.get()) == EOF) {
2018-12-08 05:18:42 +00:00
fail = true;
break;
}
}
m_lines.clear();
newFile.reset();
m_lockedFile.reset();
2018-12-08 05:18:42 +00:00
if (fail) {
2015-07-22 19:14:50 +00:00
#if HECL_UCS2
2018-12-08 05:18:42 +00:00
_wunlink(newPath.c_str());
2015-07-22 19:14:50 +00:00
#else
2018-12-08 05:18:42 +00:00
unlink(newPath.c_str());
2015-07-22 19:14:50 +00:00
#endif
2018-12-08 05:18:42 +00:00
return false;
} else {
2015-07-22 19:14:50 +00:00
#if HECL_UCS2
2018-12-08 05:18:42 +00:00
//_wrename(newPath.c_str(), m_filepath.c_str());
MoveFileExW(newPath.c_str(), m_filepath.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
2015-07-22 19:14:50 +00:00
#else
2018-12-08 05:18:42 +00:00
rename(newPath.c_str(), m_filepath.c_str());
2015-07-22 19:14:50 +00:00
#endif
2018-12-08 05:18:42 +00:00
return true;
}
2015-06-10 23:34:14 +00:00
}
2015-06-09 23:21:45 +00:00
/**********************************************
* Project
**********************************************/
2015-06-10 02:40:03 +00:00
Project::Project(const ProjectRootPath& rootPath)
2018-12-08 05:18:42 +00:00
: m_rootPath(rootPath)
, m_workRoot(*this, _SYS_STR(""))
, m_dotPath(m_workRoot, _SYS_STR(".hecl"))
, m_cookedRoot(m_dotPath, _SYS_STR("cooked"))
, m_specs(*this, _SYS_STR("specs"))
, m_paths(*this, _SYS_STR("paths"))
, m_groups(*this, _SYS_STR("groups")) {
/* Stat for existing project directory (must already exist) */
Sstat myStat;
if (hecl::Stat(m_rootPath.getAbsolutePath().data(), &myStat)) {
2019-07-20 04:22:58 +00:00
LogModule.report(logvisor::Error, fmt(_SYS_STR("unable to stat {}")), m_rootPath.getAbsolutePath());
2018-12-08 05:18:42 +00:00
return;
}
if (!S_ISDIR(myStat.st_mode)) {
2019-07-20 04:22:58 +00:00
LogModule.report(logvisor::Error, fmt(_SYS_STR("provided path must be a directory; '{}' isn't")),
m_rootPath.getAbsolutePath());
2018-12-08 05:18:42 +00:00
return;
}
/* Create project directory structure */
m_dotPath.makeDir();
m_cookedRoot.makeDir();
/* Ensure beacon is valid or created */
const ProjectPath beaconPath(m_dotPath, _SYS_STR("beacon"));
auto bf = hecl::FopenUnique(beaconPath.getAbsolutePath().data(), _SYS_STR("a+b"));
2018-12-08 05:18:42 +00:00
struct BeaconStruct {
hecl::FourCC magic;
uint32_t version;
} beacon;
constexpr uint32_t DATA_VERSION = 1;
if (std::fread(&beacon, 1, sizeof(beacon), bf.get()) != sizeof(beacon)) {
std::fseek(bf.get(), 0, SEEK_SET);
2018-12-08 05:18:42 +00:00
beacon.magic = HECLfcc;
beacon.version = SBig(DATA_VERSION);
std::fwrite(&beacon, 1, sizeof(beacon), bf.get());
2018-12-08 05:18:42 +00:00
}
bf.reset();
2018-12-08 05:18:42 +00:00
if (beacon.magic != HECLfcc || SBig(beacon.version) != DATA_VERSION) {
2019-07-20 04:22:58 +00:00
LogModule.report(logvisor::Fatal, fmt("incompatible project version"));
2018-12-08 05:18:42 +00:00
return;
}
/* Compile current dataspec */
rescanDataSpecs();
m_valid = true;
2015-06-09 22:57:21 +00:00
}
2015-05-21 02:33:05 +00:00
2018-12-08 05:18:42 +00:00
const ProjectPath& Project::getProjectCookedPath(const DataSpecEntry& spec) const {
for (const ProjectDataSpec& sp : m_compiledSpecs)
if (&sp.spec == &spec)
return sp.cookedPath;
2019-07-28 01:19:48 +00:00
LogModule.report(logvisor::Fatal, fmt(_SYS_STR("Unable to find spec '{}'")), spec.m_name);
2018-12-08 05:18:42 +00:00
return m_cookedRoot;
}
2018-12-08 05:18:42 +00:00
bool Project::addPaths(const std::vector<ProjectPath>& paths) {
m_paths.lockAndRead();
for (const ProjectPath& path : paths)
m_paths.addLine(path.getRelativePathUTF8());
return m_paths.unlockAndCommit();
2015-06-09 22:57:21 +00:00
}
2015-06-09 22:19:59 +00:00
2018-12-08 05:18:42 +00:00
bool Project::removePaths(const std::vector<ProjectPath>& paths, bool recursive) {
std::vector<std::string>& existingPaths = m_paths.lockAndRead();
if (recursive) {
for (const ProjectPath& path : paths) {
auto recursiveBase = path.getRelativePathUTF8();
for (auto it = existingPaths.begin(); it != existingPaths.end();) {
if (!(*it).compare(0, recursiveBase.size(), recursiveBase)) {
it = existingPaths.erase(it);
continue;
2015-06-11 09:41:10 +00:00
}
2018-12-08 05:18:42 +00:00
++it;
}
2015-06-11 09:41:10 +00:00
}
2018-12-08 05:18:42 +00:00
} else
for (const ProjectPath& path : paths)
m_paths.removeLine(path.getRelativePathUTF8());
return m_paths.unlockAndCommit();
2015-06-09 22:57:21 +00:00
}
2015-05-21 02:33:05 +00:00
2018-12-08 05:18:42 +00:00
bool Project::addGroup(const hecl::ProjectPath& path) {
m_groups.lockAndRead();
m_groups.addLine(path.getRelativePathUTF8());
return m_groups.unlockAndCommit();
2015-06-09 22:57:21 +00:00
}
2015-05-21 02:33:05 +00:00
2018-12-08 05:18:42 +00:00
bool Project::removeGroup(const ProjectPath& path) {
m_groups.lockAndRead();
m_groups.removeLine(path.getRelativePathUTF8());
return m_groups.unlockAndCommit();
2015-06-09 22:57:21 +00:00
}
2015-05-21 02:33:05 +00:00
2018-12-08 05:18:42 +00:00
void Project::rescanDataSpecs() {
m_compiledSpecs.clear();
m_specs.lockAndRead();
for (const DataSpecEntry* spec : DATA_SPEC_REGISTRY) {
hecl::SystemString specStr(spec->m_name);
SystemUTF8Conv specUTF8(specStr);
m_compiledSpecs.push_back({*spec, ProjectPath(m_cookedRoot, hecl::SystemString(spec->m_name) + _SYS_STR(".spec")),
m_specs.checkForLine(specUTF8.str())});
}
m_specs.unlockAndDiscard();
2015-06-12 04:02:23 +00:00
}
2018-12-08 05:18:42 +00:00
bool Project::enableDataSpecs(const std::vector<SystemString>& specs) {
m_specs.lockAndRead();
for (const SystemString& spec : specs) {
SystemUTF8Conv specView(spec);
m_specs.addLine(specView.str());
}
bool result = m_specs.unlockAndCommit();
rescanDataSpecs();
return result;
2015-06-09 22:57:21 +00:00
}
2018-12-08 05:18:42 +00:00
bool Project::disableDataSpecs(const std::vector<SystemString>& specs) {
m_specs.lockAndRead();
for (const SystemString& spec : specs) {
SystemUTF8Conv specView(spec);
m_specs.removeLine(specView.str());
}
bool result = m_specs.unlockAndCommit();
rescanDataSpecs();
return result;
2015-06-09 22:57:21 +00:00
}
2018-12-08 05:18:42 +00:00
class CookProgress {
const hecl::MultiProgressPrinter& m_progPrinter;
const SystemChar* m_dir = nullptr;
const SystemChar* m_file = nullptr;
float m_prog = 0.f;
2015-10-04 04:35:18 +00:00
public:
2018-12-08 05:18:42 +00:00
CookProgress(const hecl::MultiProgressPrinter& progPrinter) : m_progPrinter(progPrinter) {}
void changeDir(const SystemChar* dir) {
m_dir = dir;
m_progPrinter.startNewLine();
}
void changeFile(const SystemChar* file, float prog) {
m_file = file;
m_prog = prog;
}
void reportFile(const DataSpecEntry* specEnt) {
SystemString submsg(m_file);
submsg += _SYS_STR(" (");
submsg += specEnt->m_name.data();
submsg += _SYS_STR(')');
m_progPrinter.print(m_dir, submsg.c_str(), m_prog);
}
void reportFile(const DataSpecEntry* specEnt, const SystemChar* extra) {
SystemString submsg(m_file);
submsg += _SYS_STR(" (");
submsg += specEnt->m_name.data();
submsg += _SYS_STR(", ");
submsg += extra;
submsg += _SYS_STR(')');
m_progPrinter.print(m_dir, submsg.c_str(), m_prog);
}
void reportDirComplete() { m_progPrinter.print(m_dir, nullptr, 1.f); }
2015-10-04 04:35:18 +00:00
};
2015-10-22 02:01:08 +00:00
static void VisitFile(const ProjectPath& path, bool force, bool fast,
2019-05-08 03:47:34 +00:00
std::vector<std::unique_ptr<IDataSpec>>& specInsts, CookProgress& progress, ClientProcess* cp) {
2018-12-08 05:18:42 +00:00
for (auto& spec : specInsts) {
2019-05-08 03:47:34 +00:00
if (spec->canCook(path, hecl::blender::SharedBlenderToken)) {
2018-12-08 05:18:42 +00:00
if (cp) {
cp->addCookTransaction(path, force, fast, spec.get());
} else {
2019-05-08 03:47:34 +00:00
const DataSpecEntry* override = spec->overrideDataSpec(path, spec->getDataSpecEntry());
2018-12-08 05:18:42 +00:00
if (!override)
continue;
ProjectPath cooked = path.getCookedPath(*override);
if (fast)
cooked = cooked.getWithExtension(_SYS_STR(".fast"));
if (force || cooked.getPathType() == ProjectPath::Type::None || path.getModtime() > cooked.getModtime()) {
progress.reportFile(override);
spec->doCook(path, cooked, fast, hecl::blender::SharedBlenderToken,
[&](const SystemChar* extra) { progress.reportFile(override, extra); });
2015-09-30 06:23:07 +00:00
}
2018-12-08 05:18:42 +00:00
}
2015-09-30 06:23:07 +00:00
}
2018-12-08 05:18:42 +00:00
}
2015-09-30 06:23:07 +00:00
}
2018-12-08 05:18:42 +00:00
static void VisitDirectory(const ProjectPath& dir, bool recursive, bool force, bool fast,
std::vector<std::unique_ptr<IDataSpec>>& specInsts, CookProgress& progress,
2019-05-08 03:47:34 +00:00
ClientProcess* cp) {
2018-12-08 05:18:42 +00:00
if (dir.getLastComponent().size() > 1 && dir.getLastComponent()[0] == _SYS_STR('.'))
return;
if (hecl::ProjectPath(dir, _SYS_STR("!project.yaml")).isFile() &&
hecl::ProjectPath(dir, _SYS_STR("!pool.yaml")).isFile()) {
/* Handle AudioGroup case */
2019-05-08 03:47:34 +00:00
VisitFile(dir, force, fast, specInsts, progress, cp);
2018-12-08 05:18:42 +00:00
return;
}
std::map<SystemString, ProjectPath> children;
dir.getDirChildren(children);
/* Pass 1: child file count */
int childFileCount = 0;
for (auto& child : children)
if (child.second.getPathType() == ProjectPath::Type::File)
++childFileCount;
/* Pass 2: child files */
int progNum = 0;
float progDenom = childFileCount;
progress.changeDir(dir.getLastComponent().data());
for (auto& child : children) {
if (child.second.getPathType() == ProjectPath::Type::File) {
progress.changeFile(child.first.c_str(), progNum++ / progDenom);
2019-05-08 03:47:34 +00:00
VisitFile(child.second, force, fast, specInsts, progress, cp);
2018-12-08 05:18:42 +00:00
}
}
progress.reportDirComplete();
/* Pass 3: child directories */
if (recursive) {
for (auto& child : children) {
switch (child.second.getPathType()) {
case ProjectPath::Type::Directory: {
2019-05-08 03:47:34 +00:00
VisitDirectory(child.second, recursive, force, fast, specInsts, progress, cp);
2015-10-04 04:35:18 +00:00
break;
2018-12-08 05:18:42 +00:00
}
default:
2015-10-04 04:35:18 +00:00
break;
2018-12-08 05:18:42 +00:00
}
2015-10-04 04:35:18 +00:00
}
2018-12-08 05:18:42 +00:00
}
}
2015-09-30 06:23:07 +00:00
2018-12-08 05:18:42 +00:00
bool Project::cookPath(const ProjectPath& path, const hecl::MultiProgressPrinter& progress, bool recursive, bool force,
2019-05-08 03:47:34 +00:00
bool fast, const DataSpecEntry* spec, ClientProcess* cp) {
2018-12-08 05:18:42 +00:00
/* Construct DataSpec instances for cooking */
if (spec) {
if (m_cookSpecs.size() != 1 || m_cookSpecs[0]->getDataSpecEntry() != spec) {
m_cookSpecs.clear();
if (spec->m_factory)
m_cookSpecs.push_back(spec->m_factory(*this, DataSpecTool::Cook));
}
} else if (m_cookSpecs.empty()) {
m_cookSpecs.reserve(m_compiledSpecs.size());
for (const ProjectDataSpec& projectSpec : m_compiledSpecs) {
if (projectSpec.active && projectSpec.spec.m_factory) {
m_cookSpecs.push_back(projectSpec.spec.m_factory(*this, DataSpecTool::Cook));
}
}
2018-12-08 05:18:42 +00:00
}
/* Iterate complete directory/file/glob list */
CookProgress cookProg(progress);
switch (path.getPathType()) {
case ProjectPath::Type::File:
case ProjectPath::Type::Glob: {
cookProg.changeFile(path.getLastComponent().data(), 0.f);
2019-05-08 03:47:34 +00:00
VisitFile(path, force, fast, m_cookSpecs, cookProg, cp);
2018-12-08 05:18:42 +00:00
break;
}
case ProjectPath::Type::Directory: {
2019-05-08 03:47:34 +00:00
VisitDirectory(path, recursive, force, fast, m_cookSpecs, cookProg, cp);
2018-12-08 05:18:42 +00:00
break;
}
default:
break;
}
return true;
2015-06-09 22:57:21 +00:00
}
2018-12-08 05:18:42 +00:00
bool Project::packagePath(const ProjectPath& path, const hecl::MultiProgressPrinter& progress, bool fast,
const DataSpecEntry* spec, ClientProcess* cp) {
/* Construct DataSpec instance for packaging */
const DataSpecEntry* specEntry = nullptr;
if (spec) {
if (spec->m_factory) {
2018-12-08 05:18:42 +00:00
specEntry = spec;
}
2018-12-08 05:18:42 +00:00
} else {
bool foundPC = false;
for (const ProjectDataSpec& projectSpec : m_compiledSpecs) {
if (projectSpec.active && projectSpec.spec.m_factory) {
if (hecl::StringUtils::EndsWith(projectSpec.spec.m_name, _SYS_STR("-PC"))) {
2018-12-08 05:18:42 +00:00
foundPC = true;
specEntry = &projectSpec.spec;
2018-12-08 05:18:42 +00:00
} else if (!foundPC) {
specEntry = &projectSpec.spec;
2017-10-25 07:46:32 +00:00
}
2018-12-08 05:18:42 +00:00
}
2017-10-25 07:46:32 +00:00
}
2018-12-08 05:18:42 +00:00
}
2017-10-25 07:46:32 +00:00
2018-12-08 05:18:42 +00:00
if (!specEntry)
2019-07-20 04:22:58 +00:00
LogModule.report(logvisor::Fatal, fmt("No matching DataSpec"));
2017-10-30 07:29:07 +00:00
2018-12-08 05:18:42 +00:00
if (!m_lastPackageSpec || m_lastPackageSpec->getDataSpecEntry() != specEntry)
m_lastPackageSpec = specEntry->m_factory(*this, DataSpecTool::Package);
2017-10-25 07:46:32 +00:00
2018-12-08 05:18:42 +00:00
if (m_lastPackageSpec->canPackage(path)) {
m_lastPackageSpec->doPackage(path, specEntry, fast, hecl::blender::SharedBlenderToken, progress, cp);
return true;
}
2017-10-25 07:46:32 +00:00
2018-12-08 05:18:42 +00:00
return false;
2017-10-25 07:46:32 +00:00
}
2018-12-08 05:18:42 +00:00
void Project::interruptCook() {
if (m_lastPackageSpec)
m_lastPackageSpec->interruptCook();
2015-06-09 22:57:21 +00:00
}
2018-12-08 05:18:42 +00:00
bool Project::cleanPath(const ProjectPath& path, bool recursive) { return false; }
2015-05-21 02:33:05 +00:00
2018-12-08 05:18:42 +00:00
PackageDepsgraph Project::buildPackageDepsgraph(const ProjectPath& path) { return PackageDepsgraph(); }
2015-05-21 02:33:05 +00:00
2018-12-08 05:18:42 +00:00
void Project::addBridgePathToCache(uint64_t id, const ProjectPath& path) { m_bridgePathCache[id] = path; }
2018-12-08 05:18:42 +00:00
void Project::clearBridgePathCache() { m_bridgePathCache.clear(); }
2018-12-08 05:18:42 +00:00
const ProjectPath* Project::lookupBridgePath(uint64_t id) const {
auto search = m_bridgePathCache.find(id);
if (search == m_bridgePathCache.cend())
return nullptr;
return &search->second;
}
2018-12-08 05:18:42 +00:00
} // namespace hecl::Database