2
3
4
5
6
7
8
9
10
14#include "lib/MqttMessage.h"
15#include "lib/StoragePlan.h"
17#include <nlohmann/json.hpp>
19#ifndef DOXYGEN_SHOULD_SKIP_THIS
25#include <log/Logger.h>
34namespace mqtt::mqttstore::
lib {
39 std::vector<std::string> result;
40 std::stringstream topicStream(topic);
42 for (std::string level; std::getline(topicStream, level,
'/');) {
43 result.push_back(level);
46 if (!topic.empty() && topic.back() ==
'/') {
47 result.emplace_back();
58 bool autoCreateRawTable,
84 throw std::runtime_error(
"Unsafe raw table name: " +
this->rawTable);
87 if (autoCreateRawTable) {
93 const std::optional<
nlohmann::json> payloadJson = parsePayload(message.payload);
98 [connectionName =
this->connectionName, topic = message.topic]() ->
void {
99 VLOG(1) << connectionName <<
" MariaDB: stored raw MQTT message for topic '" << topic <<
"'";
101 [connectionName =
this->connectionName](
const std::string& errorString,
unsigned int errorNumber) ->
void {
102 execLogFailure(connectionName,
"raw MQTT message insert", errorString, errorNumber);
109 return !identifier.empty() && std::all_of(identifier.begin(), identifier.end(), [](
unsigned char character) {
110 return std::isalnum(character) != 0 || character ==
'_';
116 throw std::runtime_error(
"Unsafe SQL identifier: " + identifier);
119 return "`" + identifier +
"`";
124 quoted.reserve(value.size() + 2);
125 quoted.push_back(
'\'');
127 for (
const char character : value) {
151 quoted.push_back(character);
156 quoted.push_back(
'\'');
162 if (value.is_null()) {
165 if (value.is_boolean()) {
166 return value.get<
bool>() ?
"TRUE" :
"FALSE";
168 if (value.is_number()) {
171 if (value.is_string()) {
180 return nlohmann::json::parse(payload);
181 }
catch (
const nlohmann::json::parse_error&) {
187 return std::any_of(payload.begin(), payload.end(), [](
unsigned char character) {
188 return character ==
'\0' || (character < 0x09) || (character > 0x0D && character < 0x20);
194 const std::optional<
nlohmann::json>& payloadJson) {
196 const std::string payloadFormat = payloadJson.has_value() ?
"json" : (binaryPayload ?
"binary" :
"text");
197 const std::string payloadTextValue = binaryPayload ?
"NULL" :
sqlQuote(message
.payload);
198 const std::string payloadJsonValue = payloadJson.has_value() ?
sqlQuote(payloadJson->dump()) :
"NULL";
201 "(`received_at`, `source_instance`, `topic`, `qos`, `retain_flag`, `dup_flag`, `packet_identifier`, `payload`, "
202 "`payload_text`, `payload_json`, `payload_format`) VALUES (CURRENT_TIMESTAMP(6), " +
204 std::to_string(
static_cast<
unsigned int>(message
.qoS)) +
", " + (message
.retain ?
"TRUE" :
"FALSE") +
", " +
206 ", " + payloadTextValue +
", " + payloadJsonValue +
", " +
sqlQuote(payloadFormat
) +
")";
211 const nlohmann::json& payloadJson) {
212 std::string columnList;
213 std::string valueList;
215 for (
const StoragePlan::ColumnMapping& mapping : projection.columns) {
216 const std::string value = jsonValueForColumn(mapping, message, payloadJson);
217 if (value.empty() && !mapping.required) {
221 columnList += (columnList.empty() ?
"" :
", ") + quoteIdentifier(mapping.column);
222 valueList += (valueList.empty() ?
"" :
", ") + (value.empty() ?
"NULL" : value);
225 if (columnList.empty()) {
226 throw std::runtime_error(
"Projection '" + projection
.name +
"' produced no columns");
234 const nlohmann::json& payloadJson) {
240 const std::vector<std::string> topicLevels = splitTopic(message.topic);
252 const nlohmann::json::json_pointer pointer(mapping
.jsonPointer);
253 if (!payloadJson.contains(pointer)) {
261 const std::string& operation,
262 const std::string& errorString,
263 unsigned int errorNumber) {
264 VLOG(0) << connectionName <<
" MariaDB " << operation <<
" failed: " << errorString <<
" : " << errorNumber;
270 "`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,"
271 "`received_at` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),"
272 "`source_instance` VARCHAR(255) NULL,"
273 "`topic` VARCHAR(1024) NOT NULL,"
274 "`qos` TINYINT UNSIGNED NOT NULL,"
275 "`retain_flag` BOOLEAN NOT NULL,"
276 "`dup_flag` BOOLEAN NOT NULL,"
277 "`packet_identifier` INT UNSIGNED NULL,"
278 "`payload` LONGBLOB NOT NULL,"
279 "`payload_text` LONGTEXT NULL,"
280 "`payload_json` JSON NULL,"
281 "`payload_format` ENUM('json', 'text', 'binary') NOT NULL,"
282 "INDEX `idx_received_at` (`received_at`),"
283 "INDEX `idx_topic` (`topic`(255))"
288 [connectionName =
this->connectionName, rawTable =
this->rawTable]() ->
void {
289 VLOG(0) << connectionName <<
" MariaDB: ensured raw MQTT table '" << rawTable <<
"'";
291 [connectionName =
this->connectionName](
const std::string& errorString,
unsigned int errorNumber) ->
void {
292 execLogFailure(connectionName,
"raw MQTT table creation", errorString, errorNumber);
297 if (!payloadJson.has_value()) {
301 for (
const StoragePlan::Projection* projection : storagePlan.match(message.topic)) {
303 const std::string sql = buildProjectionInsertSql(*projection, message, *payloadJson);
306 [connectionName =
this->connectionName, projectionName = projection->name]() ->
void {
307 VLOG(1) << connectionName <<
" MariaDB: projection insert completed for '" << projectionName <<
"'";
309 [connectionName =
this->connectionName, projectionName = projection->name](
const std::string& errorString,
310 unsigned int errorNumber) ->
void {
311 execLogFailure(connectionName,
"projection '" + projectionName +
"' insert", errorString, errorNumber);
313 }
catch (
const std::exception& error) {
314 VLOG(0) << connectionName <<
" MariaDB projection '" << projection->name <<
"' skipped: " << error.what();
static std::string quoteIdentifier(const std::string &identifier)
std::string connectionName
static std::string buildRawInsertSql(const std::string &rawTable, const MqttMessage &message, const std::optional< nlohmann::json > &payloadJson)
static std::string jsonValueForColumn(const StoragePlan::ColumnMapping &mapping, const MqttMessage &message, const nlohmann::json &payloadJson)
static std::string sqlValue(const nlohmann::json &value)
static bool hasBinaryContent(const std::string &payload)
MariaDbStorage(const std::string &connectionName, const ConnectionConfig &connectionConfig, std::string rawTable, bool autoCreateRawTable, StoragePlan storagePlan)
static bool isSafeIdentifier(const std::string &identifier)
void storeProjections(const MqttMessage &message, const std::optional< nlohmann::json > &payloadJson)
static std::string buildProjectionInsertSql(const StoragePlan::Projection &projection, const MqttMessage &message, const nlohmann::json &payloadJson)
static std::optional< nlohmann::json > parsePayload(const std::string &payload)
void store(const MqttMessage &message)
static void execLogFailure(const std::string &connectionName, const std::string &operation, const std::string &errorString, unsigned int errorNumber)
static std::string sqlQuote(const std::string &value)
std::vector< std::string > splitTopic(const std::string &topic)
std::uint16_t packetIdentifier
std::string connectionName
std::optional< std::size_t > topicLevel
std::optional< std::string > literal