MQTTSuite
Loading...
Searching...
No Matches
MariaDbStorage.cpp
Go to the documentation of this file.
1/*
2 * MQTTSuite - A lightweight MQTT Integration System
3 * Copyright (C) Volker Christian <me@vchrist.at>
4 * 2022, 2023, 2024, 2025, 2026
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the Free
8 * Software Foundation, either version 3 of the License, or (at your option)
9 * any later version.
10 */
11
12#include "MariaDbStorage.h"
13
14#include "lib/MqttMessage.h"
15#include "lib/StoragePlan.h"
16
17#include <nlohmann/json.hpp>
18
19#ifndef DOXYGEN_SHOULD_SKIP_THIS
20
21#include <algorithm>
22#include <cctype>
23#include <exception>
24#include <functional>
25#include <log/Logger.h>
26#include <map>
27#include <sstream>
28#include <stdexcept>
29#include <utility>
30#include <vector>
31
32#endif
33
34namespace mqtt::mqttstore::lib {
35
36 namespace {
37
38 [[nodiscard]] std::vector<std::string> splitTopic(const std::string& topic) {
39 std::vector<std::string> result;
40 std::stringstream topicStream(topic);
41
42 for (std::string level; std::getline(topicStream, level, '/');) {
43 result.push_back(level);
44 }
45
46 if (!topic.empty() && topic.back() == '/') {
47 result.emplace_back();
48 }
49
50 return result;
51 }
52
53 } // namespace
54
55 MariaDbStorage::MariaDbStorage(const std::string& connectionName,
56 const ConnectionConfig& connectionConfig,
57 std::string rawTable,
58 bool autoCreateRawTable,
59 StoragePlan storagePlan)
60 : connectionName(connectionName)
61 , mariaDB(
62 {
71 },
73 if (state.connected) {
74 VLOG(0) << connectionName << " MariaDB: connected";
75 } else if (state.error != 0) {
76 VLOG(0) << connectionName << " MariaDB: " << state.errorMessage << " [" << state.error << "]";
77 } else {
78 VLOG(0) << connectionName << " MariaDB: lost connection";
79 }
80 })
81 , rawTable(std::move(rawTable))
82 , storagePlan(std::move(storagePlan)) {
84 throw std::runtime_error("Unsafe raw table name: " + this->rawTable);
85 }
86
87 if (autoCreateRawTable) {
89 }
90 }
91
92 void MariaDbStorage::store(const MqttMessage& message) {
93 const std::optional<nlohmann::json> payloadJson = parsePayload(message.payload);
94 const std::string rawInsertSql = buildRawInsertSql(rawTable, message, payloadJson);
95
96 mariaDB.exec(
97 rawInsertSql,
98 [connectionName = this->connectionName, topic = message.topic]() -> void {
99 VLOG(1) << connectionName << " MariaDB: stored raw MQTT message for topic '" << topic << "'";
100 },
101 [connectionName = this->connectionName](const std::string& errorString, unsigned int errorNumber) -> void {
102 execLogFailure(connectionName, "raw MQTT message insert", errorString, errorNumber);
103 });
104
105 storeProjections(message, payloadJson);
106 }
107
108 bool MariaDbStorage::isSafeIdentifier(const std::string& identifier) {
109 return !identifier.empty() && std::all_of(identifier.begin(), identifier.end(), [](unsigned char character) {
110 return std::isalnum(character) != 0 || character == '_';
111 });
112 }
113
114 std::string MariaDbStorage::quoteIdentifier(const std::string& identifier) {
115 if (!isSafeIdentifier(identifier)) {
116 throw std::runtime_error("Unsafe SQL identifier: " + identifier);
117 }
118
119 return "`" + identifier + "`";
120 }
121
122 std::string MariaDbStorage::sqlQuote(const std::string& value) {
123 std::string quoted;
124 quoted.reserve(value.size() + 2);
125 quoted.push_back('\'');
126
127 for (const char character : value) {
128 switch (character) {
129 case '\0':
130 quoted += "\\0";
131 break;
132 case '\n':
133 quoted += "\\n";
134 break;
135 case '\r':
136 quoted += "\\r";
137 break;
138 case '\\':
139 quoted += "\\\\";
140 break;
141 case '\'':
142 quoted += "\\'";
143 break;
144 case '"':
145 quoted += "\\\"";
146 break;
147 case '\x1a':
148 quoted += "\\Z";
149 break;
150 default:
151 quoted.push_back(character);
152 break;
153 }
154 }
155
156 quoted.push_back('\'');
157
158 return quoted;
159 }
160
161 std::string MariaDbStorage::sqlValue(const nlohmann::json& value) {
162 if (value.is_null()) {
163 return "NULL";
164 }
165 if (value.is_boolean()) {
166 return value.get<bool>() ? "TRUE" : "FALSE";
167 }
168 if (value.is_number()) {
169 return value.dump();
170 }
171 if (value.is_string()) {
172 return sqlQuote(value.get<std::string>());
173 }
174
175 return sqlQuote(value.dump());
176 }
177
178 std::optional<nlohmann::json> MariaDbStorage::parsePayload(const std::string& payload) {
179 try {
180 return nlohmann::json::parse(payload);
181 } catch (const nlohmann::json::parse_error&) {
182 return std::nullopt;
183 }
184 }
185
186 bool MariaDbStorage::hasBinaryContent(const std::string& payload) {
187 return std::any_of(payload.begin(), payload.end(), [](unsigned char character) {
188 return character == '\0' || (character < 0x09) || (character > 0x0D && character < 0x20);
189 });
190 }
191
192 std::string MariaDbStorage::buildRawInsertSql(const std::string& rawTable,
193 const MqttMessage& message,
194 const std::optional<nlohmann::json>& payloadJson) {
195 const bool binaryPayload = !payloadJson.has_value() && hasBinaryContent(message.payload);
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";
199
200 return "INSERT INTO " + quoteIdentifier(rawTable) +
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), " +
203 sqlQuote(message.connectionName) + ", " + sqlQuote(message.topic) + ", " +
204 std::to_string(static_cast<unsigned int>(message.qoS)) + ", " + (message.retain ? "TRUE" : "FALSE") + ", " +
205 (message.dup ? "TRUE" : "FALSE") + ", " + std::to_string(message.packetIdentifier) + ", " + sqlQuote(message.payload) +
206 ", " + payloadTextValue + ", " + payloadJsonValue + ", " + sqlQuote(payloadFormat) + ")";
207 }
208
210 const MqttMessage& message,
211 const nlohmann::json& payloadJson) {
212 std::string columnList;
213 std::string valueList;
214
215 for (const StoragePlan::ColumnMapping& mapping : projection.columns) {
216 const std::string value = jsonValueForColumn(mapping, message, payloadJson);
217 if (value.empty() && !mapping.required) {
218 continue;
219 }
220
221 columnList += (columnList.empty() ? "" : ", ") + quoteIdentifier(mapping.column);
222 valueList += (valueList.empty() ? "" : ", ") + (value.empty() ? "NULL" : value);
223 }
224
225 if (columnList.empty()) {
226 throw std::runtime_error("Projection '" + projection.name + "' produced no columns");
227 }
228
229 return "INSERT INTO " + quoteIdentifier(projection.table) + "(" + columnList + ") VALUES (" + valueList + ")";
230 }
231
233 const MqttMessage& message,
234 const nlohmann::json& payloadJson) {
235 if (mapping.literal.has_value()) {
236 return sqlQuote(*mapping.literal);
237 }
238
239 if (mapping.topicLevel.has_value()) {
240 const std::vector<std::string> topicLevels = splitTopic(message.topic);
241 if (*mapping.topicLevel >= topicLevels.size()) {
242 return {};
243 }
244
245 return sqlQuote(topicLevels[*mapping.topicLevel]);
246 }
247
248 if (mapping.jsonPointer.empty()) {
249 return {};
250 }
251
252 const nlohmann::json::json_pointer pointer(mapping.jsonPointer);
253 if (!payloadJson.contains(pointer)) {
254 return {};
255 }
256
257 return sqlValue(payloadJson.at(pointer));
258 }
259
260 void MariaDbStorage::execLogFailure(const std::string& connectionName,
261 const std::string& operation,
262 const std::string& errorString,
263 unsigned int errorNumber) {
264 VLOG(0) << connectionName << " MariaDB " << operation << " failed: " << errorString << " : " << errorNumber;
265 }
266
268 const std::string sql = "CREATE TABLE IF NOT EXISTS " + quoteIdentifier(rawTable) +
269 "("
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))"
284 ")";
285
286 mariaDB.exec(
287 sql,
288 [connectionName = this->connectionName, rawTable = this->rawTable]() -> void {
289 VLOG(0) << connectionName << " MariaDB: ensured raw MQTT table '" << rawTable << "'";
290 },
291 [connectionName = this->connectionName](const std::string& errorString, unsigned int errorNumber) -> void {
292 execLogFailure(connectionName, "raw MQTT table creation", errorString, errorNumber);
293 });
294 }
295
296 void MariaDbStorage::storeProjections(const MqttMessage& message, const std::optional<nlohmann::json>& payloadJson) {
297 if (!payloadJson.has_value()) {
298 return;
299 }
300
301 for (const StoragePlan::Projection* projection : storagePlan.match(message.topic)) {
302 try {
303 const std::string sql = buildProjectionInsertSql(*projection, message, *payloadJson);
304 mariaDB.exec(
305 sql,
306 [connectionName = this->connectionName, projectionName = projection->name]() -> void {
307 VLOG(1) << connectionName << " MariaDB: projection insert completed for '" << projectionName << "'";
308 },
309 [connectionName = this->connectionName, projectionName = projection->name](const std::string& errorString,
310 unsigned int errorNumber) -> void {
311 execLogFailure(connectionName, "projection '" + projectionName + "' insert", errorString, errorNumber);
312 });
313 } catch (const std::exception& error) {
314 VLOG(0) << connectionName << " MariaDB projection '" << projection->name << "' skipped: " << error.what();
315 }
316 }
317 }
318
319} // namespace mqtt::mqttstore::lib
static std::string quoteIdentifier(const std::string &identifier)
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)