#pragma once /* * Copyright (C) 2024 Brett Terpstra * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef SQL_H #define SQL_H #include #include #include #include #include #include #include namespace detail { template std::string sql_name() { using Decay = std::decay_t; if constexpr (std::is_same_v) return "NULL"; else if constexpr (std::is_integral_v) return "INTEGER"; else if constexpr (std::is_floating_point_v) return "REAL"; else if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) return "TEXT"; else if constexpr (std::is_same_v) return "BOOLEAN"; else return "BLOB"; } struct foreign_key_t { std::string local_name; std::string foreign_table; std::string foreign_name; }; } class column_t { public: explicit column_t(sqlite3_stmt* statement): statement{statement} {} template auto get(const int col) const -> decltype(auto) { using Decay = std::decay_t; if constexpr (std::is_integral_v) { if constexpr (sizeof(Decay) == 8) { return sqlite3_column_int64(statement, col); } else { return sqlite3_column_int(statement, col); } } else if constexpr (std::is_same_v) { return sqlite3_column_double(statement, col); } else if constexpr (std::is_same_v) { return static_cast(sqlite3_column_double(statement, col)); } else if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) { return T(sqlite3_column_text(statement, col)); } else { return static_cast(sqlite3_column_blob(statement, col)); } } template [[nodiscard]] std::tuple get() const { return get_internal(std::index_sequence_for()); } [[nodiscard]] size_t size(const int col) const { return static_cast(sqlite3_column_bytes(statement, col)); } private: template [[nodiscard]] std::tuple get_internal(std::index_sequence) const { return std::tuple{get(Indices)...}; } sqlite3_stmt* statement; }; class column_binder_t { public: explicit column_binder_t(sqlite3_stmt* statement): statement{statement} {} template int bind(const T& type, int col) { static_assert(!std::is_rvalue_reference_v, "Lifetime of object must outlive its usage!"); using Decay = std::decay_t; if constexpr (std::is_same_v || std::is_integral_v) { if constexpr (sizeof(Decay) == 8) return sqlite3_bind_int64(statement, col, type); else return sqlite3_bind_int(statement, col, type); } else if constexpr (std::is_floating_point_v) { return sqlite3_bind_double(statement, col, type); } else if constexpr (std::is_same_v) { return sqlite3_bind_null(statement, col); } else if constexpr (std::is_same_v || std::is_same_v) { auto str_copy = new char[type.size()]; std::memcpy(str_copy, type.data(), type.size()); return sqlite3_bind_text(statement, col, str_copy, type.size(), [](void* ptr) { delete[] static_cast(ptr); }); } else if constexpr (std::is_same_v || std::is_same_v) { return sqlite3_bind_text(statement, col, type, -1, nullptr); } else { return sqlite3_bind_blob64(statement, col, type.data(), type.data(), nullptr); } } private: sqlite3_stmt* statement; }; class binder_t { public: explicit binder_t(sqlite3_stmt* statement): statement(statement) {} /** * Indexes start at 1 for the left most template parameter */ template int bind(const T& type, int col) { return column_binder_t{statement}.bind(type, col); } template int bind(const T& type, const std::string& name) { auto index = sqlite3_bind_parameter_index(statement, name.c_str()); return column_binder_t{statement}.bind(type, index); } template int bind_all(const Types&... types) { return bind_internal(std::index_sequence_for(), types...); } private: template int bind_internal(std::index_sequence, const Types&... types) { return ((bind(types, Indices + 1) != SQLITE_OK) | ...); } sqlite3_stmt* statement; }; class statement_t { public: explicit statement_t(sqlite3* db, const std::string& stmt); statement_t(const statement_t& copy) = delete; statement_t(statement_t& move) noexcept: statement{std::exchange(move.statement, nullptr)}, db{move.db} {} statement_t& operator=(const statement_t&) = delete; statement_t& operator=(statement_t&& move) noexcept { statement = std::exchange(move.statement, statement); db = std::exchange(move.db, db); return *this; } binder_t bind() const // NOLINT { sqlite3_reset(statement); return binder_t{statement}; } // returns true if the statement has a row. false otherwise. optional is empty if there is an error [[jetbrains::has_side_effects]] std::optional execute() const; // NOLINT [[nodiscard]] column_t fetch() const { return column_t{statement}; } ~statement_t(); private: sqlite3_stmt* statement = nullptr; sqlite3* db; }; class table_builder_t; class table_column_builder_t { public: table_column_builder_t(table_builder_t& parent, std::vector& columns, std::vector& primary_keys, std::vector& foreign_keys, std::string type, std::string name): parent{parent}, columns{columns}, primary_keys{primary_keys}, foreign_keys{foreign_keys}, type{std::move(type)}, name{std::move(name)} {} table_column_builder_t& primary_key() { primary_keys.push_back(name); return *this; } table_column_builder_t& auto_increment() { primary_key(); attributes.push_back("AUTOINCREMENT"); return *this; } table_column_builder_t& unique() { attributes.emplace_back("UNIQUE"); return *this; } table_column_builder_t& with_default(const std::string& value) { if (type == "TEXT") attributes.push_back("DEFAULT \"" + value + '"'); else attributes.push_back("DEFAULT " + value); return *this; } table_column_builder_t& not_null() { attributes.emplace_back("NOT NULL"); return *this; } table_column_builder_t& foreign_key(const std::string& table, const std::string& column) { foreign_keys.emplace_back(detail::foreign_key_t{name, table, column}); return *this; } table_builder_t& finish() { if (!created) { columns.push_back(name + ' ' + type + ' ' + join()); created = true; } return parent; } ~table_column_builder_t() { finish(); } private: [[nodiscard]] std::string join() const { std::string out; for (const auto& str : attributes) { out += str; out += ' '; } return out; } bool created = false; table_builder_t& parent; std::vector& columns; std::vector& primary_keys; std::vector& foreign_keys; std::vector attributes; std::string type; std::string name; }; class table_builder_t { public: template struct required_string_t { std::string name; required_string_t(std::string name): name{std::move(name)} // NOLINT {} operator std::string() // NOLINT { return name; } }; explicit table_builder_t(sqlite3* db, std::string name): db{db}, name{std::move(name)} {} template table_column_builder_t with_column(const std::string& name) { return table_column_builder_t{*this, columns, primary_keys, foreign_keys, detail::sql_name(), name}; } template table_builder_t& with_columns(required_string_t... names) { (with_column(names.name), ...); return *this; } table_builder_t& with_foreign_key(const std::string& local_name, const std::string& foreign_table, const std::string& foreign_name) { foreign_keys.emplace_back(detail::foreign_key_t{local_name, foreign_table, foreign_name}); return *this; } table_builder_t& with_primary_key(const std::string& name) { primary_keys.push_back(name); return *this; } statement_t build(); private: std::vector columns{}; std::vector primary_keys{}; std::vector foreign_keys{}; sqlite3* db; std::string name{}; }; class statement_builder_t { public: explicit statement_builder_t(sqlite3* db): db{db} {} table_builder_t create_table(const std::string& name) const { return table_builder_t{db, name}; } private: sqlite3* db; }; class database_t { public: explicit database_t(const std::string& file); database_t(const database_t& copy) = delete; database_t(database_t&& move) noexcept: db{std::exchange(move.db, nullptr)} {} database_t& operator=(const database_t&) = delete; database_t& operator=(database_t&& move) noexcept { db = std::exchange(move.db, db); return *this; } [[nodiscard]] statement_t prepare(const std::string& stmt) const { return statement_t{db, stmt}; } [[nodiscard]] statement_builder_t builder() const { return statement_builder_t{db}; } [[nodiscard]] auto get_error() const { return sqlite3_errmsg(db); } ~database_t(); private: sqlite3* db = nullptr; }; #endif //SQL_H