#include #include #include #include #include #include "database.hpp" #include "utils.hpp" namespace simple_object_storage { bool Database::createObjectsTable() { const char* create_table_sql = "CREATE TABLE IF NOT EXISTS objects (" "hash TEXT PRIMARY KEY," "labeltags TEXT NOT NULL," // JSON array of label:tag pairs "metadata TEXT NOT NULL" ");"; char* err_msg = nullptr; int rc = sqlite3_exec(db_, create_table_sql, nullptr, nullptr, &err_msg); if (rc != SQLITE_OK) { std::string error = err_msg; sqlite3_free(err_msg); return false; } return true; } bool Database::createVersionTable() { const char* sql = "CREATE TABLE IF NOT EXISTS version_info (" "version INTEGER NOT NULL" ");"; char* err_msg = nullptr; int rc = sqlite3_exec(db_, sql, nullptr, nullptr, &err_msg); if (rc != SQLITE_OK) { std::string error = err_msg; sqlite3_free(err_msg); return false; } // Check if we need to insert initial version sqlite3_stmt* stmt; sql = "SELECT COUNT(*) FROM version_info;"; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } bool needs_initial_version = false; if (sqlite3_step(stmt) == SQLITE_ROW) { needs_initial_version = sqlite3_column_int(stmt, 0) == 0; } sqlite3_finalize(stmt); if (needs_initial_version) { sql = "INSERT INTO version_info (version) VALUES (?);"; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } sqlite3_bind_int(stmt, 1, CURRENT_VERSION); bool success = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return success; } return true; } bool Database::getVersion(int& version) { const char* sql = "SELECT version FROM version_info;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return false; } version = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); return true; } bool Database::setVersion(int version) { const char* sql = "UPDATE version_info SET version = ?;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) { return false; } sqlite3_bind_int(stmt, 1, version); bool success = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return success; } bool Database::migrate(int from_version, int to_version) { if (from_version < 3 && to_version == 3) { // Drop old table const char* drop_sql = "DROP TABLE IF EXISTS objects;"; char* err_msg = nullptr; int rc = sqlite3_exec(db_, drop_sql, nullptr, nullptr, &err_msg); if (rc != SQLITE_OK) { std::string error = err_msg; sqlite3_free(err_msg); return false; } // Create new table with updated schema return createObjectsTable(); } return false; } Database::Database(const std::filesystem::path& path) : path_(path) { int rc = sqlite3_open(path.string().c_str(), &db_); if (rc != SQLITE_OK) { throw std::runtime_error("Cannot open database: " + std::string(sqlite3_errmsg(db_))); } // Create version table and set initial version if (!createVersionTable()) { throw std::runtime_error("Failed to create version table"); } // Check current version and migrate if needed int current_version; if (!getVersion(current_version)) { throw std::runtime_error("Failed to get database version"); } if (current_version != CURRENT_VERSION) { if (!migrate(current_version, CURRENT_VERSION)) { throw std::runtime_error("Failed to migrate database"); } if (!setVersion(CURRENT_VERSION)) { throw std::runtime_error("Failed to update database version"); } } // Create objects table if it doesn't exist if (!createObjectsTable()) { throw std::runtime_error("Failed to create objects table"); } } Database::~Database() { if (db_) { sqlite3_close(db_); } } // bool Database::remove(const std::string& labeltag) { // std::string sql = "DELETE FROM objects WHERE labeltag = ?;"; // sqlite3_stmt* stmt; // if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { // return false; // } // sqlite3_bind_text(stmt, 1, labeltag.c_str(), -1, SQLITE_STATIC); // bool success = sqlite3_step(stmt) == SQLITE_DONE; // sqlite3_finalize(stmt); // return success; // } bool Database::remove_by_hash(const std::string& hash) { std::string sql = "DELETE FROM objects WHERE hash = ?;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { return false; } sqlite3_bind_text(stmt, 1, hash.c_str(), -1, SQLITE_STATIC); bool success = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return success; } bool Database::run_sql_text(const std::string& sql, const std::string& bind_text, dbEntry& entry) { sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { return false; } sqlite3_bind_text(stmt, 1, bind_text.c_str(), -1, SQLITE_STATIC); if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return false; } entry.hash = reinterpret_cast(sqlite3_column_text(stmt, 0)); std::string labeltags_str = reinterpret_cast(sqlite3_column_text(stmt, 1)); std::string metadata_str = reinterpret_cast(sqlite3_column_text(stmt, 2)); entry.labeltags = nlohmann::json::parse(labeltags_str).get>(); entry.metadata = nlohmann::json::parse(metadata_str); sqlite3_finalize(stmt); return true; } bool is_dec_uint64(const std::string& s) { if (s.empty()) return false; for (char c : s) { if (!std::isdigit(static_cast(c))) return false; } try { uint64_t x = std::stoull(s, nullptr, 10); std::string s2=std::to_string(x); return s2 == s; } catch (...) { return false; } } bool Database::get(const std::string& hash_or_labeltag, dbEntry& entry) { std::string key=trim(hash_or_labeltag); if (hash_or_labeltag.find(':') != std::string::npos) { return (run_sql_text("SELECT hash, labeltags, metadata FROM objects WHERE json_array_length(labeltags) > 0 AND EXISTS (SELECT 1 FROM json_each(labeltags) WHERE value = ?);", hash_or_labeltag, entry)); } if (is_dec_uint64(hash_or_labeltag)) { if (run_sql_text("SELECT hash, labeltags, metadata FROM objects WHERE hash = ?;", hash_or_labeltag, entry)) return true; } // try with a :latest tag std::string with_latest = hash_or_labeltag + ":latest"; return (run_sql_text("SELECT hash, labeltags, metadata FROM objects WHERE json_array_length(labeltags) > 0 AND EXISTS (SELECT 1 FROM json_each(labeltags) WHERE value = ?);", with_latest, entry)); } bool Database::list(std::vector& entries) { std::string sql = "SELECT hash, labeltags, metadata FROM objects;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { return false; } entries.clear(); while (sqlite3_step(stmt) == SQLITE_ROW) { dbEntry entry; entry.hash = reinterpret_cast(sqlite3_column_text(stmt, 0)); std::string labeltags_str = reinterpret_cast(sqlite3_column_text(stmt, 1)); std::string metadata_str = reinterpret_cast(sqlite3_column_text(stmt, 2)); entry.labeltags = nlohmann::json::parse(labeltags_str).get>(); entry.metadata = nlohmann::json::parse(metadata_str); entries.push_back(entry); } sqlite3_finalize(stmt); return true; } bool Database::merge_existing_entry(const dbEntry& existing, const dbEntry& new_entry, dbEntry& merged) { // Merge label:tag pairs std::set merged_labeltags(existing.labeltags.begin(), existing.labeltags.end()); merged_labeltags.insert(new_entry.labeltags.begin(), new_entry.labeltags.end()); // Create merged entry merged = new_entry; // Start with new entry's data merged.labeltags = std::vector(merged_labeltags.begin(), merged_labeltags.end()); // Update metadata - preserve fields from existing entry that aren't in new entry merged.metadata = existing.metadata; // Start with existing metadata for (const auto& [key, value] : new_entry.metadata.items()) { merged.metadata[key] = value; // Override with new values } // Ensure required fields are set correctly merged.metadata["labeltags"] = merged.labeltags; merged.metadata["hash"] = merged.hash; // Update database std::string sql = "UPDATE objects SET labeltags = ?, metadata = ? WHERE hash = ?;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { return false; } std::string labeltags_str = nlohmann::json(merged.labeltags).dump(); std::string metadata_str = merged.metadata.dump(); sqlite3_bind_text(stmt, 1, labeltags_str.c_str(), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, metadata_str.c_str(), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, merged.hash.c_str(), -1, SQLITE_STATIC); bool success = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return success; } bool Database::insert_new_entry(const dbEntry& entry) { std::string sql = "INSERT INTO objects (hash, labeltags, metadata) VALUES (?, ?, ?);"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { return false; } // Update metadata to include labeltags and hash nlohmann::json metadata = entry.metadata; metadata["labeltags"] = entry.labeltags; metadata["hash"] = entry.hash; std::string labeltags_str = nlohmann::json(entry.labeltags).dump(); std::string metadata_str = metadata.dump(); sqlite3_bind_text(stmt, 1, entry.hash.c_str(), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 2, labeltags_str.c_str(), -1, SQLITE_STATIC); sqlite3_bind_text(stmt, 3, metadata_str.c_str(), -1, SQLITE_STATIC); bool success = sqlite3_step(stmt) == SQLITE_DONE; sqlite3_finalize(stmt); return success; } bool Database::handle_tag_conflicts(const dbEntry& entry) { for (const auto& labeltag : entry.labeltags) { // Find all entries with this exact label:tag pair std::string find_sql = "SELECT hash, labeltags, metadata FROM objects WHERE labeltags LIKE ?;"; sqlite3_stmt* stmt; if (sqlite3_prepare_v2(db_, find_sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { return false; } std::string labeltag_pattern = "%\"" + labeltag + "\"%"; sqlite3_bind_text(stmt, 1, labeltag_pattern.c_str(), -1, SQLITE_STATIC); while (sqlite3_step(stmt) == SQLITE_ROW) { std::string other_hash = reinterpret_cast(sqlite3_column_text(stmt, 0)); if (other_hash == entry.hash) continue; // Skip our own entry std::string other_labeltags_str = reinterpret_cast(sqlite3_column_text(stmt, 1)); std::string other_metadata_str = reinterpret_cast(sqlite3_column_text(stmt, 2)); // Parse the other entry dbEntry other; other.hash = other_hash; other.labeltags = nlohmann::json::parse(other_labeltags_str).get>(); other.metadata = nlohmann::json::parse(other_metadata_str); // Remove the exact label:tag pair std::vector new_labeltags; for (const auto& other_labeltag : other.labeltags) { if (other_labeltag != labeltag) { new_labeltags.push_back(other_labeltag); } } // Update the other entry if it had the label:tag pair removed if (new_labeltags.size() != other.labeltags.size()) { other.labeltags = new_labeltags; other.metadata["labeltags"] = new_labeltags; // Update metadata to match std::string update_sql = "UPDATE objects SET labeltags = ?, metadata = ? WHERE hash = ?;"; sqlite3_stmt* update_stmt; if (sqlite3_prepare_v2(db_, update_sql.c_str(), -1, &update_stmt, nullptr) != SQLITE_OK) { sqlite3_finalize(stmt); return false; } std::string new_labeltags_str = nlohmann::json(new_labeltags).dump(); std::string new_metadata_str = other.metadata.dump(); sqlite3_bind_text(update_stmt, 1, new_labeltags_str.c_str(), -1, SQLITE_STATIC); sqlite3_bind_text(update_stmt, 2, new_metadata_str.c_str(), -1, SQLITE_STATIC); sqlite3_bind_text(update_stmt, 3, other.hash.c_str(), -1, SQLITE_STATIC); bool update_success = sqlite3_step(update_stmt) == SQLITE_DONE; sqlite3_finalize(update_stmt); if (!update_success) { sqlite3_finalize(stmt); return false; } } } sqlite3_finalize(stmt); } return true; } bool Database::update_or_insert(const dbEntry& entry) { // First try to get existing entry by hash dbEntry existing; bool exists = get(entry.hash, existing); if (exists) { // Merge with existing entry dbEntry merged; if (!merge_existing_entry(existing, entry, merged)) { return false; } // Handle tag conflicts return handle_tag_conflicts(merged); } else { // Insert new entry if (!insert_new_entry(entry)) { return false; } // Handle tag conflicts return handle_tag_conflicts(entry); } } } // namespace simple_object_storage