diff --git a/src/database.cpp b/src/database.cpp index d6deb4a..0edfef3 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -183,25 +183,46 @@ bool Database::remove_by_hash(const std::string& hash) { return success; } -bool Database::get(const std::string& hash, dbEntry& entry) { - std::string sql = "SELECT labels, tags, metadata FROM objects WHERE hash = ?;"; - sqlite3_stmt* stmt; +bool Database::get(const std::string& key, dbEntry& entry) { + std::string sql; + if (key.find(':') != std::string::npos) { + // Query by label:tag + sql = "SELECT hash, labels, tags, metadata FROM objects WHERE labels LIKE ? AND tags LIKE ?;"; + } else { + // Query by hash + sql = "SELECT hash, labels, tags, metadata 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); + if (key.find(':') != std::string::npos) { + // Split label:tag + size_t pos = key.find(':'); + std::string label = key.substr(0, pos); + std::string tag = key.substr(pos + 1); + + // Create JSON array patterns for LIKE query + std::string label_pattern = "%\"" + label + "\"%"; + std::string tag_pattern = "%\"" + tag + "\"%"; + + sqlite3_bind_text(stmt, 1, label_pattern.c_str(), -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, tag_pattern.c_str(), -1, SQLITE_STATIC); + } else { + sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_STATIC); + } if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return false; } - entry.hash = hash; - std::string labels_str = reinterpret_cast(sqlite3_column_text(stmt, 0)); - std::string tags_str = reinterpret_cast(sqlite3_column_text(stmt, 1)); - std::string metadata_str = reinterpret_cast(sqlite3_column_text(stmt, 2)); + entry.hash = reinterpret_cast(sqlite3_column_text(stmt, 0)); + std::string labels_str = reinterpret_cast(sqlite3_column_text(stmt, 1)); + std::string tags_str = reinterpret_cast(sqlite3_column_text(stmt, 2)); + std::string metadata_str = reinterpret_cast(sqlite3_column_text(stmt, 3)); entry.labels = nlohmann::json::parse(labels_str).get>(); entry.tags = nlohmann::json::parse(tags_str).get>(); @@ -237,69 +258,168 @@ bool Database::list(std::vector& entries) { return true; } +bool Database::merge_existing_entry(const dbEntry& existing, const dbEntry& new_entry, dbEntry& merged) { + // Merge labels and tags + std::set merged_labels(existing.labels.begin(), existing.labels.end()); + merged_labels.insert(new_entry.labels.begin(), new_entry.labels.end()); + std::set merged_tags(existing.tags.begin(), existing.tags.end()); + merged_tags.insert(new_entry.tags.begin(), new_entry.tags.end()); + + // Create merged entry + merged = new_entry; // Start with new entry's data + merged.labels = std::vector(merged_labels.begin(), merged_labels.end()); + merged.tags = std::vector(merged_tags.begin(), merged_tags.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["labels"] = merged.labels; + merged.metadata["tags"] = merged.tags; + merged.metadata["hash"] = merged.hash; + + // Update database + std::string sql = "UPDATE objects SET labels = ?, tags = ?, metadata = ? WHERE hash = ?;"; + sqlite3_stmt* stmt; + + if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { + return false; + } + + std::string labels_str = nlohmann::json(merged.labels).dump(); + std::string tags_str = nlohmann::json(merged.tags).dump(); + std::string metadata_str = merged.metadata.dump(); + + sqlite3_bind_text(stmt, 1, labels_str.c_str(), -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, tags_str.c_str(), -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, metadata_str.c_str(), -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 4, 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, labels, tags, metadata) VALUES (?, ?, ?, ?);"; + sqlite3_stmt* stmt; + + if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { + return false; + } + + // Update metadata to include labels, tags, and hash + nlohmann::json metadata = entry.metadata; + metadata["labels"] = entry.labels; + metadata["tags"] = entry.tags; + metadata["hash"] = entry.hash; + + std::string labels_str = nlohmann::json(entry.labels).dump(); + std::string tags_str = nlohmann::json(entry.tags).dump(); + std::string metadata_str = metadata.dump(); + + sqlite3_bind_text(stmt, 1, entry.hash.c_str(), -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, labels_str.c_str(), -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, tags_str.c_str(), -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 4, 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& label : entry.labels) { + // Find all entries with this label + std::string find_sql = "SELECT hash, labels, tags, metadata FROM objects WHERE labels LIKE ?;"; + sqlite3_stmt* stmt; + if (sqlite3_prepare_v2(db_, find_sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { + return false; + } + + std::string label_pattern = "%\"" + label + "\"%"; + sqlite3_bind_text(stmt, 1, label_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_labels_str = reinterpret_cast(sqlite3_column_text(stmt, 1)); + std::string other_tags_str = reinterpret_cast(sqlite3_column_text(stmt, 2)); + std::string other_metadata_str = reinterpret_cast(sqlite3_column_text(stmt, 3)); + + // Parse the other entry + dbEntry other; + other.hash = other_hash; + other.labels = nlohmann::json::parse(other_labels_str).get>(); + other.tags = nlohmann::json::parse(other_tags_str).get>(); + other.metadata = nlohmann::json::parse(other_metadata_str); + + // Remove any tags that are in our entry + std::vector new_tags; + for (const auto& tag : other.tags) { + if (std::find(entry.tags.begin(), entry.tags.end(), tag) == entry.tags.end()) { + new_tags.push_back(tag); + } + } + + // Update the other entry if it had any tags removed + if (new_tags.size() != other.tags.size()) { + other.tags = new_tags; + other.metadata["tags"] = new_tags; // Update metadata to match + + std::string update_sql = "UPDATE objects SET tags = ?, 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_tags_str = nlohmann::json(new_tags).dump(); + std::string new_metadata_str = other.metadata.dump(); + + sqlite3_bind_text(update_stmt, 1, new_tags_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 + // First try to get existing entry by hash dbEntry existing; bool exists = get(entry.hash, existing); if (exists) { - // Merge labels and tags - std::set merged_labels(existing.labels.begin(), existing.labels.end()); - merged_labels.insert(entry.labels.begin(), entry.labels.end()); - std::set merged_tags(existing.tags.begin(), existing.tags.end()); - merged_tags.insert(entry.tags.begin(), entry.tags.end()); - - // Create new entry with merged data - dbEntry merged = entry; - merged.labels = std::vector(merged_labels.begin(), merged_labels.end()); - merged.tags = std::vector(merged_tags.begin(), merged_tags.end()); - - // Merge metadata - for (const auto& [key, value] : entry.metadata.items()) { - merged.metadata[key] = value; - } - - // Update database - std::string sql = "UPDATE objects SET labels = ?, tags = ?, metadata = ? WHERE hash = ?;"; - sqlite3_stmt* stmt; - - if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { + // Merge with existing entry + dbEntry merged; + if (!merge_existing_entry(existing, entry, merged)) { return false; } - - std::string labels_str = nlohmann::json(merged.labels).dump(); - std::string tags_str = nlohmann::json(merged.tags).dump(); - std::string metadata_str = merged.metadata.dump(); - sqlite3_bind_text(stmt, 1, labels_str.c_str(), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, tags_str.c_str(), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, metadata_str.c_str(), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 4, merged.hash.c_str(), -1, SQLITE_STATIC); - - bool success = sqlite3_step(stmt) == SQLITE_DONE; - sqlite3_finalize(stmt); - return success; + // Handle tag conflicts + return handle_tag_conflicts(merged); } else { // Insert new entry - std::string sql = "INSERT INTO objects (hash, labels, tags, metadata) VALUES (?, ?, ?, ?);"; - sqlite3_stmt* stmt; - - if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) { + if (!insert_new_entry(entry)) { return false; } - - std::string labels_str = nlohmann::json(entry.labels).dump(); - std::string tags_str = nlohmann::json(entry.tags).dump(); - std::string metadata_str = entry.metadata.dump(); - sqlite3_bind_text(stmt, 1, entry.hash.c_str(), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, labels_str.c_str(), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, tags_str.c_str(), -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 4, metadata_str.c_str(), -1, SQLITE_STATIC); - - bool success = sqlite3_step(stmt) == SQLITE_DONE; - sqlite3_finalize(stmt); - return success; + // Handle tag conflicts + return handle_tag_conflicts(entry); } } diff --git a/src/database.hpp b/src/database.hpp index 554728b..2bd34e2 100644 --- a/src/database.hpp +++ b/src/database.hpp @@ -19,7 +19,7 @@ class dbEntry { class Database { public: - static const int CURRENT_VERSION = 2; + static const int CURRENT_VERSION = 3; Database(const std::filesystem::path& path); ~Database(); @@ -37,6 +37,11 @@ class Database { bool setVersion(int version); bool migrate(int from_version, int to_version); bool createObjectsTable(); + + // New utility functions + bool merge_existing_entry(const dbEntry& existing, const dbEntry& new_entry, dbEntry& merged); + bool insert_new_entry(const dbEntry& entry); + bool handle_tag_conflicts(const dbEntry& entry); }; } // namespace simple_object_storage diff --git a/test.sh b/test.sh index a6fa972..596239a 100755 --- a/test.sh +++ b/test.sh @@ -83,7 +83,9 @@ echo "upload response: ${UPLOAD_RESPONSE}" OBJECT_HASH=$(echo ${UPLOAD_RESPONSE} | jq -r '.hash') # check the hash matches. -CHECK_HASH=$(curl -s "${BASE_URL}/hash/${BASE_TAG}:test1" | jq -r '.hash') +CMD="${BASE_URL}/hash/${BASE_TAG}:test1" +echo "checking hash via ${CMD}" +CHECK_HASH=$(curl -s "${CMD}" | jq -r '.hash') [ "${OBJECT_HASH}" != "${CHECK_HASH}" ] && die "hash does not match: ${OBJECT_HASH} != ${CHECK_HASH}" # get md5sum of this file