mirror of
https://github.com/mumble-voip/mumble.git
synced 2025-10-26 11:19:16 +00:00
FEAT(server): Implement new database backend
This commit implements a new, Qt-independent mechanism for accessing and handling a database. It fully supports SQLite, MySQL and PostgreSQL.
This commit is contained in:
parent
4985b18d6b
commit
2521806ee6
@ -1,5 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
sudo apt update
|
||||
|
||||
sudo apt -y install \
|
||||
@ -29,4 +32,31 @@ sudo apt -y install \
|
||||
libzeroc-ice-dev \
|
||||
zsync \
|
||||
appstream \
|
||||
libpoco-dev
|
||||
libpoco-dev \
|
||||
libsqlite3-dev
|
||||
|
||||
# MySQL and PostgreSQL are pre-installed on GitHub-hosted runners. More info about the default setup can be found at
|
||||
# - https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu1804-Readme.md#databases
|
||||
# - https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md#databases
|
||||
|
||||
# Setup MySQL and PostgreSQL databases for the Mumble tests
|
||||
echo "Configuring MySQL..."
|
||||
|
||||
echo -e "[mysqld]\nlog-bin-trust-function-creators = 1" | sudo tee -a /etc/mysql/my.cnf
|
||||
|
||||
sudo systemctl enable mysql.service
|
||||
sudo systemctl start mysql.service
|
||||
|
||||
echo "CREATE DATABASE mumble_test_db; "\
|
||||
"CREATE USER 'mumble_test_user'@'localhost' IDENTIFIED BY 'MumbleTestPassword'; "\
|
||||
"GRANT ALL PRIVILEGES ON mumble_test_db.* TO 'mumble_test_user'@'localhost';" | sudo mysql --user=root --password="root"
|
||||
|
||||
|
||||
echo "Configuring PostgreSQL..."
|
||||
|
||||
sudo systemctl enable postgresql.service
|
||||
sudo systemctl start postgresql.service
|
||||
|
||||
echo "CREATE DATABASE mumble_test_db; "\
|
||||
"CREATE USER mumble_test_user ENCRYPTED PASSWORD 'MumbleTestPassword'; "\
|
||||
"GRANT ALL PRIVILEGES ON DATABASE mumble_test_db TO mumble_test_user;" | sudo -u postgres psql
|
||||
|
||||
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
@ -5,8 +5,7 @@ on: [push, pull_request]
|
||||
env:
|
||||
# Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.)
|
||||
BUILD_TYPE: Release
|
||||
CMAKE_OPTIONS: |
|
||||
-Dtests=ON -Dsymbols=ON -Ddisplay-install-paths=ON
|
||||
CMAKE_OPTIONS: -Dtests=ON -Dsymbols=ON -Ddisplay-install-paths=ON -Ddatabase-mysql-tests=ON -Ddatabase-postgresql-tests=ON
|
||||
MUMBLE_ENVIRONMENT_SOURCE: 'https://dl.mumble.info/build/vcpkg/'
|
||||
|
||||
|
||||
@ -58,6 +57,12 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
|
||||
# Ubuntu 18.04 does not ship a recent enough MySQL version, so we have to skip the MySQL tests there
|
||||
- name: Disable MySQL tests on Ubuntu 18.04
|
||||
if: ${{ matrix.os == 'ubuntu-18.04' }}
|
||||
run: echo "CMAKE_OPTIONS=${{ env.CMAKE_OPTIONS }} -Ddatabase-mysql-tests=OFF" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -34,3 +34,6 @@
|
||||
[submodule "3rdparty/utfcpp"]
|
||||
path = 3rdparty/utfcpp
|
||||
url = https://github.com/mumble-voip/utfcpp.git
|
||||
[submodule "3rdparty/soci"]
|
||||
path = 3rdparty/soci
|
||||
url = https://github.com/SOCI/soci.git
|
||||
|
||||
1
3rdparty/soci
vendored
Submodule
1
3rdparty/soci
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 438e3549594eb59d84b434c814647648e7c2f10a
|
||||
109
cmake/FindModules/FindSoci.cmake
Normal file
109
cmake/FindModules/FindSoci.cmake
Normal file
@ -0,0 +1,109 @@
|
||||
# Adapted from https://github.com/SOCI/soci/blob/master/cmake/modules/FindSoci.cmake
|
||||
###############################################################################
|
||||
# CMake module to search for SOCI library
|
||||
#
|
||||
# This module defines:
|
||||
# Soci_INCLUDE_DIRS = include dirs to be used when using the soci library
|
||||
# Soci_LIBRARY = full path to the soci library
|
||||
# Soci_VERSION = the soci version found
|
||||
# Soci_FOUND = true if soci was found
|
||||
#
|
||||
# This module respects:
|
||||
# LIB_SUFFIX = (64|32|"") Specifies the suffix for the lib directory
|
||||
#
|
||||
# For each component you specify in find_package(), the following variables are set.
|
||||
#
|
||||
# Soci_${COMPONENT}_PLUGIN = full path to the soci plugin (not set for the "core" component)
|
||||
# Soci_${COMPONENT}_FOUND
|
||||
#
|
||||
# Redistribution and use is allowed according to the terms of the BSD license.
|
||||
# For details see the accompanying COPYING-CMAKE-SCRIPTS file.
|
||||
#
|
||||
###############################################################################
|
||||
#
|
||||
### Global Configuration Section
|
||||
#
|
||||
SET(_SOCI_ALL_PLUGINS mysql odbc postgresql sqlite3)
|
||||
SET(_SOCI_REQUIRED_VARS Soci_INCLUDE_DIR Soci_LIBRARY)
|
||||
|
||||
#
|
||||
### FIRST STEP: Find the soci headers.
|
||||
#
|
||||
FIND_PATH(
|
||||
Soci_INCLUDE_DIR soci.h
|
||||
HINTS "/usr/local"
|
||||
PATH_SUFFIXES "" "soci"
|
||||
DOC "Soci (http://soci.sourceforge.net) include directory")
|
||||
MARK_AS_ADVANCED(SOCI_INCLUDE_DIR)
|
||||
|
||||
SET(Soci_INCLUDE_DIRS ${Soci_INCLUDE_DIR} CACHE STRING "")
|
||||
|
||||
#
|
||||
### SECOND STEP: Find the soci core library. Respect LIB_SUFFIX
|
||||
#
|
||||
FIND_LIBRARY(
|
||||
Soci_LIBRARY
|
||||
NAMES soci_core
|
||||
HINTS ${Soci_INCLUDE_DIR}/..
|
||||
PATH_SUFFIXES lib${LIB_SUFFIX})
|
||||
MARK_AS_ADVANCED(SOCI_LIBRARY)
|
||||
|
||||
GET_FILENAME_COMPONENT(Soci_LIBRARY_DIR ${Soci_LIBRARY} PATH)
|
||||
MARK_AS_ADVANCED(Soci_LIBRARY_DIR)
|
||||
|
||||
#
|
||||
### THIRD STEP: Find all installed plugins if the library was found
|
||||
#
|
||||
IF(Soci_INCLUDE_DIR AND Soci_LIBRARY)
|
||||
SET(Soci_core_FOUND TRUE CACHE BOOL "")
|
||||
|
||||
#
|
||||
### FOURTH STEP: Obtain SOCI version
|
||||
#
|
||||
set(Soci_VERSION_FILE "${SOCI_INCLUDE_DIR}/version.h")
|
||||
IF(EXISTS "${Soci_VERSION_FILE}")
|
||||
file(READ "${Soci_VERSION_FILE}" VERSION_CONTENT)
|
||||
string(REGEX MATCH "#define[ \t]*SOCI_VERSION[ \t]*[0-9]+" VERSION_MATCH "${VERSION_CONTENT}")
|
||||
string(REGEX REPLACE "#define[ \t]*SOCI_VERSION[ \t]*" "" VERSION_MATCH "${VERSION_MATCH}")
|
||||
|
||||
IF(NOT VERSION_MATCH)
|
||||
message(WARNING "Failed to extract SOCI version")
|
||||
ELSE()
|
||||
math(EXPR MAJOR "${VERSION_MATCH} / 100000" OUTPUT_FORMAT DECIMAL)
|
||||
math(EXPR MINOR "${VERSION_MATCH} / 100 % 1000" OUTPUT_FORMAT DECIMAL)
|
||||
math(EXPR PATCH "${VERSION_MATCH} % 100" OUTPUT_FORMAT DECIMAL)
|
||||
|
||||
set(Soci_VERSION "${MAJOR}.${MINOR}.${PATCH}" CACHE STRING "")
|
||||
ENDIF()
|
||||
ELSE()
|
||||
message(WARNING "Unable to check SOCI version")
|
||||
ENDIF()
|
||||
|
||||
FOREACH(plugin IN LISTS _SOCI_ALL_PLUGINS)
|
||||
|
||||
FIND_LIBRARY(
|
||||
Soci_${plugin}_PLUGIN
|
||||
NAMES soci_${plugin}
|
||||
HINTS ${Soci_INCLUDE_DIR}/..
|
||||
PATH_SUFFIXES lib${LIB_SUFFIX})
|
||||
MARK_AS_ADVANCED(Soci_${plugin}_PLUGIN)
|
||||
|
||||
IF(Soci_${plugin}_PLUGIN)
|
||||
SET(Soci_${plugin}_FOUND TRUE CACHE BOOL "")
|
||||
ELSE()
|
||||
SET(Soci_${plugin}_FOUND FALSE CACHE BOOL "")
|
||||
ENDIF()
|
||||
|
||||
ENDFOREACH()
|
||||
|
||||
ENDIF()
|
||||
|
||||
#
|
||||
### ADHERE TO STANDARDS
|
||||
#
|
||||
include(FindPackageHandleStandardArgs)
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(Soci
|
||||
REQUIRED_VARS ${_SOCI_REQUIRED_VARS}
|
||||
VERSION_VAR Soci_VERSION
|
||||
HANDLE_COMPONENTS
|
||||
)
|
||||
@ -15,6 +15,7 @@ option(zeroconf "Build support for zeroconf (mDNS/DNS-SD)." ON)
|
||||
option(tracy "Enable the tracy profiler." OFF)
|
||||
|
||||
option(bundled-gsl "Use the bundled GSL version instead of looking for one on the system" ON)
|
||||
option(bundled-json "Build the included version of nlohmann_json instead of looking for one on the system" ON)
|
||||
|
||||
|
||||
find_pkg(Qt5
|
||||
@ -245,6 +246,8 @@ if(client)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_subdirectory(database)
|
||||
|
||||
if(server)
|
||||
add_subdirectory(murmur)
|
||||
endif()
|
||||
|
||||
20
src/NonCopyable.h
Normal file
20
src/NonCopyable.h
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_NONCOPYABLE_H_
|
||||
#define MUMBLE_NONCOPYABLE_H_
|
||||
|
||||
/*
|
||||
* Helper class whose only property is that it can't be copied. Intended
|
||||
* to be used as superclass to inherit this behavior.
|
||||
*/
|
||||
struct NonCopyable {
|
||||
NonCopyable() = default;
|
||||
|
||||
NonCopyable(const NonCopyable &) = delete;
|
||||
NonCopyable &operator=(const NonCopyable &) = delete;
|
||||
};
|
||||
|
||||
#endif // MUMBLE_NONCOPYABLE_H_
|
||||
24
src/database/AccessException.h
Normal file
24
src/database/AccessException.h
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_ACCESSEXCEPTION_H_
|
||||
#define MUMBLE_DATABASE_ACCESSEXCEPTION_H_
|
||||
|
||||
#include "Exception.h"
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
/**
|
||||
* Thrown whenever accessing the database results in an error
|
||||
*/
|
||||
struct AccessException : public Exception {
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_ACCESSEXCEPTION_H_
|
||||
44
src/database/Backend.cpp
Normal file
44
src/database/Backend.cpp
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "Backend.h"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
Backend stringToBackend(const std::string &str) {
|
||||
if (boost::iequals(str, "sqlite")) {
|
||||
return Backend::SQLite;
|
||||
} else if (boost::iequals(str, "mysql")) {
|
||||
return Backend::MySQL;
|
||||
} else if (boost::iequals(str, "postgresql")) {
|
||||
return Backend::PostgreSQL;
|
||||
}
|
||||
|
||||
throw BadBackendException(std::string("Unknown backend \"") + str + "\"");
|
||||
}
|
||||
|
||||
std::string backendToString(Backend backend) {
|
||||
switch (backend) {
|
||||
case Backend::SQLite:
|
||||
return "SQLite";
|
||||
case Backend::MySQL:
|
||||
return "MySQL";
|
||||
case Backend::PostgreSQL:
|
||||
return "PostgreSQL";
|
||||
}
|
||||
|
||||
// This code shouldn't be reached under normal circumstances
|
||||
assert(false && "Reached unreachable code path");
|
||||
throw BadBackendException(std::string("Invalid value for Backend enum: ")
|
||||
+ std::to_string(static_cast< std::underlying_type_t< Backend > >(backend)));
|
||||
}
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
53
src/database/Backend.h
Normal file
53
src/database/Backend.h
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_BACKEND_H_
|
||||
#define MUMBLE_DATABASE_BACKEND_H_
|
||||
|
||||
#include "Exception.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
/**
|
||||
* An enum holding the different supported backend types
|
||||
*/
|
||||
enum class Backend {
|
||||
SQLite,
|
||||
MySQL,
|
||||
PostgreSQL,
|
||||
};
|
||||
|
||||
/**
|
||||
* An exception thrown if something goes wrong during Backend <-> string conversions
|
||||
*/
|
||||
class BadBackendException : public Exception {
|
||||
public:
|
||||
// Re-use all constructors of std::runtime_error
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the given string into a Backend enum value. The string matching is performed case-insensitively.
|
||||
*
|
||||
* @param str The string to convert
|
||||
* @returns The Backend corresponding to the given string
|
||||
*/
|
||||
Backend stringToBackend(const std::string &str);
|
||||
|
||||
/**
|
||||
* Converts the given Backend value into a string representation.
|
||||
*
|
||||
* @param backend The Backend to convert
|
||||
* @returns The corresponding string representation
|
||||
*/
|
||||
std::string backendToString(Backend backend);
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_BACKEND_H_
|
||||
87
src/database/CMakeLists.txt
Normal file
87
src/database/CMakeLists.txt
Normal file
@ -0,0 +1,87 @@
|
||||
# Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license
|
||||
# that can be found in the LICENSE file at the root of the
|
||||
# Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
option(bundled-soci "Build the included version of SOCI instead of looking for one on the system" ON)
|
||||
|
||||
add_library(mumble_database STATIC
|
||||
"Backend.cpp"
|
||||
"Column.cpp"
|
||||
"Constraint.cpp"
|
||||
"DBUtils.cpp"
|
||||
"DataType.cpp"
|
||||
"Database.cpp"
|
||||
"Index.cpp"
|
||||
"MetaTable.cpp"
|
||||
"MySQLConnectionParameter.cpp"
|
||||
"PostgreSQLConnectionParameter.cpp"
|
||||
"SQLiteConnectionParameter.cpp"
|
||||
"Table.cpp"
|
||||
"Trigger.cpp"
|
||||
"Version.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(mumble_database PUBLIC "${CMAKE_SOURCE_DIR}/src")
|
||||
|
||||
if (bundled-soci)
|
||||
# Include SOCI, but hard-code a few options
|
||||
set(SOCI_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/soci)
|
||||
|
||||
set(SOCI_SHARED ON CACHE BOOL "" FORCE)
|
||||
set(SOCI_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(SOCI_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(SOCI_LTO ${lto} CACHE BOOL "" FORCE)
|
||||
set(SOCI_DB2 OFF CACHE BOOL "" FORCE)
|
||||
set(SOCI_FIREBIRD OFF CACHE BOOL "" FORCE)
|
||||
set(SOCI_ODBC OFF CACHE BOOL "" FORCE)
|
||||
set(SOCI_ORACLE OFF CACHE BOOL "" FORCE)
|
||||
set(SOCI_EMPTY OFF CACHE BOOL "" FORCE)
|
||||
set(SOCI_MYSQL ON CACHE BOOL "" FORCE)
|
||||
set(SOCI_POSTGRESQL ON CACHE BOOL "" FORCE)
|
||||
set(SOCI_SQLITE3 ON CACHE BOOL "" FORCE)
|
||||
|
||||
add_subdirectory("${3RDPARTY_DIR}/soci" "${SOCI_BINARY_DIR}" EXCLUDE_FROM_ALL)
|
||||
|
||||
disable_warnings_for_all_targets_in("${3RDPARTY_DIR}/soci")
|
||||
|
||||
# SOCI's built include/ directory has to be added to the project explicitly.
|
||||
# This is needed so that SOCI can find the file soci/soci-config.h that gets generated by CMake.
|
||||
target_include_directories(mumble_database
|
||||
PUBLIC
|
||||
${SOCI_BINARY_DIR}/include
|
||||
)
|
||||
|
||||
set(soci_components
|
||||
"soci_mysql_static"
|
||||
"soci_postgresql_static"
|
||||
"soci_sqlite3_static"
|
||||
"soci_core_static"
|
||||
)
|
||||
else()
|
||||
find_pkg("Soci" 4.0.0 COMPONENTS mysql postgresql sqlite3 core REQUIRED)
|
||||
|
||||
set(soci_static "")
|
||||
set(soci_components
|
||||
"${Soci_mysql_PLUGIN}"
|
||||
"${Soci_postgresql_PLUGIN}"
|
||||
"${Soci_sqlite3_PLUGIN}"
|
||||
"${Soci_LIBRARY}"
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
target_link_libraries(mumble_database PUBLIC ${soci_components})
|
||||
|
||||
|
||||
if (NOT TARGET nlohmann_json)
|
||||
if(bundled-json)
|
||||
set(JSON_BuildTests OFF CACHE INTERNAL "")
|
||||
set(JSON_ImplicitConversions OFF CACHE INTERNAL "")
|
||||
add_subdirectory("${3RDPARTY_DIR}/nlohmann_json/" "${CMAKE_CURRENT_BINARY_DIR}/nlohmann_json/" EXCLUDE_FROM_ALL)
|
||||
else()
|
||||
find_pkg("nlohmann_json" REQUIRED)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_link_libraries(mumble_database PUBLIC nlohmann_json)
|
||||
49
src/database/Column.cpp
Normal file
49
src/database/Column.cpp
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "Column.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
Column::Column(const std::string &name, const DataType &type, const std::vector< Constraint > &constraints,
|
||||
const std::string &defaultValue)
|
||||
: m_type(type), m_constraints(constraints), m_default(defaultValue) {
|
||||
setName(name);
|
||||
}
|
||||
|
||||
const std::string Column::getName() const { return m_name; }
|
||||
|
||||
void Column::setName(const std::string &name) {
|
||||
assert(!boost::contains(name, " "));
|
||||
|
||||
m_name = name;
|
||||
}
|
||||
|
||||
const DataType &Column::getType() const { return m_type; }
|
||||
|
||||
void Column::setType(const DataType &type) { m_type = type; }
|
||||
|
||||
const std::string &Column::getDefaultValue() const { return m_default; }
|
||||
|
||||
void Column::setDefaultValue(const std::string &defaultValue) { m_default = defaultValue; }
|
||||
|
||||
bool Column::hasDefaultValue() const { return !m_default.empty(); }
|
||||
|
||||
const std::vector< Constraint > &Column::getConstraints() const { return m_constraints; }
|
||||
|
||||
void Column::setConstraints(const std::vector< Constraint > &constraints) { m_constraints = constraints; }
|
||||
|
||||
void Column::addConstraint(const Constraint &constraint) { m_constraints.push_back(constraint); }
|
||||
|
||||
void Column::clearConstraints() { m_constraints.clear(); }
|
||||
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
48
src/database/Column.h
Normal file
48
src/database/Column.h
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_COLUMN_H_
|
||||
#define MUMBLE_DATABASE_COLUMN_H_
|
||||
|
||||
#include "Constraint.h"
|
||||
#include "DataType.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
class Column {
|
||||
public:
|
||||
Column(const std::string &name = {}, const DataType &type = {},
|
||||
const std::vector< Constraint > &constraints = {}, const std::string &defaultValue = {});
|
||||
|
||||
const std::string getName() const;
|
||||
void setName(const std::string &name);
|
||||
|
||||
const DataType &getType() const;
|
||||
void setType(const DataType &type);
|
||||
|
||||
const std::string &getDefaultValue() const;
|
||||
void setDefaultValue(const std::string &defaultValue);
|
||||
bool hasDefaultValue() const;
|
||||
|
||||
const std::vector< Constraint > &getConstraints() const;
|
||||
void setConstraints(const std::vector< Constraint > &constraints);
|
||||
void addConstraint(const Constraint &constraint);
|
||||
void clearConstraints();
|
||||
|
||||
protected:
|
||||
std::string m_name;
|
||||
DataType m_type;
|
||||
std::vector< Constraint > m_constraints;
|
||||
std::string m_default;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_COLUMN_H_
|
||||
26
src/database/ConnectionParameter.h
Normal file
26
src/database/ConnectionParameter.h
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_CONNECTIONPARAMETER_H_
|
||||
#define MUMBLE_DATABASE_CONNECTIONPARAMETER_H_
|
||||
|
||||
#include "database/Backend.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
struct ConnectionParameter {
|
||||
ConnectionParameter() = default;
|
||||
virtual ~ConnectionParameter() = default;
|
||||
|
||||
virtual Backend applicability() const = 0;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_CONNECTIONPARAMETER_H_
|
||||
19
src/database/Constants.h
Normal file
19
src/database/Constants.h
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_CONSTANTS_H_
|
||||
#define MUMBLE_DATABASE_CONSTANTS_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
namespace constants {
|
||||
const std::string DB_SCHEME_VERSION_KEY = "mumble_database_scheme_version";
|
||||
}
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_CONSTANTS_H_
|
||||
216
src/database/Constraint.cpp
Normal file
216
src/database/Constraint.cpp
Normal file
@ -0,0 +1,216 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "Constraint.h"
|
||||
#include "Backend.h"
|
||||
#include "Column.h"
|
||||
#include "DataType.h"
|
||||
#include "Table.h"
|
||||
#include "UnsupportedOperationException.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
Constraint::Constraint(Constraint::Type type, const std::string &name, const Table *foreignTable,
|
||||
const Column *foreignColumn)
|
||||
: m_type(type), m_name(name) {
|
||||
// clang-format off
|
||||
assert(m_type != Constraint::ForeignKey || (
|
||||
!m_name.empty()
|
||||
&& foreignTable
|
||||
&& foreignColumn
|
||||
// Assert that the foreign column is the primary key of the foreign table
|
||||
// (otherwise we can't create a foreign key on it)
|
||||
&& std::find_if(
|
||||
foreignColumn->getConstraints().begin(), foreignColumn->getConstraints().end(),
|
||||
[](const Constraint &constraint) { return constraint.getType() == Constraint::PrimaryKey; })
|
||||
!= foreignColumn->getConstraints().end()
|
||||
// In theory we should also assert whether foreignColumn is part of foreignTable, but at the time
|
||||
// this code runs, it is somewhat likely that the column has not yet been added to its table, so
|
||||
// we can't assert on that.
|
||||
));
|
||||
// clang-format on
|
||||
|
||||
if (m_type == Constraint::ForeignKey) {
|
||||
m_foreignColumnName = foreignColumn->getName();
|
||||
m_foreignTableName = foreignTable->getName();
|
||||
}
|
||||
}
|
||||
|
||||
Constraint::Type Constraint::getType() const { return m_type; }
|
||||
|
||||
void Constraint::setType(Constraint::Type type) { m_type = type; }
|
||||
|
||||
const std::string Constraint::getName() const { return m_name; }
|
||||
|
||||
void Constraint::setName(const std::string &name) { m_name = name; }
|
||||
|
||||
bool Constraint::canInline() const {
|
||||
// All constraints can be applied inline, but if we have defined a name for this constraint,
|
||||
// then we can't inline it, as inlined constraints can't have names.
|
||||
return m_name.empty() || m_type == Constraint::ForeignKey;
|
||||
}
|
||||
|
||||
bool Constraint::canOutOfLine() const {
|
||||
// For out-of-line constraints, we require a name
|
||||
return !m_name.empty();
|
||||
}
|
||||
|
||||
bool Constraint::prefersInline() const {
|
||||
// https://stackoverflow.com/a/54689613 - apparently applying NOT NULL inline, is (can be) more efficient
|
||||
return m_type == Constraint::NotNull || m_name.empty();
|
||||
}
|
||||
|
||||
bool Constraint::canBeDropped(Backend backend) const {
|
||||
return backend != Backend::SQLite && (!m_name.empty() || m_type == Constraint::NotNull);
|
||||
}
|
||||
|
||||
std::string Constraint::inlineSQL(Backend backend) const {
|
||||
assert(m_name.empty());
|
||||
|
||||
switch (m_type) {
|
||||
case Constraint::NotNull:
|
||||
return "NOT NULL";
|
||||
case Constraint::Unique:
|
||||
return "UNIQUE";
|
||||
case Constraint::PrimaryKey: {
|
||||
switch (backend) {
|
||||
case Backend::MySQL:
|
||||
// Fallthrough
|
||||
case Backend::PostgreSQL:
|
||||
return "PRIMARY KEY";
|
||||
case Backend::SQLite:
|
||||
// In SQLite primary keys do not imply NOT NULL (in most cases) due to historic reasons, so in
|
||||
// order to make sure our table conforms to the SQL standard (which does dictate that a primary
|
||||
// key must not be NULL), we have to add the NOT NULL constraint ourselves.
|
||||
return "PRIMARY KEY NOT NULL";
|
||||
}
|
||||
|
||||
// This code should be unreachable
|
||||
assert(false);
|
||||
return "PRIMARY KEY";
|
||||
}
|
||||
case Constraint::ForeignKey:
|
||||
assert(false);
|
||||
throw UnsupportedOperationException("Foreign keys can't be added inline");
|
||||
}
|
||||
|
||||
// This code should never be reached
|
||||
assert(false);
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string Constraint::outOfLineSQL(const Column &column, Backend backend) const {
|
||||
assert(!m_name.empty());
|
||||
|
||||
std::string query = "CONSTRAINT " + m_name + " ";
|
||||
|
||||
switch (m_type) {
|
||||
case Constraint::NotNull:
|
||||
// In order to have the NOT NULL constraint as a out-of-line constraint, we'll have to rewrite
|
||||
// it into a CHECK constraint
|
||||
query += "CHECK(" + column.getName() + " IS NOT NULL)";
|
||||
break;
|
||||
case Constraint::Unique:
|
||||
query += "UNIQUE(" + column.getName() + ")";
|
||||
break;
|
||||
case Constraint::PrimaryKey:
|
||||
query += "PRIMARY KEY (" + column.getName() + ")";
|
||||
switch (backend) {
|
||||
case Backend::MySQL:
|
||||
// Fallthrough
|
||||
case Backend::PostgreSQL:
|
||||
// Do nothing special
|
||||
break;
|
||||
case Backend::SQLite:
|
||||
// Explicitly also add NOT NULL as this is not implied for most cases in SQLite (see inlineSQL
|
||||
// for more info).
|
||||
// Note that since SQLite doesn't allow for dropping constraints anyway, we don't have to worry
|
||||
// about remembering the dummy name.
|
||||
query += ", CONSTRAINT " + m_name + "_sqlite_special_not_null " + "CHECK(" + column.getName()
|
||||
+ " IS NOT NULL)";
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case Constraint::ForeignKey:
|
||||
assert(!m_foreignTableName.empty());
|
||||
assert(!m_foreignColumnName.empty());
|
||||
|
||||
// We want to make sure that the referencing key is always staying synchronized with the referenced
|
||||
// (foreign) key. Thus, if the referenced value changes, we want the reference to change as well and if
|
||||
// the referenced value gets deleted, we also want to delete the row referencing that value.
|
||||
query += "FOREIGN KEY(" + column.getName() + ") REFERENCES \"" + m_foreignTableName + "\"("
|
||||
+ m_foreignColumnName + ") ON UPDATE CASCADE ON DELETE CASCADE";
|
||||
break;
|
||||
}
|
||||
|
||||
assert(!query.empty());
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::string Constraint::dropQuery(const Table &table, const Column &column, Backend backend) const {
|
||||
if (backend == Backend::SQLite) {
|
||||
throw UnsupportedOperationException("SQLite does not support dropping constraints from tables");
|
||||
} else if (backend == Backend::MySQL) {
|
||||
// MySQL is weird in that it doesn't allow for the standard "DROP CONSTRAINT <name>" syntax, that
|
||||
// virtually every other RDMS seems to support. Instead, we'll have to use a specific syntax for
|
||||
// dropping different kinds of constraints. In theory, MySQL 8.0.19 added support for the standard
|
||||
// DROP CONSTRAINT approach but based on preliminary tests, this support is still not quite
|
||||
// compatible with other RDMS, so for now we'll stick to using special code for MySQL...
|
||||
switch (m_type) {
|
||||
case Constraint::NotNull:
|
||||
if (m_name.empty()) {
|
||||
return "ALTER TABLE \"" + table.getName() + "\" MODIFY " + column.getName() + " "
|
||||
+ column.getType().sqlRepresentation() + " NULL";
|
||||
} else {
|
||||
// Named constraints for NOT NULL are implemented as a check
|
||||
return "ALTER TABLE \"" + table.getName() + "\" DROP CHECK " + m_name;
|
||||
}
|
||||
case Constraint::Unique:
|
||||
assert(!m_name.empty());
|
||||
return "ALTER TABLE \"" + table.getName() + "\" DROP INDEX " + m_name;
|
||||
case Constraint::PrimaryKey:
|
||||
return "ALTER TABLE \"" + table.getName() + "\" DROP PRIMARY KEY";
|
||||
case Constraint::ForeignKey:
|
||||
assert(!m_name.empty());
|
||||
return "ALTER TABLE \"" + table.getName() + "\" DROP FOREIGN KEY " + m_name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_name.empty()) {
|
||||
return "ALTER TABLE \"" + table.getName() + "\" DROP CONSTRAINT " + m_name;
|
||||
}
|
||||
|
||||
switch (m_type) {
|
||||
case Constraint::NotNull:
|
||||
if (backend == Backend::PostgreSQL) {
|
||||
// Postgres is doing this differently than everyone else
|
||||
return "ALTER TABLE \"" + table.getName() + "\" ALTER COLUMN " + column.getName()
|
||||
+ " DROP NOT NULL";
|
||||
} else {
|
||||
return "ALTER TABLE \"" + table.getName() + "\" MODIFY " + column.getName() + " "
|
||||
+ column.getType().sqlRepresentation() + " NULL";
|
||||
}
|
||||
case Constraint::PrimaryKey:
|
||||
case Constraint::Unique:
|
||||
case Constraint::ForeignKey:
|
||||
// In all of these cases, we'd need a name to drop the constraint. But if we had one, we'd not reach
|
||||
// this code
|
||||
throw UnsupportedOperationException(
|
||||
"The only constraint that can be dropped without a name is NotNull");
|
||||
return "";
|
||||
}
|
||||
|
||||
// This code should never be reached
|
||||
assert(false);
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
59
src/database/Constraint.h
Normal file
59
src/database/Constraint.h
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_CONSTRAINT_H_
|
||||
#define MUMBLE_DATABASE_CONSTRAINT_H_
|
||||
|
||||
#include "Backend.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
class Table;
|
||||
class Column;
|
||||
|
||||
class Constraint {
|
||||
public:
|
||||
enum Type {
|
||||
NotNull,
|
||||
Unique,
|
||||
PrimaryKey,
|
||||
ForeignKey,
|
||||
};
|
||||
|
||||
Constraint(Type type, const std::string &name = {}, const Table *foreignTable = nullptr,
|
||||
const Column *foreignColumn = nullptr);
|
||||
~Constraint() = default;
|
||||
|
||||
Type getType() const;
|
||||
void setType(Type type);
|
||||
|
||||
const std::string getName() const;
|
||||
void setName(const std::string &name);
|
||||
|
||||
bool canInline() const;
|
||||
bool canOutOfLine() const;
|
||||
bool prefersInline() const;
|
||||
|
||||
bool canBeDropped(Backend backend) const;
|
||||
|
||||
std::string inlineSQL(Backend backend) const;
|
||||
std::string outOfLineSQL(const Column &column, Backend backend) const;
|
||||
|
||||
std::string dropQuery(const Table &table, const Column &column, Backend backend) const;
|
||||
|
||||
protected:
|
||||
Type m_type;
|
||||
std::string m_name;
|
||||
std::string m_foreignTableName;
|
||||
std::string m_foreignColumnName;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_CONSTRAINT_H_
|
||||
84
src/database/DBUtils.cpp
Normal file
84
src/database/DBUtils.cpp
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "DBUtils.h"
|
||||
|
||||
#include <soci/soci.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
namespace utils {
|
||||
|
||||
std::string to_string(const soci::row &row, std::size_t columnIndex) {
|
||||
const soci::column_properties &props = row.get_properties(columnIndex);
|
||||
|
||||
switch (props.get_data_type()) {
|
||||
case soci::dt_string:
|
||||
return row.get< std::string >(columnIndex);
|
||||
case soci::dt_double:
|
||||
return std::to_string(row.get< double >(columnIndex));
|
||||
case soci::dt_integer:
|
||||
return std::to_string(row.get< int >(columnIndex));
|
||||
case soci::dt_long_long:
|
||||
return std::to_string(row.get< long long >(columnIndex));
|
||||
case soci::dt_unsigned_long_long:
|
||||
return std::to_string(row.get< unsigned long long >(columnIndex));
|
||||
case soci::dt_date:
|
||||
case soci::dt_xml:
|
||||
case soci::dt_blob:
|
||||
std::cerr << "[ERROR]: Tried to convert unsupported DB datatype to string: "
|
||||
<< props.get_data_type() << std::endl;
|
||||
std::abort();
|
||||
}
|
||||
|
||||
assert(false);
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string to_string(const nlohmann::json &json) {
|
||||
switch (json.type()) {
|
||||
case nlohmann::json::value_t::string:
|
||||
// For strings the nlohmann::to_string method would add extra quotes around the actual string,
|
||||
// which is not what we want. Thus, we have to handle them here explicitly.
|
||||
return json.get< std::string >();
|
||||
default:
|
||||
return nlohmann::to_string(json);
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json to_json(const soci::row &row, std::size_t columnIndex) {
|
||||
const soci::column_properties &props = row.get_properties(columnIndex);
|
||||
|
||||
switch (props.get_data_type()) {
|
||||
case soci::dt_string:
|
||||
return row.get< std::string >(columnIndex);
|
||||
case soci::dt_double:
|
||||
return row.get< double >(columnIndex);
|
||||
case soci::dt_integer:
|
||||
return row.get< int >(columnIndex);
|
||||
case soci::dt_long_long:
|
||||
return row.get< long long >(columnIndex);
|
||||
case soci::dt_unsigned_long_long:
|
||||
return row.get< unsigned long long >(columnIndex);
|
||||
case soci::dt_date:
|
||||
case soci::dt_xml:
|
||||
case soci::dt_blob:
|
||||
std::cerr << "[ERROR]: Tried to convert unsupported DB datatype to JSON: " << props.get_data_type()
|
||||
<< std::endl;
|
||||
std::abort();
|
||||
}
|
||||
|
||||
assert(false);
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace utils
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
30
src/database/DBUtils.h
Normal file
30
src/database/DBUtils.h
Normal file
@ -0,0 +1,30 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_UTILS_H_
|
||||
#define MUMBLE_DATABASE_UTILS_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
namespace soci {
|
||||
class row;
|
||||
}
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
namespace utils {
|
||||
|
||||
std::string to_string(const soci::row &row, std::size_t columnIndex);
|
||||
std::string to_string(const nlohmann::json &json);
|
||||
|
||||
nlohmann::json to_json(const soci::row &row, std::size_t columnIndex);
|
||||
|
||||
} // namespace utils
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_UTILS_H_
|
||||
158
src/database/DataType.cpp
Normal file
158
src/database/DataType.cpp
Normal file
@ -0,0 +1,158 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "DataType.h"
|
||||
#include "Backend.h"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
constexpr const std::size_t DataType::Unsized;
|
||||
|
||||
|
||||
DataType::DataType(DataType::Type type, std::size_t size) : m_type(type), m_size(size) {
|
||||
assert((size == Unsized && canBeUnsized()) || (size != Unsized && canBeSized()));
|
||||
}
|
||||
|
||||
DataType::Type DataType::getType() const { return m_type; }
|
||||
|
||||
void DataType::setType(DataType::Type type) { m_type = type; }
|
||||
|
||||
std::size_t DataType::getSize() const { return m_size; }
|
||||
|
||||
void DataType::setSize(std::size_t size) { m_size = size; }
|
||||
|
||||
bool DataType::isSized() const { return m_size != DataType::Unsized; }
|
||||
|
||||
bool DataType::canBeSized() const { return canBeSized(m_type); }
|
||||
|
||||
bool DataType::canBeSized(DataType::Type type) {
|
||||
switch (type) {
|
||||
case DataType::Integer:
|
||||
return false;
|
||||
case DataType::SmallInteger:
|
||||
return false;
|
||||
case DataType::Double:
|
||||
return false;
|
||||
case DataType::FixedSizeString:
|
||||
return true;
|
||||
case DataType::String:
|
||||
return true;
|
||||
}
|
||||
|
||||
// This code should be unreachable
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DataType::canBeUnsized() const { return canBeUnsized(m_type); }
|
||||
|
||||
bool DataType::canBeUnsized(DataType::Type type) {
|
||||
switch (type) {
|
||||
case DataType::Integer:
|
||||
return true;
|
||||
case DataType::SmallInteger:
|
||||
return true;
|
||||
case DataType::Double:
|
||||
return true;
|
||||
case DataType::FixedSizeString:
|
||||
return false;
|
||||
case DataType::String:
|
||||
return false;
|
||||
}
|
||||
|
||||
// This code should be unreachable
|
||||
assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string DataType::sqlRepresentation() const {
|
||||
std::string sqlRepr;
|
||||
|
||||
switch (m_type) {
|
||||
case DataType::Integer:
|
||||
sqlRepr = "INTEGER";
|
||||
break;
|
||||
case DataType::SmallInteger:
|
||||
sqlRepr = "SMALLINT";
|
||||
break;
|
||||
case DataType::Double:
|
||||
sqlRepr = "DOUBLE PRECISION";
|
||||
break;
|
||||
case DataType::FixedSizeString:
|
||||
sqlRepr = "CHAR";
|
||||
break;
|
||||
case DataType::String:
|
||||
sqlRepr = "VARCHAR";
|
||||
break;
|
||||
}
|
||||
|
||||
assert(!sqlRepr.empty());
|
||||
|
||||
if (isSized()) {
|
||||
sqlRepr += "(" + std::to_string(m_size) + ")";
|
||||
}
|
||||
|
||||
return sqlRepr;
|
||||
}
|
||||
|
||||
DataType DataType::fromSQLRepresentation(const std::string &strRepr) {
|
||||
std::string name;
|
||||
|
||||
std::size_t i = 0;
|
||||
for (; i < strRepr.size(); ++i) {
|
||||
if (strRepr[i] == '(') {
|
||||
break;
|
||||
}
|
||||
|
||||
name += strRepr[i];
|
||||
}
|
||||
|
||||
std::size_t size = DataType::Unsized;
|
||||
if (strRepr[i] == '(') {
|
||||
if (strRepr[strRepr.size() - 1] != ')' || strRepr.size() - i < 3) {
|
||||
// If we found a '(', we expect at least something in the form of "(1)"
|
||||
throw UnknownDataTypeException("Malformed data type size in \"" + strRepr + "\"");
|
||||
}
|
||||
|
||||
try {
|
||||
size = std::stoi(strRepr.substr(i + 1, strRepr.size() - 1));
|
||||
} catch (const std::invalid_argument &e) {
|
||||
throw UnknownDataTypeException("Size of data type \"" + strRepr
|
||||
+ "\" could not be parsed: " + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
DataType::Type type;
|
||||
if (boost::iequals(name, "INTEGER")) {
|
||||
type = DataType::Integer;
|
||||
} else if (boost::iequals(name, "SMALLINT")) {
|
||||
type = DataType::SmallInteger;
|
||||
} else if (boost::iequals(name, "DOUBLE PRECISION")) {
|
||||
type = DataType::Double;
|
||||
} else if (boost::iequals(name, "CHAR")) {
|
||||
type = DataType::FixedSizeString;
|
||||
} else if (boost::iequals(name, "VARCHAR")) {
|
||||
type = DataType::String;
|
||||
} else {
|
||||
throw UnknownDataTypeException("Unknown data type \"" + name + "\"");
|
||||
}
|
||||
|
||||
if (!canBeSized(type) && size != DataType::Unsized) {
|
||||
throw UnknownDataTypeException("Data type \"" + name + "\" can't be sized");
|
||||
}
|
||||
if (!canBeUnsized(type) && size == DataType::Unsized) {
|
||||
throw UnknownDataTypeException("Data type \"" + name + "\" has to be sized");
|
||||
}
|
||||
|
||||
return DataType(type, size);
|
||||
}
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
59
src/database/DataType.h
Normal file
59
src/database/DataType.h
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_DATATYPE_H_
|
||||
#define MUMBLE_DATABASE_DATATYPE_H_
|
||||
|
||||
#include "Exception.h"
|
||||
|
||||
#include <limits>
|
||||
#include <string>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
struct UnknownDataTypeException : public Exception {
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
class DataType {
|
||||
public:
|
||||
enum Type {
|
||||
Integer,
|
||||
SmallInteger,
|
||||
Double,
|
||||
FixedSizeString,
|
||||
String,
|
||||
};
|
||||
static constexpr const std::size_t Unsized = std::numeric_limits< std::size_t >::max();
|
||||
|
||||
DataType(Type type = {}, std::size_t size = Unsized);
|
||||
~DataType() = default;
|
||||
|
||||
Type getType() const;
|
||||
void setType(Type type);
|
||||
|
||||
std::size_t getSize() const;
|
||||
void setSize(std::size_t size);
|
||||
bool isSized() const;
|
||||
|
||||
bool canBeSized() const;
|
||||
bool canBeUnsized() const;
|
||||
|
||||
static bool canBeSized(Type type);
|
||||
static bool canBeUnsized(Type type);
|
||||
|
||||
std::string sqlRepresentation() const;
|
||||
static DataType fromSQLRepresentation(const std::string &strRepr);
|
||||
|
||||
protected:
|
||||
Type m_type;
|
||||
std::size_t m_size;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_DATATYPE_H_
|
||||
525
src/database/Database.cpp
Normal file
525
src/database/Database.cpp
Normal file
@ -0,0 +1,525 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "Database.h"
|
||||
#include "AccessException.h"
|
||||
#include "Constants.h"
|
||||
#include "FormatException.h"
|
||||
#include "InitException.h"
|
||||
#include "MetaTable.h"
|
||||
#include "MySQLConnectionParameter.h"
|
||||
#include "PostgreSQLConnectionParameter.h"
|
||||
#include "SQLiteConnectionParameter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <memory>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
// These functions are defined by the respective SOCI backend libraries
|
||||
extern "C" void register_factory_sqlite3();
|
||||
extern "C" void register_factory_mysql();
|
||||
extern "C" void register_factory_postgresql();
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
struct find_by_name {
|
||||
const std::string &name;
|
||||
|
||||
bool operator()(const std::unique_ptr< Table > &ptr) { return ptr && ptr->getName() == name; }
|
||||
};
|
||||
|
||||
Database::Database(Backend backend) : m_backend(backend){};
|
||||
|
||||
Database::~Database() {}
|
||||
|
||||
void Database::init(const ConnectionParameter ¶meter) {
|
||||
assert(parameter.applicability() == m_backend);
|
||||
if (parameter.applicability() != m_backend) {
|
||||
throw InitException("Supplied connection parameter does not apply to chosen database backend");
|
||||
}
|
||||
|
||||
connectToDB(parameter);
|
||||
|
||||
applyBackendSpecificSetup(parameter);
|
||||
|
||||
// Create a meta-table
|
||||
auto metaTable = std::make_unique< MetaTable >(m_sql, m_backend);
|
||||
|
||||
if (!tableExistsInDB(metaTable->getName())) {
|
||||
metaTable->create();
|
||||
}
|
||||
|
||||
unsigned int schemeVersion = metaTable->getSchemeVersion();
|
||||
|
||||
addTable(std::move(metaTable));
|
||||
|
||||
// Make sure all standard tables are added
|
||||
setupStandardTables();
|
||||
|
||||
if (schemeVersion == 0) {
|
||||
// The scheme version entry does not exist. We assume that this means that this is a freshly created DB
|
||||
createTables();
|
||||
} else if (schemeVersion < getSchemeVersion()) {
|
||||
// The DB is still using an older scheme than we want to use -> perform an upgrade
|
||||
migrateTables(schemeVersion, getSchemeVersion());
|
||||
} else if (schemeVersion > getSchemeVersion()) {
|
||||
// The DB is using a more recent scheme than we want to use -> abort immediately as we don't know how this
|
||||
// could possibly look like and we don't want to risk data loss.
|
||||
throw InitException(std::string("The existing database is using a more recent scheme version (")
|
||||
+ std::to_string(schemeVersion) + ") than the current Mumble instance intends to ("
|
||||
+ std::to_string(getSchemeVersion()) + ")");
|
||||
} else {
|
||||
// The DB's scheme version matches the one we want to use already -> just use as is
|
||||
}
|
||||
}
|
||||
|
||||
Database::table_id Database::addTable(std::unique_ptr< Table > table) {
|
||||
m_tables.push_back(std::move(table));
|
||||
|
||||
// The "table ID" is just the index of the table in the used vector
|
||||
return m_tables.size() - 1;
|
||||
}
|
||||
|
||||
const Table *Database::getTable(const std::string &name) const {
|
||||
auto it = std::find_if(m_tables.begin(), m_tables.end(), find_by_name{ name });
|
||||
|
||||
if (it != m_tables.end()) {
|
||||
return it->get();
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
const Table *Database::getTable(Database::table_id tableID) const {
|
||||
assert(tableID < m_tables.size());
|
||||
|
||||
if (tableID < m_tables.size()) {
|
||||
return m_tables[tableID].get();
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
Table *Database::getTable(const std::string &name) {
|
||||
// Reuse const implementation
|
||||
return const_cast< Table * >(const_cast< const Database * >(this)->getTable(name));
|
||||
}
|
||||
|
||||
Table *Database::getTable(Database::table_id tableID) {
|
||||
// Reuse const implementation
|
||||
return const_cast< Table * >(const_cast< const Database * >(this)->getTable(tableID));
|
||||
}
|
||||
|
||||
std::unique_ptr< Table > Database::takeTable(const std::string &name) {
|
||||
auto it = std::find_if(m_tables.begin(), m_tables.end(), find_by_name{ name });
|
||||
|
||||
if (it != m_tables.end()) {
|
||||
// In order to not invalidate an previously assigned table IDs, we can't actually remove the table from our
|
||||
// vector (that would mess up all IDs (indices) of the tables after it. Instead, we move the pointer out of
|
||||
// the vector, but keep a nullptr at its original place in the list.
|
||||
std::unique_ptr< Table > ptr = std::move(*it);
|
||||
|
||||
it->reset();
|
||||
|
||||
return ptr;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr< Table > Database::takeTable(Database::table_id tableID) {
|
||||
assert(tableID < m_tables.size());
|
||||
|
||||
if (tableID < m_tables.size()) {
|
||||
// Same argument here, as in the overload taking a string (see above)
|
||||
std::unique_ptr< Table > ptr = std::move(m_tables[tableID]);
|
||||
|
||||
m_tables[tableID].reset();
|
||||
|
||||
return ptr;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Database::removeTable(const std::string &name) { takeTable(name); }
|
||||
|
||||
void Database::removeTable(Database::table_id tableID) { takeTable(tableID); }
|
||||
|
||||
void Database::importFromJSON(const nlohmann::json &json, bool createMissingTables) {
|
||||
// We wrap the entire import into a transaction to make sure, that we don't end up with partial imports if an
|
||||
// error is encountered during the import.
|
||||
soci::transaction transaction(m_sql);
|
||||
|
||||
if (!json.contains("meta_data")) {
|
||||
throw FormatException("JSON-import: JSON is missing top-level \"meta_data\" object");
|
||||
}
|
||||
|
||||
importMetaData(json["meta_data"]);
|
||||
|
||||
if (!json.contains("tables")) {
|
||||
throw FormatException("JSON-import: JSON is missing top-level \"tables\" object");
|
||||
}
|
||||
|
||||
const nlohmann::json tables = json["tables"];
|
||||
|
||||
if (!tables.is_object()) {
|
||||
throw FormatException("JSON-import: Top-level \"tables\" entry is not of type object");
|
||||
}
|
||||
|
||||
for (auto it = tables.begin(); it != tables.end(); ++it) {
|
||||
std::string tableName = it.key();
|
||||
|
||||
auto tableIt = std::find_if(m_tables.begin(), m_tables.end(), find_by_name{ tableName });
|
||||
|
||||
Table *table;
|
||||
bool tableIsNew = false;
|
||||
if (tableIt == m_tables.end()) {
|
||||
if (createMissingTables) {
|
||||
// Create table on-the-fly
|
||||
table_id id = addTable(std::make_unique< Table >(m_sql, m_backend, tableName));
|
||||
|
||||
table = m_tables[id].get();
|
||||
tableIsNew = true;
|
||||
} else {
|
||||
throw FormatException("JSON-import: Unknown table \"" + tableName + "\"");
|
||||
}
|
||||
} else {
|
||||
table = tableIt->get();
|
||||
}
|
||||
|
||||
const nlohmann::json &body = it.value();
|
||||
|
||||
if (!body.is_object()) {
|
||||
throw FormatException(std::string("JSON-import: Specification for table \"") + tableName
|
||||
+ "\" is not an object");
|
||||
}
|
||||
|
||||
table->importFromJSON(body, tableIsNew);
|
||||
}
|
||||
|
||||
transaction.commit();
|
||||
}
|
||||
|
||||
nlohmann::json Database::exportToJSON() const {
|
||||
nlohmann::json json;
|
||||
json["meta_data"] = exportMetaData();
|
||||
|
||||
nlohmann::json &tables = json["tables"];
|
||||
|
||||
for (const std::unique_ptr< Table > ¤tTable : m_tables) {
|
||||
tables[currentTable->getName()] = currentTable->exportToJSON();
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
void Database::destroyTables() {
|
||||
for (const std::unique_ptr< Table > ¤tTable : m_tables) {
|
||||
if (currentTable) {
|
||||
if (tableExistsInDB(currentTable->getName())) {
|
||||
currentTable->destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Database::clearTables() {
|
||||
for (const std::unique_ptr< Table > ¤tTable : m_tables) {
|
||||
if (tableExistsInDB(currentTable->getName())) {
|
||||
currentTable->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Version Database::getBackendVersion() {
|
||||
Version version;
|
||||
try {
|
||||
switch (m_backend) {
|
||||
case Backend::SQLite: {
|
||||
std::string versionString;
|
||||
m_sql << "SELECT sqlite_version()", soci::into(versionString);
|
||||
|
||||
if (std::sscanf(versionString.c_str(), "%u.%u.%u", &version.m_major, &version.m_minor,
|
||||
&version.m_patch)
|
||||
!= 3) {
|
||||
// Parsing failed
|
||||
return {};
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case Backend::MySQL: {
|
||||
std::string versionString;
|
||||
m_sql << "SELECT VERSION()", soci::into(versionString);
|
||||
|
||||
if (std::sscanf(versionString.c_str(), "%u.%u.%u", &version.m_major, &version.m_minor,
|
||||
&version.m_patch)
|
||||
!= 3) {
|
||||
// Parsing failed
|
||||
return {};
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case Backend::PostgreSQL: {
|
||||
std::string versionString;
|
||||
m_sql << "SHOW server_version", soci::into(versionString);
|
||||
|
||||
// Postgres seems to (sometimes) only supply major and minor version, so we're also content with
|
||||
// only parsing 2 version segments
|
||||
int parsedFields = std::sscanf(versionString.c_str(), "%u.%u.%u", &version.m_major,
|
||||
&version.m_minor, &version.m_patch);
|
||||
if (parsedFields != 2 && parsedFields != 3) {
|
||||
// Parsing failed
|
||||
return {};
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed to query DB backend version: ") + e.what());
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
void Database::connectToDB(const ConnectionParameter ¶meter) {
|
||||
assert(parameter.applicability() == m_backend);
|
||||
|
||||
std::string connectionString;
|
||||
switch (m_backend) {
|
||||
case Backend::SQLite: {
|
||||
register_factory_sqlite3();
|
||||
const auto &sqliteParameter = static_cast< const SQLiteConnectionParameter & >(parameter);
|
||||
|
||||
connectionString = "sqlite3://dbname=" + sqliteParameter.dbPath;
|
||||
break;
|
||||
}
|
||||
case Backend::MySQL: {
|
||||
register_factory_mysql();
|
||||
const auto &mysqlParameter = static_cast< const MySQLConnectionParameter & >(parameter);
|
||||
|
||||
connectionString = "mysql://dbname='" + mysqlParameter.dbName + "'";
|
||||
|
||||
if (!mysqlParameter.userName.empty()) {
|
||||
connectionString += " user='" + mysqlParameter.userName + "'";
|
||||
}
|
||||
if (!mysqlParameter.password.empty()) {
|
||||
connectionString += " password='" + mysqlParameter.password + "'";
|
||||
}
|
||||
if (!mysqlParameter.host.empty()) {
|
||||
connectionString += " host='" + mysqlParameter.host + "'";
|
||||
}
|
||||
if (!mysqlParameter.port.empty()) {
|
||||
connectionString += " port='" + mysqlParameter.port + "'";
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Backend::PostgreSQL: {
|
||||
register_factory_postgresql();
|
||||
const auto &postgresqlParameter = static_cast< const PostgreSQLConnectionParameter & >(parameter);
|
||||
|
||||
connectionString = "postgresql://dbname='" + postgresqlParameter.dbName + "'";
|
||||
|
||||
if (!postgresqlParameter.userName.empty()) {
|
||||
connectionString += " user='" + postgresqlParameter.userName + "'";
|
||||
}
|
||||
if (!postgresqlParameter.password.empty()) {
|
||||
connectionString += " password='" + postgresqlParameter.password + "'";
|
||||
}
|
||||
if (!postgresqlParameter.host.empty()) {
|
||||
connectionString += " host='" + postgresqlParameter.host + "'";
|
||||
}
|
||||
if (!postgresqlParameter.port.empty()) {
|
||||
connectionString += " port='" + postgresqlParameter.port + "'";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
m_sql.open(connectionString);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw InitException(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
bool Database::tableExistsInDB(const std::string &name) {
|
||||
std::string query;
|
||||
switch (m_backend) {
|
||||
case Backend::SQLite:
|
||||
query = "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name=:tableName)";
|
||||
break;
|
||||
case Backend::MySQL:
|
||||
// Make sure to restrict our search to the currently selected database (we always assume that we have a
|
||||
// specific DB selected)
|
||||
query = "SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_schema=(SELECT DATABASE()) "
|
||||
"AND table_name=:tableName)";
|
||||
break;
|
||||
case Backend::PostgreSQL:
|
||||
query = "SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name=:tableName AND "
|
||||
"table_catalog=(SELECT CURRENT_DATABASE()))";
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
int exists = 0;
|
||||
m_sql << query, soci::use(name), soci::into(exists);
|
||||
|
||||
return exists;
|
||||
} catch (const soci::soci_error &e) {
|
||||
// Rethrow
|
||||
throw AccessException(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void Database::applyBackendSpecificSetup(const ConnectionParameter ¶meter) {
|
||||
assert(parameter.applicability() == m_backend);
|
||||
|
||||
try {
|
||||
switch (m_backend) {
|
||||
case Backend::SQLite: {
|
||||
// Check the DB encoding to make sure that it is Unicode-compatible
|
||||
std::string encoding;
|
||||
m_sql << "PRAGMA ENCODING", soci::into(encoding);
|
||||
|
||||
if (!boost::iequals(encoding, "UTF-8") && !boost::iequals(encoding, "UTF-16")
|
||||
&& !boost::iequals(encoding, "UTF-16le") && !boost::iequals(encoding, "UTF-16be")) {
|
||||
throw InitException("Database has invalid encoding \"" + encoding + "\"");
|
||||
}
|
||||
|
||||
// Make sure that foreign key constraints are actually enforced
|
||||
m_sql << "PRAGMA foreign_keys = ON";
|
||||
int enabled;
|
||||
m_sql << "PRAGMA foreign_keys", soci::into(enabled);
|
||||
|
||||
if (!enabled) {
|
||||
throw InitException("Failed at enabling foreign key enforcement");
|
||||
}
|
||||
|
||||
const SQLiteConnectionParameter sqliteParam =
|
||||
static_cast< const SQLiteConnectionParameter & >(parameter);
|
||||
if (sqliteParam.useWAL) {
|
||||
// Below pragma statement returns the new journal mode (or the old one on failure)
|
||||
std::string newJournalMode;
|
||||
m_sql << "PRAGMA journal_mode=WAL", soci::into(newJournalMode);
|
||||
|
||||
if (!boost::iequals(newJournalMode, "WAL")) {
|
||||
throw InitException("Failed at changing journal mode from \"" + newJournalMode
|
||||
+ "\" to \"WAL\"");
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case Backend::MySQL: {
|
||||
if (getBackendVersion() < Version{ 8, 0, 16 }) {
|
||||
// The CHECK constraint that we use for named NOT NULL constrains was introduced only in
|
||||
// MySQL 8.0.16
|
||||
throw InitException("We require at least MySQL v8.0.16 as earlier version don't implement all "
|
||||
"necessary features");
|
||||
}
|
||||
// Make MySQL as conforming to ANSI standard SQL as possible
|
||||
m_sql << "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE";
|
||||
m_sql << "SET sql_mode = 'ANSI'";
|
||||
|
||||
// Ensure that MySQL uses proper Unicode character encoding (note that "utf8" isn't actual proper
|
||||
// UTF8, so we have to use the fixed version "utf8mb4" instead
|
||||
m_sql << "ALTER DATABASE CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
|
||||
|
||||
// Ensure that log_bin_trust_function_creators is enabled. This is required in order for us to be
|
||||
// able to e.g. create triggers, without requiring the SUPER privilege for our user account.
|
||||
int enabled = 0;
|
||||
m_sql << "SELECT @@GLOBAL.log_bin_trust_function_creators", soci::into(enabled);
|
||||
|
||||
if (!enabled) {
|
||||
throw InitException("'log_bin_trust_function_creators' is NOT enabled. Mumble can't operate "
|
||||
"properly without it.");
|
||||
}
|
||||
|
||||
// Ensure the binlog_format is ROW to make sure, we don't run into issues due to
|
||||
// log-bin-trust-function-creators being enabled
|
||||
std::string binlogFormat;
|
||||
m_sql << "SELECT @@SESSION.binlog_format", soci::into(binlogFormat);
|
||||
|
||||
if (!boost::iequals(binlogFormat, "ROW")) {
|
||||
throw InitException(
|
||||
"Mumble requires MySQL's binlog_format to be set to \"ROW\" but it is set to \""
|
||||
+ binlogFormat + "\"");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case Backend::PostgreSQL: {
|
||||
// Check the DB encoding to make sure that it is Unicode-compatible
|
||||
std::string encoding;
|
||||
m_sql << "SHOW SERVER_ENCODING", soci::into(encoding);
|
||||
|
||||
if (!boost::iequals(encoding, "utf8")) {
|
||||
throw InitException("Invalid (non-UTF-8) database encoding encountered: " + encoding);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Applying DB-specific settings failed: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void Database::setupStandardTables() {
|
||||
// The default implementation does nothing
|
||||
}
|
||||
|
||||
void Database::createTables() {
|
||||
for (std::unique_ptr< Table > ¤tTable : m_tables) {
|
||||
if (currentTable->getName() == MetaTable::NAME) {
|
||||
// Meta table is special. We assume it is created separately
|
||||
continue;
|
||||
}
|
||||
|
||||
currentTable->create();
|
||||
}
|
||||
}
|
||||
|
||||
void Database::migrateTables(unsigned int fromSchemeVersion, unsigned int toSchemeVersion) {
|
||||
for (std::unique_ptr< Table > ¤tTable : m_tables) {
|
||||
currentTable->migrate(fromSchemeVersion, toSchemeVersion);
|
||||
}
|
||||
}
|
||||
|
||||
void Database::importMetaData(const nlohmann::json &metaData) {
|
||||
if (!metaData.contains("scheme_version")) {
|
||||
throw FormatException("JSON-import: meta_data is missing \"scheme_version\" field");
|
||||
}
|
||||
if (!metaData["scheme_version"].is_number_integer()) {
|
||||
throw FormatException("JSON-import: Expected to be \"scheme_version\" field to be of type integer");
|
||||
}
|
||||
if (metaData["scheme_version"].get< unsigned int >() != getSchemeVersion()) {
|
||||
// For the time being, we will only verify that the specified version scheme will match our current scheme
|
||||
// version
|
||||
throw FormatException(std::string("JSON-import: Can't import data for version scheme ")
|
||||
+ std::to_string(metaData["scheme_version"].get< unsigned int >())
|
||||
+ " when current scheme version is " + std::to_string(getSchemeVersion()));
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json Database::exportMetaData() const {
|
||||
nlohmann::json metaData;
|
||||
metaData["scheme_version"] = getSchemeVersion();
|
||||
|
||||
return metaData;
|
||||
}
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
95
src/database/Database.h
Normal file
95
src/database/Database.h
Normal file
@ -0,0 +1,95 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_DATABASE_H_
|
||||
#define MUMBLE_DATABASE_DATABASE_H_
|
||||
|
||||
#include "Backend.h"
|
||||
#include "ConnectionParameter.h"
|
||||
#include "NonCopyable.h"
|
||||
#include "Table.h"
|
||||
#include "Version.h"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <soci/soci.h>
|
||||
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
/**
|
||||
* A general class representing a database which in turn consists of tables. This is an abstract class
|
||||
* that is intended to be subclassed for actual implementations.
|
||||
*/
|
||||
class Database : NonCopyable {
|
||||
public:
|
||||
using table_id = unsigned int;
|
||||
|
||||
Database(Backend backend);
|
||||
virtual ~Database();
|
||||
|
||||
virtual void init(const ConnectionParameter ¶meter);
|
||||
|
||||
virtual unsigned int getSchemeVersion() const = 0;
|
||||
|
||||
table_id addTable(std::unique_ptr< Table > table);
|
||||
|
||||
const Table *getTable(const std::string &name) const;
|
||||
const Table *getTable(table_id tableID) const;
|
||||
Table *getTable(const std::string &name);
|
||||
Table *getTable(table_id tableID);
|
||||
|
||||
std::unique_ptr< Table > takeTable(const std::string &name);
|
||||
std::unique_ptr< Table > takeTable(table_id tableID);
|
||||
|
||||
void removeTable(const std::string &name);
|
||||
void removeTable(table_id tableID);
|
||||
|
||||
void importFromJSON(const nlohmann::json &json, bool createMissingTables);
|
||||
nlohmann::json exportToJSON() const;
|
||||
|
||||
/**
|
||||
* Deletes all tables from the database. Note that this will leave the actual Table objects contained
|
||||
* in this object in tact.
|
||||
*/
|
||||
virtual void destroyTables();
|
||||
/**
|
||||
* Clears all tables contained in this database
|
||||
*/
|
||||
virtual void clearTables();
|
||||
|
||||
/**
|
||||
* @returns The version of the used DB backend. If the version query fails, an AccessException is thrown. If
|
||||
* the query succeeds but the result can't be parsed properly, a default constructed Version object (0.0.0)
|
||||
* is returned.
|
||||
*/
|
||||
Version getBackendVersion();
|
||||
|
||||
protected:
|
||||
Backend m_backend;
|
||||
std::vector< std::unique_ptr< Table > > m_tables;
|
||||
soci::session m_sql;
|
||||
|
||||
void connectToDB(const ConnectionParameter ¶meter);
|
||||
|
||||
bool tableExistsInDB(const std::string &name);
|
||||
|
||||
void applyBackendSpecificSetup(const ConnectionParameter ¶meter);
|
||||
|
||||
virtual void setupStandardTables();
|
||||
void createTables();
|
||||
void migrateTables(unsigned int fromSchemeVersion, unsigned int toSchemeVersion);
|
||||
|
||||
virtual void importMetaData(const nlohmann::json &json);
|
||||
virtual nlohmann::json exportMetaData() const;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_DATABASE_H_
|
||||
24
src/database/Exception.h
Normal file
24
src/database/Exception.h
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_EXCEPTION_H_
|
||||
#define MUMBLE_DATABASE_EXCEPTION_H_
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
/**
|
||||
* Exception superclass for all database-related exceptions
|
||||
*/
|
||||
struct Exception : public std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_EXCEPTION_H_
|
||||
24
src/database/FormatException.h
Normal file
24
src/database/FormatException.h
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_FORMATEXCEPTION_H_
|
||||
#define MUMBLE_DATABASE_FORMATEXCEPTION_H_
|
||||
|
||||
#include "Exception.h"
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
/**
|
||||
* Thrown whenever an invalid format is encountered in the context of a database operation
|
||||
*/
|
||||
struct FormatException : public Exception {
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_FORMATEXCEPTION_H_
|
||||
64
src/database/Index.cpp
Normal file
64
src/database/Index.cpp
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "Index.h"
|
||||
#include "Table.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
Index::Index(const std::string &name, const std::vector< std::string > &columns)
|
||||
: m_name(name), m_columns(columns) {}
|
||||
Index::Index(const std::string &name, std::vector< std::string > &&columns)
|
||||
: m_name(name), m_columns(std::move(columns)) {}
|
||||
|
||||
const std::string &Index::getName() const { return m_name; }
|
||||
|
||||
const std::vector< std::string > &Index::getColumnNames() const { return m_columns; }
|
||||
|
||||
std::string Index::creationQuery(const Table &table, Backend) const {
|
||||
assert(!m_name.empty());
|
||||
assert(!m_columns.empty());
|
||||
|
||||
std::string query = "CREATE INDEX \"" + m_name + "\" ON \"" + table.getName() + "\" (";
|
||||
|
||||
for (std::size_t i = 0; i < m_columns.size(); ++i) {
|
||||
query += m_columns[i];
|
||||
|
||||
if (i + 1 < m_columns.size()) {
|
||||
query += ", ";
|
||||
}
|
||||
}
|
||||
|
||||
query += ")";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::string Index::dropQuery(const Table &table, Backend backend) const {
|
||||
switch (backend) {
|
||||
case Backend::SQLite:
|
||||
// Fallthrough
|
||||
case Backend::PostgreSQL:
|
||||
return "DROP INDEX \"" + m_name + "\"";
|
||||
case Backend::MySQL:
|
||||
return "DROP INDEX \"" + m_name + "\" ON \"" + table.getName() + "\"";
|
||||
}
|
||||
|
||||
// This code should be unreachable
|
||||
assert(false);
|
||||
return "";
|
||||
}
|
||||
|
||||
bool operator==(const Index &lhs, const Index &rhs) {
|
||||
return lhs.m_name == rhs.m_name && lhs.m_columns == rhs.m_columns;
|
||||
}
|
||||
|
||||
bool operator!=(const Index &lhs, const Index &rhs) { return !(lhs == rhs); }
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
41
src/database/Index.h
Normal file
41
src/database/Index.h
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_INDEX_H_
|
||||
#define MUMBLE_DATABASE_INDEX_H_
|
||||
|
||||
#include "Backend.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
class Table;
|
||||
|
||||
class Index {
|
||||
public:
|
||||
Index(const std::string &name, const std::vector< std::string > &columns);
|
||||
Index(const std::string &name, std::vector< std::string > &&columns);
|
||||
|
||||
const std::string &getName() const;
|
||||
const std::vector< std::string > &getColumnNames() const;
|
||||
|
||||
std::string creationQuery(const Table &table, Backend backend) const;
|
||||
std::string dropQuery(const Table &table, Backend backend) const;
|
||||
|
||||
friend bool operator==(const Index &lhs, const Index &rhs);
|
||||
friend bool operator!=(const Index &lhs, const Index &rhs);
|
||||
|
||||
protected:
|
||||
std::string m_name;
|
||||
std::vector< std::string > m_columns;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_INDEX_H_
|
||||
24
src/database/InitException.h
Normal file
24
src/database/InitException.h
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_INITEXCEPTION_H_
|
||||
#define MUMBLE_DATABASE_INITEXCEPTION_H_
|
||||
|
||||
#include "Exception.h"
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
/**
|
||||
* Thrown whenever an error occurs during database initialization
|
||||
*/
|
||||
struct InitException : public Exception {
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_INITEXCEPTION_H_
|
||||
50
src/database/MetaTable.cpp
Normal file
50
src/database/MetaTable.cpp
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "MetaTable.h"
|
||||
#include "AccessException.h"
|
||||
#include "Column.h"
|
||||
#include "DataType.h"
|
||||
#include "InitException.h"
|
||||
|
||||
#include <soci/soci.h>
|
||||
|
||||
#include "FormatException.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
constexpr const char *MetaTable::NAME;
|
||||
|
||||
MetaTable::MetaTable(soci::session &sql, Backend backend)
|
||||
: Table(sql, backend, MetaTable::NAME,
|
||||
{ Column("meta_key", DataType(DataType::String, 500),
|
||||
{ Constraint(Constraint::Unique, std::string(MetaTable::NAME) + "_unique_key"),
|
||||
Constraint(Constraint::NotNull), Constraint(Constraint::PrimaryKey) }),
|
||||
Column("meta_value", DataType(DataType::String, 5000), { Constraint(Constraint::NotNull) }) }) {}
|
||||
|
||||
unsigned int MetaTable::getSchemeVersion() {
|
||||
try {
|
||||
unsigned int version = 0;
|
||||
m_sql << "SELECT meta_value FROM " << m_name << " WHERE meta_key='version'", soci::into(version);
|
||||
|
||||
return version;
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void MetaTable::setSchemeVersion(unsigned int version) {
|
||||
try {
|
||||
m_sql << "INSERT INTO \"" << m_name << "\" (meta_key, meta_value) VALUES ('scheme_version', :version)",
|
||||
soci::use(version);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
27
src/database/MetaTable.h
Normal file
27
src/database/MetaTable.h
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_METATABLE_H_
|
||||
#define MUMBLE_DATABASE_METATABLE_H_
|
||||
|
||||
#include "Backend.h"
|
||||
#include "Table.h"
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
class MetaTable : public Table {
|
||||
public:
|
||||
static constexpr const char *NAME = "meta";
|
||||
MetaTable(soci::session &sql, Backend backend);
|
||||
|
||||
unsigned int getSchemeVersion();
|
||||
void setSchemeVersion(unsigned int version);
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_METATABLE_H_
|
||||
20
src/database/MySQLConnectionParameter.cpp
Normal file
20
src/database/MySQLConnectionParameter.cpp
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "database/MySQLConnectionParameter.h"
|
||||
#include "database/Backend.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
MySQLConnectionParameter::MySQLConnectionParameter(const std::string &dbName) : dbName(dbName) {}
|
||||
|
||||
Backend MySQLConnectionParameter::applicability() const { return Backend::MySQL; }
|
||||
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
33
src/database/MySQLConnectionParameter.h
Normal file
33
src/database/MySQLConnectionParameter.h
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_MYSQLCONNECTIONPARAMETER_H_
|
||||
#define MUMBLE_DATABASE_MYSQLCONNECTIONPARAMETER_H_
|
||||
|
||||
#include "database/Backend.h"
|
||||
#include "database/ConnectionParameter.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
class MySQLConnectionParameter : public ConnectionParameter {
|
||||
public:
|
||||
MySQLConnectionParameter(const std::string &dbName);
|
||||
|
||||
std::string dbName;
|
||||
std::string userName = "";
|
||||
std::string password = "";
|
||||
std::string host = "";
|
||||
std::string port = "";
|
||||
|
||||
virtual Backend applicability() const override;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_MYSQLCONNECTIONPARAMETER_H_
|
||||
23
src/database/NoDataException.h
Normal file
23
src/database/NoDataException.h
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_NODATAEXCEPTION_H_
|
||||
#define MUMBLE_DATABASE_NODATAEXCEPTION_H_
|
||||
|
||||
#include "AccessException.h"
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
/**
|
||||
* Thrown whenever a query does not result in any (usable) data
|
||||
*/
|
||||
struct NoDataException : public AccessException {
|
||||
using AccessException::AccessException;
|
||||
};
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_NODATAEXCEPTION_H_
|
||||
20
src/database/PostgreSQLConnectionParameter.cpp
Normal file
20
src/database/PostgreSQLConnectionParameter.cpp
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "database/PostgreSQLConnectionParameter.h"
|
||||
#include "database/Backend.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
PostgreSQLConnectionParameter::PostgreSQLConnectionParameter(const std::string &dbName) : dbName(dbName) {}
|
||||
|
||||
Backend PostgreSQLConnectionParameter::applicability() const { return Backend::PostgreSQL; }
|
||||
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
34
src/database/PostgreSQLConnectionParameter.h
Normal file
34
src/database/PostgreSQLConnectionParameter.h
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_POSTGRESQLCONNECTIONPARAMETER_H_
|
||||
#define MUMBLE_DATABASE_POSTGRESQLCONNECTIONPARAMETER_H_
|
||||
|
||||
#include "database/Backend.h"
|
||||
#include "database/ConnectionParameter.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
class PostgreSQLConnectionParameter : public ConnectionParameter {
|
||||
public:
|
||||
PostgreSQLConnectionParameter(const std::string &dbName);
|
||||
|
||||
std::string dbName;
|
||||
std::string userName = "";
|
||||
std::string password = "";
|
||||
// Explicitly use localhost IP to circumvent peer authentication errors
|
||||
std::string host = "127.0.0.1";
|
||||
std::string port = "";
|
||||
|
||||
virtual Backend applicability() const override;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_POSTGRESQLCONNECTIONPARAMETER_H_
|
||||
25
src/database/SQLiteConnectionParameter.cpp
Normal file
25
src/database/SQLiteConnectionParameter.cpp
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "SQLiteConnectionParameter.h"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
SQLiteConnectionParameter::SQLiteConnectionParameter(const std::string &dbPath, bool useWAL) : useWAL(useWAL) {
|
||||
// Make sure the proper file extension is used
|
||||
if (!boost::iends_with(dbPath, ".sqlite")) {
|
||||
this->dbPath = dbPath + ".sqlite";
|
||||
} else {
|
||||
this->dbPath = dbPath;
|
||||
}
|
||||
}
|
||||
|
||||
Backend SQLiteConnectionParameter::applicability() const { return Backend::SQLite; }
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
31
src/database/SQLiteConnectionParameter.h
Normal file
31
src/database/SQLiteConnectionParameter.h
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_SQLITECONNECTIONPARAMTER_H_
|
||||
#define MUMBLE_DATABASE_SQLITECONNECTIONPARAMTER_H_
|
||||
|
||||
#include "database/Backend.h"
|
||||
#include "database/ConnectionParameter.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
class SQLiteConnectionParameter : public ConnectionParameter {
|
||||
public:
|
||||
SQLiteConnectionParameter(const std::string &dbPath, bool useWAL = true);
|
||||
|
||||
std::string dbPath;
|
||||
/** Whether to use write-ahead logging */
|
||||
bool useWAL;
|
||||
|
||||
virtual Backend applicability() const override;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_SQLITECONNECTIONPARAMTER_H_
|
||||
1
src/database/TODO
Normal file
1
src/database/TODO
Normal file
@ -0,0 +1 @@
|
||||
- Test DB migration (in general)
|
||||
418
src/database/Table.cpp
Normal file
418
src/database/Table.cpp
Normal file
@ -0,0 +1,418 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "Table.h"
|
||||
#include "AccessException.h"
|
||||
#include "DBUtils.h"
|
||||
#include "FormatException.h"
|
||||
|
||||
#include <soci/soci.h>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
// If this looks weird to you, check out https://stackoverflow.com/a/8016853
|
||||
// (Essentially this is needed to avoid an undefined reference error for this constant)
|
||||
constexpr const char *Table::BACKUP_SUFFIX;
|
||||
|
||||
Table::Table(soci::session &sql, Backend backend) : Table(sql, backend, std::string{}, {}) {}
|
||||
Table::Table(soci::session &sql, Backend backend, const std::string &name) : Table(sql, backend, name, {}) {}
|
||||
Table::Table(soci::session &sql, Backend backend, const std::string &name, const std::vector< Column > &columns)
|
||||
: m_name(name), m_columns(columns), m_sql(sql), m_backend(backend) {
|
||||
performCtorAssertions();
|
||||
}
|
||||
|
||||
const std::string &Table::getName() const { return m_name; }
|
||||
void Table::setName(const std::string &name) { m_name = name; }
|
||||
|
||||
const std::vector< Column > &Table::getColumns() const { return m_columns; }
|
||||
|
||||
void Table::setColumns(const std::vector< Column > &columns) {
|
||||
// we don't support overwriting columns after they have been initialized already
|
||||
assert(m_columns.empty());
|
||||
|
||||
m_columns = columns;
|
||||
}
|
||||
|
||||
const Column *Table::findColumn(const std::string &name) const {
|
||||
for (const Column ¤tCol : m_columns) {
|
||||
if (currentCol.getName() == name) {
|
||||
return ¤tCol;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool Table::containsColumn(const std::string &name) const { return findColumn(name) != nullptr; }
|
||||
|
||||
void Table::create() {
|
||||
assert(!m_name.empty());
|
||||
assert(!m_columns.empty());
|
||||
|
||||
std::string createQuery = "CREATE TABLE \"" + m_name + "\" (";
|
||||
std::string outOfLineConstraints;
|
||||
|
||||
for (const Column ¤tColumn : m_columns) {
|
||||
createQuery += currentColumn.getName() + " " + currentColumn.getType().sqlRepresentation();
|
||||
|
||||
if (currentColumn.hasDefaultValue()) {
|
||||
createQuery += " DEFAULT ";
|
||||
std::string defaultValue = currentColumn.getDefaultValue();
|
||||
if (currentColumn.getType().getType() == DataType::String
|
||||
|| currentColumn.getType().getType() == DataType::FixedSizeString) {
|
||||
// Escape single quotes by doubling them up
|
||||
boost::replace_all(defaultValue, "'", "''");
|
||||
|
||||
// We'll have to wrap the value in quotes in order to make sure it gets recognized as a String
|
||||
createQuery += "'" + defaultValue + "'";
|
||||
} else {
|
||||
// If the default is not a String-type, we don't want to see spaces in it
|
||||
assert(defaultValue.find(" ") == std::string::npos);
|
||||
|
||||
createQuery += defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const Constraint ¤tConstraint : currentColumn.getConstraints()) {
|
||||
if (currentConstraint.canInline()
|
||||
&& (currentConstraint.prefersInline() || !currentConstraint.canOutOfLine())) {
|
||||
createQuery += " " + currentConstraint.inlineSQL(m_backend);
|
||||
} else {
|
||||
assert(currentConstraint.canOutOfLine());
|
||||
|
||||
outOfLineConstraints += currentConstraint.outOfLineSQL(currentColumn, m_backend) + ", ";
|
||||
}
|
||||
}
|
||||
|
||||
createQuery += ", ";
|
||||
}
|
||||
|
||||
// Append out-of-line constraints
|
||||
createQuery += outOfLineConstraints;
|
||||
|
||||
// Remove trailing ", "
|
||||
createQuery.erase(createQuery.size() - 2);
|
||||
|
||||
createQuery += ")";
|
||||
|
||||
try {
|
||||
m_sql << createQuery;
|
||||
|
||||
// Also create all necessary indices
|
||||
for (const Index ¤tIndex : m_indices) {
|
||||
m_sql << currentIndex.creationQuery(*this, m_backend);
|
||||
}
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void Table::migrate(unsigned int fromSchemeVersion, unsigned int toSchemeVersion) {
|
||||
(void) fromSchemeVersion;
|
||||
(void) toSchemeVersion;
|
||||
// The default implementation does nothing
|
||||
}
|
||||
|
||||
void Table::destroy() {
|
||||
assert(!m_name.empty());
|
||||
|
||||
try {
|
||||
m_sql << "DROP TABLE \"" << m_name + "\"";
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void Table::clear() {
|
||||
assert(!m_name.empty());
|
||||
|
||||
try {
|
||||
// Note that thanks to the missing WHERE clause, this deletes all rows in this table
|
||||
m_sql << "DELETE FROM \"" << m_name + "\"";
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector< Index > &Table::getIndices() const { return m_indices; }
|
||||
|
||||
void Table::addIndex(const Index &index, bool applyToDB) {
|
||||
if (applyToDB) {
|
||||
try {
|
||||
m_sql << index.creationQuery(*this, m_backend);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException("Failed at creating index \"" + index.getName() + "\": " + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
m_indices.push_back(index);
|
||||
}
|
||||
|
||||
bool Table::removeIndex(const Index &index, bool applyToDB) {
|
||||
auto it = std::find(m_indices.begin(), m_indices.end(), index);
|
||||
if (it == m_indices.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_indices.erase(it);
|
||||
|
||||
if (applyToDB) {
|
||||
try {
|
||||
m_sql << index.dropQuery(*this, m_backend);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException("Failed at dropping index \"" + index.getName() + "\": " + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::vector< Trigger > &Table::getTrigger() const { return m_trigger; }
|
||||
|
||||
void Table::addTrigger(const Trigger &trigger, bool applyToDB) {
|
||||
if (applyToDB) {
|
||||
try {
|
||||
m_sql << trigger.creationQuery(*this, m_backend);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException("Failed at creating trigger \"" + trigger.getName() + "\": " + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
m_trigger.push_back(trigger);
|
||||
}
|
||||
|
||||
bool Table::removeTrigger(const Trigger &trigger, bool applyToDB) {
|
||||
auto it = std::find(m_trigger.begin(), m_trigger.end(), trigger);
|
||||
if (it == m_trigger.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_trigger.erase(it);
|
||||
|
||||
if (applyToDB) {
|
||||
try {
|
||||
m_sql << trigger.dropQuery(*this, m_backend);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException("Failed at dropping trigger \"" + trigger.getName() + "\": " + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#define THROW_FORMATERROR(msg) throw FormatException(std::string("JSON-Import (table \"") + m_name + "\"): " + msg)
|
||||
void Table::importFromJSON(const nlohmann::json &json, bool create) {
|
||||
assert(!m_name.empty());
|
||||
|
||||
if (!json.is_object()) {
|
||||
THROW_FORMATERROR("Expected table to represented as a single JSON object");
|
||||
}
|
||||
// Validate that the expected fields are present and of the expected type
|
||||
std::vector< std::pair< std::string, nlohmann::json::value_t > > expectedFields = {
|
||||
{ "column_names", nlohmann::json::value_t::array },
|
||||
{ "column_types", nlohmann::json::value_t::array },
|
||||
{ "rows", nlohmann::json::value_t::array }
|
||||
};
|
||||
for (const std::pair< std::string, nlohmann::json::value_t > ¤tPair : expectedFields) {
|
||||
if (!json.contains(currentPair.first)) {
|
||||
THROW_FORMATERROR("Table specification is missing the \"" + currentPair.first + "\" field");
|
||||
}
|
||||
if (json[currentPair.first].type() != currentPair.second) {
|
||||
THROW_FORMATERROR("Field \"" + currentPair.first + "\" is of the wrong type");
|
||||
}
|
||||
}
|
||||
// Validate that there are no extra fields
|
||||
if (json.size() > expectedFields.size()) {
|
||||
THROW_FORMATERROR("Table spec is expected to contain only " + std::to_string(expectedFields.size())
|
||||
+ " but contained " + std::to_string(json.size()));
|
||||
}
|
||||
|
||||
const nlohmann::json &colNames = json["column_names"];
|
||||
const nlohmann::json &colTypes = json["column_types"];
|
||||
const nlohmann::json &rows = json["rows"];
|
||||
|
||||
// Some more validations
|
||||
if (colNames.size() != colTypes.size()) {
|
||||
THROW_FORMATERROR("Amount of column names (" + std::to_string(colNames.size())
|
||||
+ " does not match column types (" + std::to_string(colTypes.size()) + ")");
|
||||
}
|
||||
for (std::size_t i = 0; i < colNames.size(); ++i) {
|
||||
if (!colNames[i].is_string()) {
|
||||
THROW_FORMATERROR("Encountered non-string column name specification at position "
|
||||
+ std::to_string(i + 1));
|
||||
}
|
||||
if (!colTypes[i].is_string()) {
|
||||
THROW_FORMATERROR("Encountered non-string column type specification at position "
|
||||
+ std::to_string(i + 1));
|
||||
}
|
||||
if (boost::contains(colNames[i].get< std::string >(), " ")) {
|
||||
THROW_FORMATERROR("Invalid column name \"" + colNames[i].get< std::string >() + "\"");
|
||||
}
|
||||
try {
|
||||
// Check if we can convert the given string to a known data type
|
||||
DataType::fromSQLRepresentation(colTypes[i].get< std::string >());
|
||||
} catch (const UnknownDataTypeException &e) {
|
||||
THROW_FORMATERROR("Unknown column type \"" + colTypes[i].get< std::string >() + "\" for column \""
|
||||
+ colNames[i].get< std::string >() + "\": " + e.what());
|
||||
}
|
||||
}
|
||||
for (std::size_t i = 0; i < rows.size(); ++i) {
|
||||
const nlohmann::json ¤tRow = rows.at(i);
|
||||
|
||||
if (!currentRow.is_array()) {
|
||||
THROW_FORMATERROR("Row entry " + std::to_string(i + 1) + " is not of type array");
|
||||
}
|
||||
if (currentRow.size() != colNames.size()) {
|
||||
THROW_FORMATERROR("Row " + std::to_string(i + 1) + " contains " + std::to_string(currentRow.size())
|
||||
+ " entries, but " + std::to_string(colNames.size()) + " were expected");
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_columns.empty()) {
|
||||
// Make sure that the specified columns and types match with our stored specification
|
||||
if (m_columns.size() != colNames.size()) {
|
||||
THROW_FORMATERROR("Attempted to import " + std::to_string(colNames.size())
|
||||
+ " into a pre-defined table that only contains " + std::to_string(m_columns.size())
|
||||
+ " columns");
|
||||
}
|
||||
for (std::size_t i = 0; i < m_columns.size(); ++i) {
|
||||
std::string currentName = colNames[i].get< std::string >();
|
||||
const Column *col = findColumn(currentName);
|
||||
|
||||
if (!col) {
|
||||
THROW_FORMATERROR("A column with the name \"" + currentName
|
||||
+ "\" is not part of the pre-defined columns for this table");
|
||||
}
|
||||
if (col->getType().sqlRepresentation() != colTypes[i].get< std::string >()) {
|
||||
THROW_FORMATERROR("Column type mismatch for column \"" + currentName + "\": Expected: \""
|
||||
+ col->getType().sqlRepresentation() + "\", got \""
|
||||
+ colTypes[i].get< std::string >() + "\"");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Import columns as specified
|
||||
m_columns.resize(colNames.size());
|
||||
for (std::size_t i = 0; i < colNames.size(); ++i) {
|
||||
Column column(colNames[i].get< std::string >(),
|
||||
DataType::fromSQLRepresentation(colTypes[i].get< std::string >()));
|
||||
|
||||
m_columns[i] = std::move(column);
|
||||
}
|
||||
}
|
||||
|
||||
if (create) {
|
||||
// Now we have all information together that we need in order to create the table
|
||||
this->create();
|
||||
}
|
||||
|
||||
// From this point on we are assuming that the table represented by this object actually exists in the
|
||||
// respective database, so we can now start inserting the provided data into it.
|
||||
std::string query = "INSERT INTO \"" + m_name + "\" (";
|
||||
std::string valuePlaceholder = "";
|
||||
for (std::size_t i = 0; i < colNames.size(); ++i) {
|
||||
query += colNames[i].get< std::string >();
|
||||
valuePlaceholder += ":" + colNames[i].get< std::string >();
|
||||
|
||||
if (i + 1 < colNames.size()) {
|
||||
query += ", ";
|
||||
valuePlaceholder += ", ";
|
||||
}
|
||||
}
|
||||
query += ") VALUES(" + valuePlaceholder + ")";
|
||||
|
||||
// We assume that this function will be called from a place that already started a transaction
|
||||
// on the DB, so we can't initiate another one here.
|
||||
soci::statement stmt = m_sql.prepare << query;
|
||||
|
||||
std::vector< std::string > values;
|
||||
values.reserve(colNames.size());
|
||||
for (const nlohmann::json ¤tRow : rows) {
|
||||
assert(currentRow.size() == colNames.size());
|
||||
|
||||
// We have to first transfer our values into the values vector in order to guarantee that they
|
||||
// are not destroyed in the middle of the DB statement (which might happen, if we were to use
|
||||
// the temporaries directly)
|
||||
for (const nlohmann::json ¤tVal : currentRow) {
|
||||
values.push_back(utils::to_string(currentVal));
|
||||
}
|
||||
for (std::size_t i = 0; i < values.size(); ++i) {
|
||||
stmt.exchange(soci::use(values[i]));
|
||||
}
|
||||
|
||||
stmt.define_and_bind();
|
||||
stmt.execute(true);
|
||||
stmt.bind_clean_up();
|
||||
|
||||
values.clear();
|
||||
}
|
||||
}
|
||||
#undef THROW_FORMATERROR
|
||||
|
||||
nlohmann::json Table::exportToJSON() {
|
||||
assert(!m_columns.empty());
|
||||
assert(!m_name.empty());
|
||||
|
||||
nlohmann::json json;
|
||||
|
||||
std::string query = "SELECT ";
|
||||
for (const Column ¤tColumn : m_columns) {
|
||||
json["column_names"].push_back(currentColumn.getName());
|
||||
json["column_types"].push_back(currentColumn.getType().sqlRepresentation());
|
||||
|
||||
query += currentColumn.getName() + ", ";
|
||||
}
|
||||
// Remove trailing ", "
|
||||
query.erase(query.size() - 2);
|
||||
|
||||
query += " FROM \"" + m_name + "\"";
|
||||
|
||||
nlohmann::json rows = nlohmann::json::array_t();
|
||||
|
||||
try {
|
||||
soci::rowset< soci::row > rowSet = m_sql.prepare << query;
|
||||
|
||||
for (auto it = rowSet.begin(); it != rowSet.end(); ++it) {
|
||||
const soci::row ¤tRow = *it;
|
||||
|
||||
nlohmann::json jsonRow = nlohmann::json::array_t();
|
||||
for (std::size_t i = 0; i < currentRow.size(); ++i) {
|
||||
jsonRow.push_back(utils::to_json(currentRow, i));
|
||||
}
|
||||
|
||||
rows.push_back(std::move(jsonRow));
|
||||
}
|
||||
|
||||
json["rows"] = std::move(rows);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(e.what());
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
void Table::performCtorAssertions() {
|
||||
// Names with spaces are not allowed as these cause issues
|
||||
assert(!boost::contains(m_name, " "));
|
||||
#ifndef NDEBUG
|
||||
for (const Column ¤tColumn : m_columns) {
|
||||
assert(!boost::contains(currentColumn.getName(), " "));
|
||||
}
|
||||
#endif
|
||||
|
||||
// We reserve the name for a table's backup (needed during migrations) right from the start
|
||||
assert(!boost::ends_with(m_name, Table::BACKUP_SUFFIX));
|
||||
}
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
82
src/database/Table.h
Normal file
82
src/database/Table.h
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_TABLE_H_
|
||||
#define MUMBLE_DATABASE_TABLE_H_
|
||||
|
||||
#include "Backend.h"
|
||||
#include "Column.h"
|
||||
#include "Index.h"
|
||||
#include "Trigger.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
namespace soci {
|
||||
class session;
|
||||
};
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
class Table {
|
||||
public:
|
||||
static constexpr const char *BACKUP_SUFFIX = "_backup";
|
||||
|
||||
Table(soci::session &sql, Backend backend);
|
||||
Table(soci::session &sql, Backend backend, const std::string &name);
|
||||
Table(soci::session &sql, Backend backend, const std::string &name, const std::vector< Column > &columns);
|
||||
virtual ~Table() = default;
|
||||
|
||||
const std::string &getName() const;
|
||||
void setName(const std::string &name);
|
||||
|
||||
const std::vector< Column > &getColumns() const;
|
||||
void setColumns(const std::vector< Column > &columns);
|
||||
const Column *findColumn(const std::string &name) const;
|
||||
bool containsColumn(const std::string &name) const;
|
||||
|
||||
virtual void create();
|
||||
virtual void migrate(unsigned int fromSchemeVersion, unsigned int toSchemeVersion);
|
||||
virtual void destroy();
|
||||
virtual void clear();
|
||||
|
||||
const std::vector< Index > &getIndices() const;
|
||||
void addIndex(const Index &index, bool applyToDB);
|
||||
bool removeIndex(const Index &index, bool applyToDB);
|
||||
|
||||
const std::vector< Trigger > &getTrigger() const;
|
||||
void addTrigger(const Trigger &trigger, bool applyToDB);
|
||||
bool removeTrigger(const Trigger &trigger, bool applyToDB);
|
||||
|
||||
/**
|
||||
* Imports the data from the given JSON into the table represented by this object. Note
|
||||
* that the caller of this function is expected to already have initiated a database
|
||||
* transaction to prevent a partial import.
|
||||
*
|
||||
* @param json The data to import
|
||||
* @param create Whether this function shall create the represented table in the associated
|
||||
* database before starting to import data.
|
||||
*/
|
||||
virtual void importFromJSON(const nlohmann::json &json, bool create = false);
|
||||
virtual nlohmann::json exportToJSON();
|
||||
|
||||
protected:
|
||||
std::string m_name;
|
||||
std::vector< Column > m_columns;
|
||||
soci::session &m_sql;
|
||||
Backend m_backend;
|
||||
std::vector< Index > m_indices;
|
||||
std::vector< Trigger > m_trigger;
|
||||
|
||||
void performCtorAssertions();
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_TABLE_H_
|
||||
102
src/database/Trigger.cpp
Normal file
102
src/database/Trigger.cpp
Normal file
@ -0,0 +1,102 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "Trigger.h"
|
||||
#include "Table.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
Trigger::Trigger(const std::string &name, Trigger::Timing timing, Trigger::Event event,
|
||||
const std::string &triggerBody)
|
||||
: m_name(name), m_timing(timing), m_event(event), m_triggerBody(triggerBody) {}
|
||||
|
||||
const std::string &Trigger::getName() const { return m_name; }
|
||||
|
||||
void Trigger::setName(const std::string &name) { m_name = name; }
|
||||
|
||||
Trigger::Timing Trigger::getTiming() const { return m_timing; }
|
||||
|
||||
void Trigger::setTiming(Trigger::Timing timing) { m_timing = timing; }
|
||||
|
||||
Trigger::Event Trigger::getEvent() const { return m_event; }
|
||||
|
||||
void Trigger::setEvent(Trigger::Event event) { m_event = event; }
|
||||
|
||||
const std::string &Trigger::getBody() const { return m_triggerBody; }
|
||||
|
||||
void Trigger::setBody(const std::string &body) { m_triggerBody = body; }
|
||||
|
||||
std::string Trigger::creationQuery(const Table &table, Backend backend) const {
|
||||
std::string query = "CREATE TRIGGER \"" + m_name + "\"";
|
||||
switch (m_timing) {
|
||||
case Trigger::Timing::Before:
|
||||
query += " BEFORE ";
|
||||
break;
|
||||
case Trigger::Timing::After:
|
||||
query += " AFTER ";
|
||||
break;
|
||||
}
|
||||
|
||||
switch (m_event) {
|
||||
case Trigger::Event::Insert:
|
||||
query += " INSERT ";
|
||||
break;
|
||||
case Trigger::Event::Update:
|
||||
query += " UPDATE ";
|
||||
break;
|
||||
case Trigger::Event::Delete:
|
||||
query += " DELETE ";
|
||||
break;
|
||||
}
|
||||
|
||||
query += "ON \"" + table.getName() + "\" FOR EACH ROW ";
|
||||
switch (backend) {
|
||||
case Backend::SQLite:
|
||||
// Fallthrough
|
||||
case Backend::MySQL:
|
||||
query += "BEGIN " + m_triggerBody + " END";
|
||||
break;
|
||||
case Backend::PostgreSQL:
|
||||
// Postgres requires us to create a function that can then be executed by the trigger
|
||||
query = "CREATE FUNCTION \"" + m_name + "_trigger_function\""
|
||||
+ "() RETURNS TRIGGER LANGUAGE PLPGSQL AS $$ BEGIN " + m_triggerBody + " END; $$; " + query
|
||||
+ "EXECUTE PROCEDURE \"" + m_name + "_trigger_function\"()";
|
||||
break;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::string Trigger::dropQuery(const Table &table, Backend backend) const {
|
||||
switch (backend) {
|
||||
case Backend::MySQL:
|
||||
// Fallthrough
|
||||
case Backend::SQLite:
|
||||
return "DROP TRIGGER \"" + m_name + "\"";
|
||||
case Backend::PostgreSQL:
|
||||
std::string query = "DROP TRIGGER \"" + m_name + "\" ON \"" + table.getName() + "\";";
|
||||
|
||||
// Also drop the function that we created for this trigger
|
||||
query += " DROP FUNCTION \"" + m_name + "_trigger_function\"()";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
// This code should be unreachable
|
||||
assert(false);
|
||||
|
||||
return "DROP TRIGGER \"" + m_name + "\"";
|
||||
}
|
||||
|
||||
bool operator==(const Trigger &lhs, const Trigger &rhs) {
|
||||
return lhs.m_name == rhs.m_name && lhs.m_timing == rhs.m_timing && lhs.m_event == rhs.m_event
|
||||
&& lhs.m_triggerBody == rhs.m_triggerBody;
|
||||
}
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
61
src/database/Trigger.h
Normal file
61
src/database/Trigger.h
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_TRIGGER_H_
|
||||
#define MUMBLE_DATABASE_TRIGGER_H_
|
||||
|
||||
#include "Backend.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
class Table;
|
||||
|
||||
class Trigger {
|
||||
public:
|
||||
enum class Timing {
|
||||
Before,
|
||||
After,
|
||||
};
|
||||
enum class Event {
|
||||
Insert,
|
||||
Update,
|
||||
Delete,
|
||||
};
|
||||
|
||||
Trigger(const std::string &name = "", Timing timing = Timing::After, Event event = Event::Insert,
|
||||
const std::string &triggerBody = "");
|
||||
|
||||
const std::string &getName() const;
|
||||
void setName(const std::string &name);
|
||||
|
||||
Timing getTiming() const;
|
||||
void setTiming(Timing timing);
|
||||
|
||||
Event getEvent() const;
|
||||
void setEvent(Event event);
|
||||
|
||||
const std::string &getBody() const;
|
||||
void setBody(const std::string &body);
|
||||
|
||||
std::string creationQuery(const Table &table, Backend backend) const;
|
||||
std::string dropQuery(const Table &table, Backend backend) const;
|
||||
|
||||
friend bool operator==(const Trigger &lhs, const Trigger &rhs);
|
||||
friend bool operator!=(const Trigger &lhs, const Trigger &rhs);
|
||||
|
||||
protected:
|
||||
std::string m_name;
|
||||
Timing m_timing;
|
||||
Event m_event;
|
||||
std::string m_triggerBody;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_TRIGGER_H_
|
||||
24
src/database/UnsupportedOperationException.h
Normal file
24
src/database/UnsupportedOperationException.h
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_UNSUPPORTEDOPERATIONEXCEPTION_H_
|
||||
#define MUMBLE_DATABASE_UNSUPPORTEDOPERATIONEXCEPTION_H_
|
||||
|
||||
#include "Exception.h"
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
/**
|
||||
* Thrown whenever an operation is not supported (in the current context)
|
||||
*/
|
||||
struct UnsupportedOperationException : public Exception {
|
||||
using Exception::Exception;
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_UNSUPPORTEDOPERATIONEXCEPTION_H_
|
||||
33
src/database/Utils.h
Normal file
33
src/database/Utils.h
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_UTILS_H_
|
||||
#define MUMBLE_DATABASE_UTILS_H_
|
||||
|
||||
#include "NoDataException.h"
|
||||
|
||||
#include <soci/soci.h>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
namespace utils {
|
||||
|
||||
/**
|
||||
* Tests the given SQL session to see if the last query resulted in any data having been fetched. If not,
|
||||
* this function will throw an exception of the given type.
|
||||
*
|
||||
* @tparam Exception The type of the exception that is to be thrown
|
||||
*/
|
||||
template< typename Exception = NoDataException > void verifyQueryResultedInData(soci::session &sql) {
|
||||
if (!sql.got_data()) {
|
||||
throw Exception("Query did not result in any data having been fetched from the database: "
|
||||
+ sql.get_last_query());
|
||||
}
|
||||
}
|
||||
} // namespace utils
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_UTILS_H_
|
||||
42
src/database/Version.cpp
Normal file
42
src/database/Version.cpp
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "Version.h"
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
Version::Version(unsigned int majorVersion, unsigned int minorVersion, unsigned int patchVersion)
|
||||
: m_major(majorVersion), m_minor(minorVersion), m_patch(patchVersion) {}
|
||||
|
||||
std::string Version::toString() const {
|
||||
return std::to_string(m_major) + "." + std::to_string(m_minor) + "." + std::to_string(m_patch);
|
||||
}
|
||||
|
||||
bool operator==(const Version &lhs, const Version &rhs) {
|
||||
return lhs.m_major == rhs.m_major && lhs.m_minor == rhs.m_minor && lhs.m_patch == rhs.m_patch;
|
||||
}
|
||||
|
||||
bool operator!=(const Version &lhs, const Version &rhs) { return !(lhs == rhs); }
|
||||
|
||||
bool operator<(const Version &lhs, const Version &rhs) {
|
||||
if (lhs.m_major != rhs.m_major) {
|
||||
return lhs.m_major < rhs.m_major;
|
||||
}
|
||||
if (lhs.m_minor != rhs.m_minor) {
|
||||
return lhs.m_minor < rhs.m_minor;
|
||||
}
|
||||
|
||||
return lhs.m_patch < rhs.m_patch;
|
||||
}
|
||||
|
||||
bool operator<=(const Version &lhs, const Version &rhs) { return lhs == rhs || lhs < rhs; }
|
||||
|
||||
bool operator>(const Version &lhs, const Version &rhs) { return lhs != rhs && !(lhs < rhs); }
|
||||
|
||||
bool operator>=(const Version &lhs, const Version &rhs) { return lhs == rhs || !(lhs < rhs); }
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
35
src/database/Version.h
Normal file
35
src/database/Version.h
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_DATABASE_VERSION_H_
|
||||
#define MUMBLE_DATABASE_VERSION_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
|
||||
struct Version {
|
||||
Version(unsigned int majorVersion = 0, unsigned int minorVersion = 0, unsigned int patchVersion = 0);
|
||||
~Version() = default;
|
||||
|
||||
unsigned int m_major = 0;
|
||||
unsigned int m_minor = 0;
|
||||
unsigned int m_patch = 0;
|
||||
|
||||
std::string toString() const;
|
||||
|
||||
friend bool operator==(const Version &lhs, const Version &rhs);
|
||||
friend bool operator!=(const Version &lhs, const Version &rhs);
|
||||
friend bool operator<(const Version &lhs, const Version &rhs);
|
||||
friend bool operator<=(const Version &lhs, const Version &rhs);
|
||||
friend bool operator>(const Version &lhs, const Version &rhs);
|
||||
friend bool operator>=(const Version &lhs, const Version &rhs);
|
||||
};
|
||||
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_DATABASE_VERSION_H_
|
||||
@ -499,13 +499,15 @@ else()
|
||||
endif()
|
||||
|
||||
|
||||
if(bundled-json)
|
||||
set(JSON_BuildTests OFF CACHE INTERNAL "")
|
||||
set(JSON_ImplicitConversions ON CACHE INTERNAL "")
|
||||
set(JSON_SystemInclude ON CACHE INTERNAL "")
|
||||
add_subdirectory("${3RDPARTY_DIR}/nlohmann_json/" "${CMAKE_CURRENT_BINARY_DIR}/nlohmann_json/" EXCLUDE_FROM_ALL)
|
||||
else()
|
||||
find_pkg("nlohmann_json" REQUIRED)
|
||||
if (NOT TARGET nlohmann_json)
|
||||
if(bundled-json)
|
||||
set(JSON_BuildTests OFF CACHE INTERNAL "")
|
||||
set(JSON_ImplicitConversions ON CACHE INTERNAL "")
|
||||
set(JSON_SystemInclude ON CACHE INTERNAL "")
|
||||
add_subdirectory("${3RDPARTY_DIR}/nlohmann_json/" "${CMAKE_CURRENT_BINARY_DIR}/nlohmann_json/" EXCLUDE_FROM_ALL)
|
||||
else()
|
||||
find_pkg("nlohmann_json" REQUIRED)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_link_libraries(mumble_client_object_lib PUBLIC nlohmann_json::nlohmann_json)
|
||||
|
||||
@ -30,6 +30,7 @@ endif()
|
||||
# Shared tests
|
||||
use_test("TestCryptographicHash")
|
||||
use_test("TestCryptographicRandom")
|
||||
use_test("TestDatabase")
|
||||
use_test("TestFFDHE")
|
||||
use_test("TestPacketDataStream")
|
||||
use_test("TestPasswordGenerator")
|
||||
|
||||
28
src/tests/TestDatabase/CMakeLists.txt
Normal file
28
src/tests/TestDatabase/CMakeLists.txt
Normal file
@ -0,0 +1,28 @@
|
||||
# Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license
|
||||
# that can be found in the LICENSE file at the root of the
|
||||
# Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
option(database-mysql-test "Whether to include the MySQL database tests (requires special setup)" OFF)
|
||||
option(database-postgresql-tests "Whether to include the PostgreSQL database tests (requires special setup)" OFF)
|
||||
|
||||
add_executable(TestDatabase
|
||||
DatabaseTest.cpp
|
||||
DefaultTable.cpp
|
||||
TestUtils.cpp
|
||||
KeyValueTable.cpp
|
||||
ConstraintTable.cpp
|
||||
)
|
||||
|
||||
if(database-mysql-tests)
|
||||
target_compile_definitions(TestDatabase PRIVATE MUMBLE_TEST_MYSQL)
|
||||
endif()
|
||||
if(database-postgresql-tests)
|
||||
target_compile_definitions(TestDatabase PRIVATE MUMBLE_TEST_POSTGRESQL)
|
||||
endif()
|
||||
|
||||
set_target_properties(TestDatabase PROPERTIES AUTOMOC ON)
|
||||
|
||||
target_link_libraries(TestDatabase PRIVATE Qt5::Test mumble_database)
|
||||
|
||||
add_test(NAME TestDatabase COMMAND $<TARGET_FILE:TestDatabase>)
|
||||
180
src/tests/TestDatabase/ConstraintTable.cpp
Normal file
180
src/tests/TestDatabase/ConstraintTable.cpp
Normal file
@ -0,0 +1,180 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "ConstraintTable.h"
|
||||
#include "database/AccessException.h"
|
||||
#include "database/Column.h"
|
||||
#include "database/Constraint.h"
|
||||
#include "database/Utils.h"
|
||||
|
||||
#include <soci/soci.h>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
namespace test {
|
||||
|
||||
constexpr const char *ConstraintTable::NAME;
|
||||
|
||||
/*
|
||||
* Note that since we are adding columns with NOT NULL, UNIQUE and PRIMARY KEY constraints, we have to make sure
|
||||
* that upon every insert that we are performing, we also add explicit values for these columns as otherwise the
|
||||
* respective constraints might easily get violated (e.g. because NULL is inserted as a default value or the
|
||||
* same value (NULL/EMPTY) is inserted multiple times.
|
||||
*/
|
||||
|
||||
ConstraintTable::ConstraintTable(soci::session &sql, Backend backend) : Table(sql, backend, NAME) {
|
||||
std::vector< Column > columns;
|
||||
|
||||
columns.push_back(Column("not_null_key", DataType(DataType::String, 100),
|
||||
{ Constraint(Constraint::NotNull) }, "myDefaultValue"));
|
||||
columns.push_back(Column("not_null_value", DataType(DataType::String, 100)));
|
||||
|
||||
columns.push_back(
|
||||
Column("unique_key", DataType(DataType::String, 100), { Constraint(Constraint::Unique) }));
|
||||
columns.push_back(Column("unique_value", DataType(DataType::String, 100)));
|
||||
|
||||
columns.push_back(
|
||||
Column("primary_key_key", DataType(DataType::String, 100), { Constraint(Constraint::PrimaryKey) }));
|
||||
std::size_t primaryKeyIndex = columns.size() - 1;
|
||||
columns.push_back(Column("primary_key_value", DataType(DataType::String, 100)));
|
||||
|
||||
columns.push_back(Column(
|
||||
"foreign_key_key", DataType(DataType::String, 100),
|
||||
{ Constraint(Constraint::ForeignKey, "myForeignKeyConstraint", this, &columns[primaryKeyIndex]) }));
|
||||
columns.push_back(Column("foreign_key_value", DataType(DataType::String, 100)));
|
||||
|
||||
setColumns(columns);
|
||||
}
|
||||
|
||||
void ConstraintTable::insertNotNull(const std::string &key, const std::string &value) {
|
||||
std::string dummy = "dummy" + std::to_string(dummyCounter++);
|
||||
try {
|
||||
m_sql << "INSERT INTO \"" + getName()
|
||||
+ "\" (not_null_key, not_null_value, unique_key, primary_key_key) VALUES (:key, :value, "
|
||||
":dummy1, :dummy2)",
|
||||
soci::use(key), soci::use(value), soci::use(dummy), soci::use(dummy);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at inserting not-null value: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ConstraintTable::insertUnique(const std::string &key, const std::string &value) {
|
||||
std::string dummy = "dummy" + std::to_string(dummyCounter++);
|
||||
try {
|
||||
m_sql << "INSERT INTO \"" + getName()
|
||||
+ "\" (unique_key, unique_value, primary_key_key) VALUES (:key, :value, :dummy)",
|
||||
soci::use(key), soci::use(value), soci::use(dummy);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at inserting unique value: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ConstraintTable::insertPrimaryKey(const std::string &key, const std::string &value) {
|
||||
std::string dummy = "dummy" + std::to_string(dummyCounter++);
|
||||
try {
|
||||
m_sql << "INSERT INTO \"" + getName()
|
||||
+ "\" (primary_key_key, primary_key_value, unique_key) VALUES (:key, :value, :dummy)",
|
||||
soci::use(key), soci::use(value), soci::use(dummy);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at inserting primary key value: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ConstraintTable::insertForeignKey(const std::string &key, const std::string &value) {
|
||||
std::string dummy = "dummy" + std::to_string(dummyCounter++);
|
||||
try {
|
||||
m_sql << "INSERT INTO \"" + getName()
|
||||
+ "\" (foreign_key_key, foreign_key_value, unique_key, primary_key_key) VALUES (:key, "
|
||||
":value, :dummy1, :dummy2)",
|
||||
soci::use(key), soci::use(value), soci::use(dummy), soci::use(dummy);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at inserting foreign key value: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::string ConstraintTable::selectNotNull(const std::string &key) {
|
||||
try {
|
||||
std::string value;
|
||||
m_sql << "SELECT not_null_value FROM \"" + getName() + "\" WHERE not_null_key = :key", soci::use(key),
|
||||
soci::into(value);
|
||||
|
||||
::mumble::db::utils::verifyQueryResultedInData(m_sql);
|
||||
|
||||
return value;
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at fetching not-null value: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::string ConstraintTable::selectUnique(const std::string &key) {
|
||||
try {
|
||||
std::string value;
|
||||
m_sql << "SELECT unique_value FROM \"" + getName() + "\" WHERE unique_key = :key", soci::use(key),
|
||||
soci::into(value);
|
||||
|
||||
::mumble::db::utils::verifyQueryResultedInData(m_sql);
|
||||
|
||||
return value;
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at fetching unique value: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::string ConstraintTable::selectPrimaryKey(const std::string &key) {
|
||||
try {
|
||||
std::string value;
|
||||
m_sql << "SELECT primary_key_value FROM \"" + getName() + "\" WHERE primary_key_key = :key",
|
||||
soci::use(key), soci::into(value);
|
||||
|
||||
::mumble::db::utils::verifyQueryResultedInData(m_sql);
|
||||
|
||||
return value;
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at fetching primary key value: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::string ConstraintTable::selectForeignKey(const std::string &key) {
|
||||
try {
|
||||
std::string value;
|
||||
m_sql << "SELECT foreign_key_value FROM \"" + getName() + "\" WHERE foreign_key_key = :key",
|
||||
soci::use(key), soci::into(value);
|
||||
|
||||
::mumble::db::utils::verifyQueryResultedInData(m_sql);
|
||||
|
||||
return value;
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at fetching foreign key value: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ConstraintTable::insertNullInNotNullCol() {
|
||||
try {
|
||||
m_sql << "INSERT INTO \"" + getName() + "\" (not_null_key, not_null_value) VALUES (NULL, 'NullValue')";
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at inserting NULL value in not-NULL col: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ConstraintTable::insertNullInPrimaryKeyCol() {
|
||||
try {
|
||||
m_sql << "INSERT INTO \"" + getName()
|
||||
+ "\" (primary_key_key, primary_key_value) VALUES (NULL, 'NullValue')";
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at inserting NULL value in primary key col: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ConstraintTable::deletePrimaryKey(const std::string &key) {
|
||||
try {
|
||||
m_sql << "DELETE FROM \"" + getName() + "\" WHERE primary_key_key = :key", soci::use(key);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at deleting primary key: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
50
src/tests/TestDatabase/ConstraintTable.h
Normal file
50
src/tests/TestDatabase/ConstraintTable.h
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_TEST_TESTDATABASE_CONTRAINTTABLE_H_
|
||||
#define MUMBLE_TEST_TESTDATABASE_CONTRAINTTABLE_H_
|
||||
|
||||
#include "database/Backend.h"
|
||||
#include "database/Table.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace soci {
|
||||
class session;
|
||||
}
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
namespace test {
|
||||
|
||||
class ConstraintTable : public Table {
|
||||
public:
|
||||
static constexpr const char *NAME = "ConstraintTable";
|
||||
|
||||
ConstraintTable(soci::session &sql, Backend backend);
|
||||
~ConstraintTable() = default;
|
||||
|
||||
#define DEFINE_TABLE(name) \
|
||||
void insert##name(const std::string &key, const std::string &value); \
|
||||
std::string select##name(const std::string &key);
|
||||
|
||||
DEFINE_TABLE(NotNull);
|
||||
DEFINE_TABLE(Unique);
|
||||
DEFINE_TABLE(PrimaryKey);
|
||||
DEFINE_TABLE(ForeignKey);
|
||||
#undef DEFINE_TABLE
|
||||
void insertNullInNotNullCol();
|
||||
void insertNullInPrimaryKeyCol();
|
||||
void deletePrimaryKey(const std::string &key);
|
||||
|
||||
protected:
|
||||
unsigned int dummyCounter = 0;
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_TEST_TESTDATABASE_CONTRAINTTABLE_H_
|
||||
646
src/tests/TestDatabase/DatabaseTest.cpp
Normal file
646
src/tests/TestDatabase/DatabaseTest.cpp
Normal file
@ -0,0 +1,646 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include <QString>
|
||||
#include <QtTest>
|
||||
|
||||
#include "database/AccessException.h"
|
||||
#include "database/ConnectionParameter.h"
|
||||
#include "database/Database.h"
|
||||
#include "database/FormatException.h"
|
||||
#include "database/Index.h"
|
||||
#include "database/MetaTable.h"
|
||||
#include "database/Trigger.h"
|
||||
#include "database/UnsupportedOperationException.h"
|
||||
|
||||
#include "ConstraintTable.h"
|
||||
#include "DefaultTable.h"
|
||||
#include "KeyValueTable.h"
|
||||
#include "TestUtils.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
using namespace mumble::db;
|
||||
|
||||
std::vector< Backend > backends = {
|
||||
Backend::SQLite,
|
||||
#ifdef MUMBLE_TEST_MYSQL
|
||||
Backend::MySQL,
|
||||
#endif
|
||||
#ifdef MUMBLE_TEST_POSTGRESQL
|
||||
Backend::PostgreSQL,
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
namespace QTest {
|
||||
// Provide an overload of toString for JSON objects so that Qt's macros can properly display
|
||||
// these objects to report failing tests
|
||||
template<> char *toString(const nlohmann::json &json) {
|
||||
std::string str = json.dump(2);
|
||||
char *buffer = new char[str.size() + 1];
|
||||
std::strcpy(buffer, str.data());
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
template<> char *toString(const std::string &str) {
|
||||
char *buffer = new char[str.size() + 1];
|
||||
std::strcpy(buffer, str.data());
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
}; // namespace QTest
|
||||
|
||||
|
||||
class TestDatabase : public Database {
|
||||
public:
|
||||
static constexpr const char *EXAMPLE_TABLE_NAME = "sample_table";
|
||||
using Database::Database;
|
||||
~TestDatabase() override {
|
||||
// Clear up everything that we have created in our test case
|
||||
this->destroyTables();
|
||||
}
|
||||
|
||||
unsigned int getSchemeVersion() const override { return 42; }
|
||||
|
||||
void connect(const ConnectionParameter ¶m) { Database::connectToDB(param); }
|
||||
|
||||
bool tableExistsInDB(const std::string &name) { return Database::tableExistsInDB(name); }
|
||||
|
||||
soci::session &getSQLHandle() { return m_sql; }
|
||||
};
|
||||
|
||||
class DatabaseTest : public QObject {
|
||||
Q_OBJECT;
|
||||
private slots:
|
||||
void connect();
|
||||
void getBackendVersion();
|
||||
void tableExistsInDB();
|
||||
void simpleExport();
|
||||
void simpleImport();
|
||||
void defaults();
|
||||
void constraints();
|
||||
void constraintFunctionality();
|
||||
void indices();
|
||||
void triggers();
|
||||
void dataTypes();
|
||||
void keyValueTable();
|
||||
void unicode();
|
||||
};
|
||||
|
||||
void DatabaseTest::connect() {
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
|
||||
db.connect(test::utils::getConnectionParamter(currentBackend));
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseTest::getBackendVersion() {
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
|
||||
db.connect(test::utils::getConnectionParamter(currentBackend));
|
||||
|
||||
Version defaultVersion = {};
|
||||
Version queriedVersion = db.getBackendVersion();
|
||||
|
||||
qInfo() << "Queried version:" << QString::fromStdString(queriedVersion.toString());
|
||||
|
||||
QVERIFY(queriedVersion > defaultVersion);
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseTest::tableExistsInDB() {
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
db.init(test::utils::getConnectionParamter(currentBackend));
|
||||
|
||||
QVERIFY(!db.tableExistsInDB("IDontExist"));
|
||||
|
||||
// The meta-table is always created in the DB init code
|
||||
QVERIFY(db.tableExistsInDB(MetaTable::NAME));
|
||||
|
||||
// Create a new table (with capital letters in its name) and ensure that we properly recognize it as well
|
||||
std::string newTable = "newTable";
|
||||
QVERIFY(!db.tableExistsInDB(newTable));
|
||||
Database::table_id id = db.addTable(
|
||||
std::make_unique< Table >(db.getSQLHandle(), currentBackend, newTable,
|
||||
std::vector< Column >{ Column("dummyCol", DataType(DataType::Integer)) }));
|
||||
db.getTable(id)->create();
|
||||
QVERIFY(db.tableExistsInDB(newTable));
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseTest::simpleExport() {
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
db.init(test::utils::getConnectionParamter(currentBackend));
|
||||
|
||||
MetaTable *metaTable = static_cast< MetaTable * >(db.getTable(MetaTable::NAME));
|
||||
|
||||
metaTable->setSchemeVersion(5);
|
||||
|
||||
// clang-format off
|
||||
nlohmann::json expectedJson = {
|
||||
{ "tables",
|
||||
{
|
||||
{ MetaTable::NAME,
|
||||
{
|
||||
{
|
||||
"column_names", { "meta_key", "meta_value" }
|
||||
},
|
||||
{
|
||||
"column_types", { metaTable->findColumn("meta_key")->getType().sqlRepresentation(),
|
||||
metaTable->findColumn("meta_value")->getType().sqlRepresentation() }
|
||||
},
|
||||
{
|
||||
"rows", nlohmann::json::array({ { "scheme_version", "5" } })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "meta_data",
|
||||
{
|
||||
{ "scheme_version", 42 }
|
||||
}
|
||||
}
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
nlohmann::json actualJSON = db.exportToJSON();
|
||||
|
||||
test::utils::alignColumnOrder(actualJSON, expectedJson);
|
||||
|
||||
QCOMPARE(db.exportToJSON(), expectedJson);
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseTest::simpleImport() {
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
db.init(test::utils::getConnectionParamter(currentBackend));
|
||||
|
||||
QVERIFY(!db.tableExistsInDB("test_table"));
|
||||
|
||||
MetaTable *metaTable = static_cast< MetaTable * >(db.getTable(MetaTable::NAME));
|
||||
|
||||
// clang-format off
|
||||
nlohmann::json serializedDB = {
|
||||
{ "tables",
|
||||
{
|
||||
{ MetaTable::NAME,
|
||||
{
|
||||
{
|
||||
"column_names", { "meta_key", "meta_value" }
|
||||
},
|
||||
{
|
||||
"column_types", { metaTable->findColumn("meta_key")->getType().sqlRepresentation(),
|
||||
metaTable->findColumn("meta_value")->getType().sqlRepresentation() }
|
||||
},
|
||||
{
|
||||
"rows", nlohmann::json::array({ { "scheme_version", "12" } })
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "test_table",
|
||||
{
|
||||
{
|
||||
"column_names", { "col1", "col2", "col3" }
|
||||
},
|
||||
{
|
||||
"column_types", { "INTEGER", "DOUBLE PRECISION", "VARCHAR(100)" }
|
||||
},
|
||||
{
|
||||
"rows", nlohmann::json::array({
|
||||
{ 1, 42.42, "I am a test" },
|
||||
{ 2, 0.5, "Other test" }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { "meta_data",
|
||||
{
|
||||
{ "scheme_version", 1 }
|
||||
}
|
||||
}
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
QVERIFY(db.getSchemeVersion() != serializedDB["meta_data"]["scheme_version"].get< unsigned int >());
|
||||
|
||||
// The scheme_version specified in meta-data doesn't match, so we expect the import to fail (before creating any
|
||||
// tables)
|
||||
QVERIFY(db.getTable("test_table") == nullptr);
|
||||
QVERIFY_EXCEPTION_THROWN(db.importFromJSON(serializedDB, true), FormatException);
|
||||
QVERIFY(!db.tableExistsInDB("test_table"));
|
||||
|
||||
serializedDB["meta_data"]["scheme_version"] = db.getSchemeVersion();
|
||||
|
||||
// Importing into a non-existent table without telling the DB to create the missing tables on-the-fly should
|
||||
// result in an error
|
||||
QVERIFY(db.getTable("test_table") == nullptr);
|
||||
QVERIFY_EXCEPTION_THROWN(db.importFromJSON(serializedDB, false), FormatException);
|
||||
QVERIFY(!db.tableExistsInDB("test_table"));
|
||||
|
||||
// Now with the version_scheme fixed and the necessary flag set, the import should succeed
|
||||
db.importFromJSON(serializedDB, true);
|
||||
QVERIFY(db.tableExistsInDB("test_table"));
|
||||
QVERIFY(db.getTable("test_table") != nullptr);
|
||||
|
||||
|
||||
// Exporting should re-yield the exact same JSON that we fed in to populate the DB in the first place
|
||||
// (potentially minus column order)
|
||||
nlohmann::json exported = db.exportToJSON();
|
||||
|
||||
test::utils::alignColumnOrder(exported, serializedDB);
|
||||
QCOMPARE(exported, serializedDB);
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseTest::defaults() {
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
|
||||
Database::table_id id = db.addTable(std::make_unique< test::DefaultTable >(db.getSQLHandle(), currentBackend));
|
||||
|
||||
db.init(test::utils::getConnectionParamter(currentBackend));
|
||||
|
||||
test::DefaultTable *table = static_cast< test::DefaultTable * >(db.getTable(id));
|
||||
|
||||
QVERIFY(table != nullptr);
|
||||
|
||||
table->insert("randomKey");
|
||||
QCOMPARE(table->select("randomKey"), std::string(test::DefaultTable::DEFAULT));
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseTest::constraints() {
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
db.init(test::utils::getConnectionParamter(currentBackend));
|
||||
|
||||
MetaTable *meta = static_cast< MetaTable * >(db.getTable(MetaTable::NAME));
|
||||
const Column *metaKeyCol = meta->findColumn("meta_key");
|
||||
QVERIFY(metaKeyCol != nullptr);
|
||||
|
||||
std::vector< Constraint > constraints;
|
||||
constraints.push_back(Constraint(Constraint::NotNull)); // unnamed
|
||||
constraints.push_back(Constraint(Constraint::NotNull, "my_not_null_constraint")); // named
|
||||
constraints.push_back(Constraint(Constraint::Unique)); // unnamed
|
||||
constraints.push_back(Constraint(Constraint::Unique, "my_unique_constraint")); // named
|
||||
constraints.push_back(Constraint(Constraint::PrimaryKey)); // unnamed
|
||||
constraints.push_back(Constraint(Constraint::PrimaryKey, "my_primary_key")); // named
|
||||
constraints.push_back(Constraint(Constraint::ForeignKey, "my_foreign_key", meta,
|
||||
metaKeyCol)); // named (unnamed doesn't work for foreign keys)
|
||||
|
||||
for (const Constraint ¤tConstraint : constraints) {
|
||||
qInfo() << " Current constraint: " << currentConstraint.getType()
|
||||
<< " name:" << currentConstraint.getName().c_str();
|
||||
|
||||
std::vector< Column > columns;
|
||||
|
||||
Column col("test_col", metaKeyCol->getType(), { currentConstraint });
|
||||
columns.push_back(std::move(col));
|
||||
|
||||
std::string tableName = "dummyTable";
|
||||
|
||||
Database::table_id id =
|
||||
db.addTable(std::make_unique< Table >(db.getSQLHandle(), currentBackend, tableName, columns));
|
||||
|
||||
Table *table = db.getTable(id);
|
||||
QVERIFY(table != nullptr);
|
||||
|
||||
// We always want to start from a clean slate
|
||||
QVERIFY(!db.tableExistsInDB(tableName));
|
||||
|
||||
table->create();
|
||||
|
||||
QVERIFY(db.tableExistsInDB(tableName));
|
||||
|
||||
if (currentConstraint.canBeDropped(currentBackend)) {
|
||||
// Also attempt removing the constraint again
|
||||
// If this doesn't error (throw an exception), we'll assume that it has worked)
|
||||
db.getSQLHandle() << table->getColumns()[0].getConstraints()[0].dropQuery(
|
||||
*table, table->getColumns()[0], currentBackend);
|
||||
}
|
||||
|
||||
// Remove the table again and immediately destroy it
|
||||
db.takeTable(id)->destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseTest::constraintFunctionality() {
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
|
||||
Database::table_id id =
|
||||
db.addTable(std::make_unique< test::ConstraintTable >(db.getSQLHandle(), currentBackend));
|
||||
|
||||
db.init(test::utils::getConnectionParamter(currentBackend));
|
||||
|
||||
test::ConstraintTable *table = static_cast< test::ConstraintTable * >(db.getTable(id));
|
||||
QVERIFY(table != nullptr);
|
||||
|
||||
#define TEST_INSERT_SELECT_ROUNDTRIP(variant, key, value) \
|
||||
table->insert##variant(key, value); \
|
||||
QCOMPARE(table->select##variant(key), std::string(value));
|
||||
|
||||
// Not-Null
|
||||
qInfo() << "Testing NOT NULL constraint";
|
||||
|
||||
TEST_INSERT_SELECT_ROUNDTRIP(NotNull, "bla", "blubb");
|
||||
// Empty and NULL are different things
|
||||
TEST_INSERT_SELECT_ROUNDTRIP(NotNull, "", "blubb");
|
||||
|
||||
// Inserting NULL should error
|
||||
QVERIFY_EXCEPTION_THROWN(table->insertNullInNotNullCol(), AccessException);
|
||||
|
||||
|
||||
// Unique
|
||||
qInfo() << "Testing UNIQUE constraint";
|
||||
TEST_INSERT_SELECT_ROUNDTRIP(Unique, "keyA", "valueA");
|
||||
TEST_INSERT_SELECT_ROUNDTRIP(Unique, "keyB", "valueB");
|
||||
TEST_INSERT_SELECT_ROUNDTRIP(Unique, "", "valueEmpty");
|
||||
|
||||
// Inserting any of the above keys again should error
|
||||
QVERIFY_EXCEPTION_THROWN(table->insertUnique("keyA", "otherA"), AccessException);
|
||||
QVERIFY_EXCEPTION_THROWN(table->insertUnique("keyB", "otherB"), AccessException);
|
||||
QVERIFY_EXCEPTION_THROWN(table->insertUnique("", "otherEmpty"), AccessException);
|
||||
|
||||
|
||||
// PrimaryKey
|
||||
qInfo() << "Testing PRIMARY KEY constraint";
|
||||
|
||||
TEST_INSERT_SELECT_ROUNDTRIP(PrimaryKey, "keyA", "valueA");
|
||||
TEST_INSERT_SELECT_ROUNDTRIP(PrimaryKey, "keyB", "valueB");
|
||||
TEST_INSERT_SELECT_ROUNDTRIP(PrimaryKey, "", "valueEmpty");
|
||||
TEST_INSERT_SELECT_ROUNDTRIP(PrimaryKey, "primaryA", "valueA");
|
||||
TEST_INSERT_SELECT_ROUNDTRIP(PrimaryKey, "primaryB", "valueB");
|
||||
|
||||
// Inserting any of the above keys again should error
|
||||
QVERIFY_EXCEPTION_THROWN(table->insertPrimaryKey("keyA", "otherA"), AccessException);
|
||||
QVERIFY_EXCEPTION_THROWN(table->insertPrimaryKey("keyB", "otherB"), AccessException);
|
||||
QVERIFY_EXCEPTION_THROWN(table->insertPrimaryKey("", "otherEmpty"), AccessException);
|
||||
// Inserting NULL should also error
|
||||
QVERIFY_EXCEPTION_THROWN(table->insertNullInPrimaryKeyCol(), AccessException);
|
||||
|
||||
// Deleting an existing key should be fine
|
||||
table->deletePrimaryKey("keyA");
|
||||
table->deletePrimaryKey("keyB");
|
||||
table->deletePrimaryKey("");
|
||||
|
||||
// Fetching one of those keys again should error (because that's what we coded up in ConstraintTable
|
||||
// in order to easily verify that above deletes were successful)
|
||||
QVERIFY_EXCEPTION_THROWN(table->selectPrimaryKey("keyA"), AccessException);
|
||||
QVERIFY_EXCEPTION_THROWN(table->selectPrimaryKey("keyB"), AccessException);
|
||||
QVERIFY_EXCEPTION_THROWN(table->selectPrimaryKey(""), AccessException);
|
||||
|
||||
|
||||
// ForeignKey
|
||||
qInfo() << "Testing FOREIGN KEY constraint";
|
||||
|
||||
// Using any key other than the ones that have been added as primary key above should fail.
|
||||
// And using one of the deleted key from above should also fail
|
||||
QVERIFY_EXCEPTION_THROWN(table->insertForeignKey("RandomKey", "RandomValue"), AccessException);
|
||||
QVERIFY_EXCEPTION_THROWN(table->insertForeignKey("keyA", "RandomValue"), AccessException);
|
||||
QVERIFY_EXCEPTION_THROWN(table->insertForeignKey("", "RandomValue"), AccessException);
|
||||
|
||||
// Inserting using one of the remaining primary keys should succeed
|
||||
TEST_INSERT_SELECT_ROUNDTRIP(ForeignKey, "primaryA", "Some value");
|
||||
TEST_INSERT_SELECT_ROUNDTRIP(ForeignKey, "primaryB", "Other value");
|
||||
|
||||
// Duplicates should be fine as well (though we currently have no way of fetching all matches)
|
||||
table->insertForeignKey("primaryB", "Duplicate value");
|
||||
|
||||
// Deleting a primary key should also delete all rows referencing that key (as foreign key)
|
||||
table->deletePrimaryKey("primaryB");
|
||||
QVERIFY_EXCEPTION_THROWN(table->selectForeignKey("primaryB"), AccessException);
|
||||
|
||||
// The other entry should still be there, though
|
||||
QCOMPARE(table->selectForeignKey("primaryA"), std::string("Some value"));
|
||||
|
||||
#undef TEST_INSERT_SELECT_ROUNDTRIP
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseTest::indices() {
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
|
||||
Database::table_id id = db.addTable(
|
||||
std::make_unique< Table >(db.getSQLHandle(), currentBackend, "dummyTable",
|
||||
std::vector< Column >{ Column("colA", DataType(DataType::SmallInteger)),
|
||||
Column("colB", DataType(DataType::Double)) }));
|
||||
|
||||
db.init(test::utils::getConnectionParamter(currentBackend));
|
||||
|
||||
Table *table = db.getTable(id);
|
||||
const Column *colA = table->findColumn("colA");
|
||||
const Column *colB = table->findColumn("colB");
|
||||
QVERIFY(colA != nullptr);
|
||||
QVERIFY(colB != nullptr);
|
||||
|
||||
// We assume that if the addition of the index doesn't throw, it has been added successfully
|
||||
|
||||
Index singleIndex("test_index", { colA->getName() });
|
||||
table->addIndex(singleIndex, true);
|
||||
|
||||
Index multiIndex("other_index", { colA->getName(), colB->getName() });
|
||||
table->addIndex(multiIndex, true);
|
||||
|
||||
// Adding the same index again should error
|
||||
QVERIFY_EXCEPTION_THROWN(table->addIndex(singleIndex, true), AccessException);
|
||||
QVERIFY_EXCEPTION_THROWN(table->addIndex(multiIndex, true), AccessException);
|
||||
|
||||
// The failed additions shouldn't have changed anything
|
||||
QCOMPARE(table->getIndices().size(), static_cast< std::size_t >(2));
|
||||
|
||||
// Now the removal should work just fine
|
||||
QVERIFY(table->removeIndex(singleIndex, true));
|
||||
QVERIFY(table->removeIndex(multiIndex, true));
|
||||
|
||||
// A second removal should fail (the error should be caught before accessing the DB though - thus no exception
|
||||
// should be thrown)
|
||||
QVERIFY(!table->removeIndex(singleIndex, true));
|
||||
QVERIFY(!table->removeIndex(multiIndex, true));
|
||||
|
||||
// Re-adding without applying and the removing with applying should now error with an exception
|
||||
table->addIndex(singleIndex, false);
|
||||
QVERIFY_EXCEPTION_THROWN(table->removeIndex(singleIndex, true), AccessException);
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseTest::triggers() {
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
|
||||
Database::table_id id = db.addTable(
|
||||
std::make_unique< Table >(db.getSQLHandle(), currentBackend, "dummyTable",
|
||||
std::vector< Column >{ Column("colA", DataType(DataType::SmallInteger)),
|
||||
Column("colB", DataType(DataType::Double)) }));
|
||||
|
||||
db.init(test::utils::getConnectionParamter(currentBackend));
|
||||
|
||||
Table *table = db.getTable(id);
|
||||
const Column *colA = table->findColumn("colA");
|
||||
QVERIFY(colA != nullptr);
|
||||
|
||||
for (Trigger::Timing timing : { Trigger::Timing::Before, Trigger::Timing::After }) {
|
||||
qInfo() << "Current timing:" << static_cast< int >(timing);
|
||||
for (Trigger::Event event : { Trigger::Event::Insert, Trigger::Event::Update, Trigger::Event::Delete }) {
|
||||
qInfo() << "Current event:" << static_cast< int >(event);
|
||||
|
||||
Trigger trigger("myTestTrigger", timing, event,
|
||||
"UPDATE \"" + table->getName() + "\" SET " + colA->getName() + "=1;");
|
||||
|
||||
table->addTrigger(trigger, true);
|
||||
|
||||
// Adding again should error
|
||||
QVERIFY_EXCEPTION_THROWN(table->addTrigger(trigger, true), AccessException);
|
||||
|
||||
QVERIFY(table->removeTrigger(trigger, true));
|
||||
|
||||
// Removing again should fail (without error though)
|
||||
QVERIFY(!table->removeTrigger(trigger, true));
|
||||
|
||||
// Adding without applying and then removing with applying should error
|
||||
table->addTrigger(trigger, false);
|
||||
QVERIFY_EXCEPTION_THROWN(table->removeTrigger(trigger, true), AccessException);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseTest::dataTypes() {
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
|
||||
db.init(test::utils::getConnectionParamter(currentBackend));
|
||||
|
||||
for (DataType::Type currentType : { DataType::Integer, DataType::SmallInteger, DataType::Double,
|
||||
DataType::FixedSizeString, DataType::String }) {
|
||||
qInfo() << "Current data type:" << currentType;
|
||||
|
||||
for (bool sized : { true, false }) {
|
||||
if ((!DataType::canBeSized(currentType) && sized) || (!DataType::canBeUnsized(currentType) && !sized)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
qInfo() << "Sized:" << sized;
|
||||
|
||||
DataType type(currentType, sized ? 42 : DataType::Unsized);
|
||||
|
||||
Column col1("dummyCol1", type);
|
||||
Column col2("dummyCol2", type);
|
||||
|
||||
Database::table_id id = db.addTable(std::make_unique< Table >(
|
||||
db.getSQLHandle(), currentBackend, "dummyTable", std::vector< Column >{ col1, col2 }));
|
||||
|
||||
Table *table = db.getTable(id);
|
||||
QVERIFY(table != nullptr);
|
||||
|
||||
// If we can create the table without errors, that means that the used datatypes seem to be fine
|
||||
table->create();
|
||||
|
||||
table->destroy();
|
||||
|
||||
db.takeTable(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseTest::keyValueTable() {
|
||||
// This test ensures that our KeyValueTable helper class actually works as expected
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
|
||||
Database::table_id id = db.addTable(std::make_unique< test::KeyValueTable >(db.getSQLHandle(), currentBackend));
|
||||
|
||||
db.init(test::utils::getConnectionParamter(currentBackend));
|
||||
|
||||
test::KeyValueTable *table = static_cast< test::KeyValueTable * >(db.getTable(id));
|
||||
QVERIFY(table != nullptr);
|
||||
|
||||
table->insert("test", "value");
|
||||
QCOMPARE(table->query("test"), std::string("value"));
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseTest::unicode() {
|
||||
for (Backend currentBackend : backends) {
|
||||
qInfo() << "Current backend:" << QString::fromStdString(backendToString(currentBackend));
|
||||
|
||||
TestDatabase db(currentBackend);
|
||||
|
||||
Database::table_id id = db.addTable(std::make_unique< test::KeyValueTable >(db.getSQLHandle(), currentBackend));
|
||||
|
||||
db.init(test::utils::getConnectionParamter(currentBackend));
|
||||
|
||||
test::KeyValueTable *table = static_cast< test::KeyValueTable * >(db.getTable(id));
|
||||
QVERIFY(table != nullptr);
|
||||
|
||||
for (const std::string &symbol :
|
||||
{ "Ä", "è", "ß", "¼", "💚", "🚒", "ձ", "Φ", "ヲ", "𐌒", "Ю", "¥", "ℏ", "ℜ", "¯_(ツ)_/¯" }) {
|
||||
qInfo() << "Current symbol:" << QString::fromStdString(symbol);
|
||||
|
||||
std::string unicodeValue = "Some string with " + symbol;
|
||||
std::string unicodeKey = "Key with " + symbol;
|
||||
|
||||
table->insert("testKey", unicodeValue);
|
||||
QCOMPARE(table->query("testKey"), unicodeValue);
|
||||
table->clear();
|
||||
|
||||
table->insert(unicodeKey, "TestValue");
|
||||
QCOMPARE(table->query(unicodeKey), std::string("TestValue"));
|
||||
table->clear();
|
||||
|
||||
table->insert(unicodeKey, unicodeValue);
|
||||
QCOMPARE(table->query(unicodeKey), unicodeValue);
|
||||
table->clear();
|
||||
|
||||
table->insert(symbol, symbol);
|
||||
QCOMPARE(table->query(symbol), symbol);
|
||||
table->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QTEST_MAIN(DatabaseTest)
|
||||
#include "DatabaseTest.moc"
|
||||
53
src/tests/TestDatabase/DefaultTable.cpp
Normal file
53
src/tests/TestDatabase/DefaultTable.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "DefaultTable.h"
|
||||
#include "database/AccessException.h"
|
||||
#include "database/Column.h"
|
||||
#include "database/DataType.h"
|
||||
#include "database/Utils.h"
|
||||
|
||||
#include <soci/soci.h>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
namespace test {
|
||||
|
||||
constexpr const char *DefaultTable::NAME;
|
||||
constexpr const char *DefaultTable::DEFAULT;
|
||||
|
||||
DefaultTable::DefaultTable(soci::session &sql, Backend backend) : Table(sql, backend, NAME) {
|
||||
std::vector< Column > columns;
|
||||
|
||||
columns.push_back(Column("DefaultColKey", DataType(DataType::String, 100)));
|
||||
columns.push_back(Column("DefaultColValue", DataType(DataType::String, 100), {}, DEFAULT));
|
||||
|
||||
setColumns(columns);
|
||||
}
|
||||
|
||||
void DefaultTable::insert(const std::string &key) {
|
||||
try {
|
||||
m_sql << "INSERT INTO \"" + getName() + "\" (DefaultColKey) VALUES (:key)", soci::use(key);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at inserting default value: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::string DefaultTable::select(const std::string &key) {
|
||||
try {
|
||||
std::string value;
|
||||
m_sql << "SELECT DefaultColValue FROM \"" + getName() + "\" WHERE DefaultColKey = :key", soci::use(key),
|
||||
soci::into(value);
|
||||
|
||||
::mumble::db::utils::verifyQueryResultedInData(m_sql);
|
||||
|
||||
return value;
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at inserting default value: ") + e.what());
|
||||
}
|
||||
}
|
||||
} // namespace test
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
38
src/tests/TestDatabase/DefaultTable.h
Normal file
38
src/tests/TestDatabase/DefaultTable.h
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_TEST_TESTDATABASE_DEFAULTTABLE_H_
|
||||
#define MUMBLE_TEST_TESTDATABASE_DEFAULTTABLE_H_
|
||||
|
||||
#include "database/Backend.h"
|
||||
#include "database/Table.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace soci {
|
||||
class session;
|
||||
}
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
namespace test {
|
||||
|
||||
class DefaultTable : public Table {
|
||||
public:
|
||||
static constexpr const char *NAME = "DefaultTable";
|
||||
// Use a default that is using spaces and different quotation marks (to make it more difficult)
|
||||
static constexpr const char *DEFAULT = "My sister's uncle is called \"Sam\"";
|
||||
|
||||
DefaultTable(soci::session &sql, Backend backend);
|
||||
~DefaultTable() = default;
|
||||
|
||||
void insert(const std::string &key);
|
||||
std::string select(const std::string &key);
|
||||
};
|
||||
} // namespace test
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_TEST_TESTDATABASE_DEFAULTTABLE_H_
|
||||
52
src/tests/TestDatabase/KeyValueTable.cpp
Normal file
52
src/tests/TestDatabase/KeyValueTable.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "KeyValueTable.h"
|
||||
#include "database/AccessException.h"
|
||||
#include "database/Column.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <soci/soci.h>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
namespace test {
|
||||
|
||||
constexpr const char *KeyValueTable::NAME;
|
||||
|
||||
KeyValueTable::KeyValueTable(soci::session &sql, Backend backend) : Table(sql, backend, NAME) {
|
||||
std::vector< Column > columns;
|
||||
|
||||
columns.push_back(Column("key_col", DataType(DataType::String, 150)));
|
||||
columns.push_back(Column("value_col", DataType(DataType::String, 150)));
|
||||
|
||||
setColumns(columns);
|
||||
}
|
||||
|
||||
void KeyValueTable::insert(const std::string &key, const std::string &value) {
|
||||
try {
|
||||
m_sql << "INSERT INTO \"" + getName() + "\" (key_col, value_col) VALUES (:key, :value)", soci::use(key),
|
||||
soci::use(value);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException(std::string("Failed at inserting key-value-pair: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::string KeyValueTable::query(const std::string &key) {
|
||||
std::string value;
|
||||
try {
|
||||
m_sql << "SELECT value_col FROM \"" + getName() + "\" WHERE key_col = :key", soci::use(key),
|
||||
soci::into(value);
|
||||
} catch (const soci::soci_error &e) {
|
||||
throw AccessException("Failed at querying value for key \"" + key + "\": " + e.what());
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
35
src/tests/TestDatabase/KeyValueTable.h
Normal file
35
src/tests/TestDatabase/KeyValueTable.h
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_TEST_TESTDATABASE_KEYVALUE_TABLE_H_
|
||||
#define MUMBLE_TEST_TESTDATABASE_KEYVALUE_TABLE_H_
|
||||
|
||||
#include "database/Backend.h"
|
||||
#include "database/Table.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace soci {
|
||||
class session;
|
||||
}
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
namespace test {
|
||||
class KeyValueTable : public Table {
|
||||
public:
|
||||
static constexpr const char *NAME = "keyValueTable";
|
||||
|
||||
KeyValueTable(soci::session &sql, Backend backend);
|
||||
~KeyValueTable() = default;
|
||||
|
||||
void insert(const std::string &key, const std::string &value);
|
||||
std::string query(const std::string &key);
|
||||
};
|
||||
} // namespace test
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_TEST_TESTDATABASE_KEYVALUE_TABLE_H_
|
||||
61
src/tests/TestDatabase/README.md
Normal file
61
src/tests/TestDatabase/README.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Database tests
|
||||
|
||||
This directory contains test cases for Mumble's database (backend) implementation. The tests can (and generally should) be performed for multiple
|
||||
database flavors: SQLite, MySQL and PostgreSQL. However for the latter two, it is necessary to perform some initial setup in order for the test
|
||||
program to obtain access to the respective databases.
|
||||
|
||||
In order to avoid failing tests due to incorrect (or missing) setup, the tests for MySQL and PostgreSQL are disabled by default, but they can be
|
||||
enabled via the `database-mysql-test` and `database-postgresql-test` cmake options (just set these to `ON` when invoking cmake).
|
||||
|
||||
|
||||
## Required setup
|
||||
|
||||
The setup is very similar for MySQL and PostgreSQL. Remember that you need to execute the following instructions via an existing account with the
|
||||
necessary privileges. On standard Ubuntu installations, this is most easily accomplished by connecting to the DB via `sudo mysql` and
|
||||
`sudo -u postgres psql` respectively.
|
||||
|
||||
### MySQL
|
||||
|
||||
1. Create a database with the name `mumble_test_db`:
|
||||
```sql
|
||||
CREATE DATABASE mumble_test_db;
|
||||
```
|
||||
|
||||
2. Create a new user with the name `mumble_test_user` and password `MumbleTestPassword`
|
||||
```sql
|
||||
CREATE USER 'mumble_test_user'@'localhost' IDENTIFIED BY 'MumbleTestPassword';
|
||||
```
|
||||
|
||||
3. Grant this new user all privileges on the test database:
|
||||
```sql
|
||||
GRANT ALL PRIVILEGES ON mumble_test_db.* TO 'mumble_test_user'@'localhost';
|
||||
```
|
||||
|
||||
4. Enable `log-bin-trust-function-creators` as otherwise creating triggers and stored functions would require global `SUPER` privilege for the Mumble
|
||||
user. To enable this mode, edit `/etc/mysql/my.cnf` and add
|
||||
```
|
||||
[mysqld]
|
||||
log-bin-trust-function-creators = 1
|
||||
```
|
||||
to that file. Afterwards, restart the MySQL daemon via `systemctl restart mysql`.
|
||||
|
||||
Note that Mumble's database backend will take care of using a binlog format that should not cause issues with this setting.
|
||||
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
1. Create a database with the name `mumble_test_db`:
|
||||
```sql
|
||||
CREATE DATABASE mumble_test_db;
|
||||
```
|
||||
|
||||
2. Create a new user with the name `mumble_test_user` and password `MumbleTestPassword`
|
||||
```sql
|
||||
CREATE USER mumble_test_user ENCRYPTED PASSWORD 'MumbleTestPassword';
|
||||
```
|
||||
|
||||
3. Grant this new user all privileges on the test database:
|
||||
```sql
|
||||
GRANT ALL PRIVILEGES ON DATABASE mumble_test_db TO mumble_test_user;
|
||||
```
|
||||
|
||||
88
src/tests/TestDatabase/TestUtils.cpp
Normal file
88
src/tests/TestDatabase/TestUtils.cpp
Normal file
@ -0,0 +1,88 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#include "TestUtils.h"
|
||||
#include "database/MySQLConnectionParameter.h"
|
||||
#include "database/PostgreSQLConnectionParameter.h"
|
||||
#include "database/SQLiteConnectionParameter.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
namespace test {
|
||||
namespace utils {
|
||||
|
||||
/**
|
||||
* Aligns the column order of the two provided JSON representation of a DB's content, so that
|
||||
* the columns appear in the same order (if they were only a permutation of each other to begin with).
|
||||
* All related fields (column_types and all row entries) are permuted accordingly to match the new
|
||||
* column order.
|
||||
*
|
||||
* This function has to be called, before checking reference and target for equality, as the column
|
||||
* order in our DB JSON representation is to some degree arbitrary, but the JSON objects compare
|
||||
* them in an order-aware fashion (as they are stored as arrays).
|
||||
*/
|
||||
void alignColumnOrder(const nlohmann::json &reference, nlohmann::json &target) {
|
||||
for (auto it = reference["tables"].begin(); it != reference["tables"].end(); ++it) {
|
||||
const nlohmann::json &referenceTable = it.value();
|
||||
nlohmann::json &targetTable = target["tables"][it.key()];
|
||||
|
||||
if (referenceTable["column_names"].size() != targetTable["column_names"].size()
|
||||
|| !std::is_permutation(referenceTable["column_names"].begin(),
|
||||
referenceTable["column_names"].end(),
|
||||
targetTable["column_names"].begin())) {
|
||||
// If the column names are not a simple permutation of each other, these tables are not equal
|
||||
// in any case, so we don't even try to align their ordering
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector< std::size_t > permutation =
|
||||
test::utils::find_permutation(referenceTable["column_names"], targetTable["column_names"]);
|
||||
|
||||
// First apply to column_names itself
|
||||
test::utils::apply_permutation(targetTable["column_names"], permutation);
|
||||
// Then in the same way to column_types
|
||||
test::utils::apply_permutation(targetTable["column_types"], permutation);
|
||||
|
||||
for (nlohmann::json ¤tRow : targetTable["rows"]) {
|
||||
test::utils::apply_permutation(currentRow, permutation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const mumble::db::ConnectionParameter &getConnectionParamter(mumble::db::Backend backend) {
|
||||
switch (backend) {
|
||||
case mumble::db::Backend::SQLite: {
|
||||
static mumble::db::SQLiteConnectionParameter param("mumble_test_db");
|
||||
|
||||
return param;
|
||||
}
|
||||
case mumble::db::Backend::MySQL: {
|
||||
static mumble::db::MySQLConnectionParameter param("mumble_test_db");
|
||||
|
||||
param.userName = "mumble_test_user";
|
||||
param.password = "MumbleTestPassword";
|
||||
|
||||
return param;
|
||||
}
|
||||
case mumble::db::Backend::PostgreSQL: {
|
||||
static mumble::db::PostgreSQLConnectionParameter param("mumble_test_db");
|
||||
|
||||
param.userName = "mumble_test_user";
|
||||
param.password = "MumbleTestPassword";
|
||||
|
||||
return param;
|
||||
}
|
||||
}
|
||||
|
||||
throw std::runtime_error("Not all backend types covered in test!");
|
||||
}
|
||||
|
||||
} // namespace utils
|
||||
} // namespace test
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
91
src/tests/TestDatabase/TestUtils.h
Normal file
91
src/tests/TestDatabase/TestUtils.h
Normal file
@ -0,0 +1,91 @@
|
||||
// Copyright 2022 The Mumble Developers. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license
|
||||
// that can be found in the LICENSE file at the root of the
|
||||
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
||||
|
||||
#ifndef MUMBLE_TEST_TESTDATABASE_UTILS_H_
|
||||
#define MUMBLE_TEST_TESTDATABASE_UTILS_H_
|
||||
|
||||
#include "database/Backend.h"
|
||||
#include "database/ConnectionParameter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <numeric>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
namespace mumble {
|
||||
namespace db {
|
||||
namespace test {
|
||||
namespace utils {
|
||||
|
||||
// Implementations for find_permutation and apply_permutation taken/inspired from/by
|
||||
// https://stackoverflow.com/a/17074810
|
||||
template< typename VectorLike >
|
||||
std::vector< std::size_t > find_permutation(const VectorLike &reference, const VectorLike &target) {
|
||||
assert(reference.size() == target.size());
|
||||
assert(std::is_permutation(reference.begin(), reference.end(), target.begin()));
|
||||
|
||||
std::vector< std::size_t > permutation(target.size());
|
||||
std::iota(permutation.begin(), permutation.end(), 0);
|
||||
|
||||
assert(std::is_sorted(permutation.begin(), permutation.end()));
|
||||
|
||||
do {
|
||||
bool matches = true;
|
||||
for (std::size_t i = 0; i < permutation.size(); ++i) {
|
||||
if (reference[i] != target[permutation[i]]) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
break;
|
||||
}
|
||||
} while (std::next_permutation(permutation.begin(), permutation.end()));
|
||||
|
||||
return permutation;
|
||||
}
|
||||
|
||||
template< typename VectorLike >
|
||||
void apply_permutation(VectorLike &vec, const std::vector< std::size_t > &permutation) {
|
||||
std::vector< bool > done(vec.size());
|
||||
for (std::size_t i = 0; i < vec.size(); ++i) {
|
||||
if (done[i]) {
|
||||
continue;
|
||||
}
|
||||
done[i] = true;
|
||||
std::size_t prev_j = i;
|
||||
std::size_t j = permutation[i];
|
||||
while (i != j) {
|
||||
std::swap(vec[prev_j], vec[j]);
|
||||
done[j] = true;
|
||||
prev_j = j;
|
||||
j = permutation[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aligns the column order of the two provided JSON representation of a DB's content, so that
|
||||
* the columns appear in the same order (if they were only a permutation of each other to begin with).
|
||||
* All related fields (column_types and all row entries) are permuted accordingly to match the new
|
||||
* column order.
|
||||
*
|
||||
* This function has to be called, before checking reference and target for equality, as the column
|
||||
* order in our DB JSON representation is to some degree arbitrary, but the JSON objects compare
|
||||
* them in an order-aware fashion (as they are stored as arrays).
|
||||
*/
|
||||
void alignColumnOrder(const nlohmann::json &reference, nlohmann::json &target);
|
||||
|
||||
const mumble::db::ConnectionParameter &getConnectionParamter(mumble::db::Backend backend);
|
||||
|
||||
} // namespace utils
|
||||
} // namespace test
|
||||
} // namespace db
|
||||
} // namespace mumble
|
||||
|
||||
#endif // MUMBLE_TEST_TESTDATABASE_UTILS_H_
|
||||
Loading…
Reference in New Issue
Block a user