summaryrefslogtreecommitdiff
path: root/modules/extra/sqlite.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'modules/extra/sqlite.cpp')
-rw-r--r--modules/extra/sqlite.cpp387
1 files changed, 387 insertions, 0 deletions
diff --git a/modules/extra/sqlite.cpp b/modules/extra/sqlite.cpp
new file mode 100644
index 000000000..2c6ccc6db
--- /dev/null
+++ b/modules/extra/sqlite.cpp
@@ -0,0 +1,387 @@
+/*
+ *
+ * (C) 2011-2024 Anope Team
+ * Contact us at team@anope.org
+ *
+ * Please read COPYING and README for further details.
+ */
+
+/* RequiredLibraries: sqlite3 */
+/* RequiredWindowsLibraries: sqlite3 */
+
+#include "module.h"
+#include "modules/sql.h"
+#include <sqlite3.h>
+
+using namespace SQL;
+
+/* SQLite3 API, based from InspIRCd */
+
+/** A SQLite result
+ */
+class SQLiteResult final
+ : public Result
+{
+public:
+ SQLiteResult(unsigned int i, const Query &q, const Anope::string &fq) : Result(i, q, fq)
+ {
+ }
+
+ SQLiteResult(const Query &q, const Anope::string &fq, const Anope::string &err) : Result(0, q, fq, err)
+ {
+ }
+
+ void AddRow(const std::map<Anope::string, Anope::string> &data)
+ {
+ this->entries.push_back(data);
+ }
+};
+
+/** A SQLite database, there can be multiple
+ */
+class SQLiteService final
+ : public Provider
+{
+ std::map<Anope::string, std::set<Anope::string> > active_schema;
+
+ Anope::string database;
+
+ sqlite3 *sql = nullptr;
+
+ Anope::string Escape(const Anope::string &query);
+
+public:
+ SQLiteService(Module *o, const Anope::string &n, const Anope::string &d);
+
+ ~SQLiteService();
+
+ void Run(Interface *i, const Query &query) override;
+
+ Result RunQuery(const Query &query) override;
+
+ std::vector<Query> CreateTable(const Anope::string &table, const Data &data) override;
+
+ Query BuildInsert(const Anope::string &table, unsigned int id, Data &data) override;
+
+ Query GetTables(const Anope::string &prefix) override;
+
+ Anope::string BuildQuery(const Query &q);
+
+ Anope::string FromUnixtime(time_t) override;
+
+ Anope::string GetColumnType(Serialize::DataType dt)
+ {
+ switch (dt)
+ {
+ case Serialize::DataType::BOOL:
+ return "INTEGER";
+
+ case Serialize::DataType::FLOAT:
+ return "REAL";
+
+ case Serialize::DataType::INT:
+ return "INTEGER";
+
+ case Serialize::DataType::TEXT:
+ return "TEXT";
+
+ // SQLite lacks support for 64-bit unsigned integers so we have to
+ // store them as text columns instead.
+ case Serialize::DataType::UINT:
+ return "TEXT";
+ }
+
+ return "TEXT"; // Should never be reached
+ }
+};
+
+class ModuleSQLite final
+ : public Module
+{
+ /* SQL connections */
+ std::map<Anope::string, SQLiteService *> SQLiteServices;
+public:
+ ModuleSQLite(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR)
+ {
+ }
+
+ ~ModuleSQLite()
+ {
+ for (std::map<Anope::string, SQLiteService *>::iterator it = this->SQLiteServices.begin(); it != this->SQLiteServices.end(); ++it)
+ delete it->second;
+ SQLiteServices.clear();
+ }
+
+ void OnReload(Configuration::Conf *conf) override
+ {
+ Configuration::Block *config = conf->GetModule(this);
+
+ for (std::map<Anope::string, SQLiteService *>::iterator it = this->SQLiteServices.begin(); it != this->SQLiteServices.end();)
+ {
+ const Anope::string &cname = it->first;
+ SQLiteService *s = it->second;
+ int i, num;
+ ++it;
+
+ for (i = 0, num = config->CountBlock("sqlite"); i < num; ++i)
+ if (config->GetBlock("sqlite", i)->Get<const Anope::string>("name", "sqlite/main") == cname)
+ break;
+
+ if (i == num)
+ {
+ Log(LOG_NORMAL, "sqlite") << "SQLite: Removing server connection " << cname;
+
+ delete s;
+ this->SQLiteServices.erase(cname);
+ }
+ }
+
+ for (int i = 0; i < config->CountBlock("sqlite"); ++i)
+ {
+ Configuration::Block *block = config->GetBlock("sqlite", i);
+ Anope::string connname = block->Get<const Anope::string>("name", "sqlite/main");
+
+ if (this->SQLiteServices.find(connname) == this->SQLiteServices.end())
+ {
+ auto database = Anope::ExpandData(block->Get<const Anope::string>("database", "anope"));
+ try
+ {
+ auto *ss = new SQLiteService(this, connname, database);
+ this->SQLiteServices[connname] = ss;
+
+ Log(LOG_NORMAL, "sqlite") << "SQLite: Successfully added database " << database;
+ }
+ catch (const SQL::Exception &ex)
+ {
+ Log(LOG_NORMAL, "sqlite") << "SQLite: " << ex.GetReason();
+ }
+ }
+ }
+ }
+};
+
+SQLiteService::SQLiteService(Module *o, const Anope::string &n, const Anope::string &d)
+: Provider(o, n), database(d)
+{
+ int db = sqlite3_open_v2(database.c_str(), &this->sql, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, 0);
+ if (db != SQLITE_OK)
+ {
+ Anope::string exstr = "Unable to open SQLite database " + database;
+ if (this->sql)
+ {
+ exstr += ": ";
+ exstr += sqlite3_errmsg(this->sql);
+ sqlite3_close(this->sql);
+ }
+ throw SQL::Exception(exstr);
+ }
+}
+
+SQLiteService::~SQLiteService()
+{
+ sqlite3_interrupt(this->sql);
+ sqlite3_close(this->sql);
+}
+
+void SQLiteService::Run(Interface *i, const Query &query)
+{
+ Result res = this->RunQuery(query);
+ if (!res.GetError().empty())
+ i->OnError(res);
+ else
+ i->OnResult(res);
+}
+
+Result SQLiteService::RunQuery(const Query &query)
+{
+ Anope::string real_query = this->BuildQuery(query);
+ sqlite3_stmt *stmt;
+ int err = sqlite3_prepare_v2(this->sql, real_query.c_str(), real_query.length(), &stmt, NULL);
+ if (err != SQLITE_OK)
+ return SQLiteResult(query, real_query, sqlite3_errmsg(this->sql));
+
+ std::vector<Anope::string> columns;
+ int cols = sqlite3_column_count(stmt);
+ columns.resize(cols);
+ for (int i = 0; i < cols; ++i)
+ columns[i] = sqlite3_column_name(stmt, i);
+
+ SQLiteResult result(0, query, real_query);
+
+ while ((err = sqlite3_step(stmt)) == SQLITE_ROW)
+ {
+ std::map<Anope::string, Anope::string> items;
+ for (int i = 0; i < cols; ++i)
+ {
+ const char *data = reinterpret_cast<const char *>(sqlite3_column_text(stmt, i));
+ if (data && *data)
+ items[columns[i]] = data;
+ }
+ result.AddRow(items);
+ }
+
+ result.id = sqlite3_last_insert_rowid(this->sql);
+
+ sqlite3_finalize(stmt);
+
+ if (err != SQLITE_DONE)
+ return SQLiteResult(query, real_query, sqlite3_errmsg(this->sql));
+
+ // GCC and clang disagree about whether this should be a move >:(
+#ifdef __clang__
+ return std::move(result);
+#else
+ return result;
+#endif
+}
+
+std::vector<Query> SQLiteService::CreateTable(const Anope::string &table, const Data &data)
+{
+ std::vector<Query> queries;
+ std::set<Anope::string> &known_cols = this->active_schema[table];
+
+ if (known_cols.empty())
+ {
+ Log(LOG_DEBUG) << "sqlite: Fetching columns for " << table;
+
+ Result columns = this->RunQuery("PRAGMA table_info(" + table + ")");
+ for (int i = 0; i < columns.Rows(); ++i)
+ {
+ const Anope::string &column = columns.Get(i, "name");
+
+ Log(LOG_DEBUG) << "sqlite: Column #" << i << " for " << table << ": " << column;
+ known_cols.insert(column);
+ }
+ }
+
+ if (known_cols.empty())
+ {
+ Anope::string query_text = "CREATE TABLE `" + table + "` (`id` INTEGER PRIMARY KEY, `timestamp` timestamp DEFAULT CURRENT_TIMESTAMP";
+
+ for (const auto &[column, _] : data.data)
+ {
+ known_cols.insert(column);
+
+ query_text += ", `" + column + "` " + GetColumnType(data.GetType(column));
+ }
+
+ query_text += ")";
+
+ queries.push_back(query_text);
+
+ query_text = "CREATE UNIQUE INDEX `" + table + "_id_idx` ON `" + table + "` (`id`)";
+ queries.push_back(query_text);
+
+ query_text = "CREATE INDEX `" + table + "_timestamp_idx` ON `" + table + "` (`timestamp`)";
+ queries.push_back(query_text);
+
+ query_text = "CREATE TRIGGER `" + table + "_trigger` AFTER UPDATE ON `" + table + "` FOR EACH ROW BEGIN UPDATE `" + table + "` SET `timestamp` = CURRENT_TIMESTAMP WHERE `id` = `old.id`; end;";
+ queries.push_back(query_text);
+ }
+ else
+ {
+ for (const auto &[column, _] : data.data)
+ {
+ if (known_cols.count(column) > 0)
+ continue;
+
+ known_cols.insert(column);
+
+ Anope::string query_text = "ALTER TABLE `" + table + "` ADD `" + column + "` " + GetColumnType(data.GetType(column));;
+
+ queries.push_back(query_text);
+ }
+ }
+
+ return queries;
+}
+
+Query SQLiteService::BuildInsert(const Anope::string &table, unsigned int id, Data &data)
+{
+ /* Empty columns not present in the data set */
+ for (const auto &known_col : this->active_schema[table])
+ {
+ if (known_col != "id" && known_col != "timestamp" && data.data.count(known_col) == 0)
+ data[known_col] << "";
+ }
+
+ Anope::string query_text = "REPLACE INTO `" + table + "` (";
+ if (id > 0)
+ query_text += "`id`,";
+ for (const auto &[field, _] : data.data)
+ query_text += "`" + field + "`,";
+ query_text.erase(query_text.length() - 1);
+ query_text += ") VALUES (";
+ if (id > 0)
+ query_text += Anope::ToString(id) + ",";
+ for (const auto &[field, _] : data.data)
+ query_text += "@" + field + "@,";
+ query_text.erase(query_text.length() - 1);
+ query_text += ")";
+
+ Query query(query_text);
+ for (auto &[field, value] : data.data)
+ {
+ Anope::string buf;
+ *value >> buf;
+
+ auto escape = true;
+ switch (data.GetType(field))
+ {
+ case Serialize::DataType::BOOL:
+ case Serialize::DataType::FLOAT:
+ case Serialize::DataType::INT:
+ {
+ if (buf.empty())
+ buf = "0";
+ escape = false;
+ break;
+ }
+
+ case Serialize::DataType::TEXT:
+ case Serialize::DataType::UINT:
+ {
+ if (buf.empty())
+ {
+ buf = "NULL";
+ escape = false;
+ }
+ break;
+ }
+ }
+
+ query.SetValue(field, buf, escape);
+ }
+
+ return query;
+}
+
+Query SQLiteService::GetTables(const Anope::string &prefix)
+{
+ return Query("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '" + prefix + "%';");
+}
+
+Anope::string SQLiteService::Escape(const Anope::string &query)
+{
+ char *e = sqlite3_mprintf("%q", query.c_str());
+ Anope::string buffer = e;
+ sqlite3_free(e);
+ return buffer;
+}
+
+Anope::string SQLiteService::BuildQuery(const Query &q)
+{
+ Anope::string real_query = q.query;
+
+ for (const auto &[name, value] : q.parameters)
+ real_query = real_query.replace_all_cs("@" + name + "@", (value.escape ? ("'" + this->Escape(value.data) + "'") : value.data));
+
+ return real_query;
+}
+
+Anope::string SQLiteService::FromUnixtime(time_t t)
+{
+ return "datetime('" + Anope::ToString(t) + "', 'unixepoch')";
+}
+
+MODULE_INIT(ModuleSQLite)