diff --git a/CHANGELOG.md b/CHANGELOG.md index c519bab..5a1cb5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix removing attachments with numeric keys, [PR-113](https://github.com/reductstore/reduct-cpp/pull/113) +- Fix removing entry attachments whose keys start with `$`, #114 ## 1.18.0 - 2026-02-04 diff --git a/src/reduct/bucket.cc b/src/reduct/bucket.cc index 149d7c4..e247833 100644 --- a/src/reduct/bucket.cc +++ b/src/reduct/bucket.cc @@ -251,15 +251,23 @@ class Bucket : public IBucket { const std::set& attachment_keys) const noexcept override { QueryOptions options; if (!attachment_keys.empty()) { + const auto escape_attachment_key = [](const std::string& key) { + if (!key.empty() && key.front() == '$') { + return fmt::format("${}", key); + } + return key; + }; + nlohmann::json when; when["$in"] = nlohmann::json::array(); when["$in"].push_back({{"&key", {{"$cast", "string"}}}}); for (const auto& key : attachment_keys) { - when["$in"].push_back(key); + when["$in"].push_back(escape_attachment_key(key)); } options.when = when.dump(); } + Batch remove_batch; const auto meta_entry = fmt::format("{}/$meta", entry_name); auto query_err = QueryV2({meta_entry}, std::nullopt, std::nullopt, std::move(options), diff --git a/tests/reduct/entry_api_test.cc b/tests/reduct/entry_api_test.cc index 680d903..d70b316 100644 --- a/tests/reduct/entry_api_test.cc +++ b/tests/reduct/entry_api_test.cc @@ -782,6 +782,27 @@ TEST_CASE("reduct::IBucket should remove entry attachments with numeric keys", " REQUIRE(stored_after.empty()); } +TEST_CASE("reduct::IBucket should remove entry attachments with reserved keys", "[entry_api][1_19]") { + Fixture ctx; + auto [bucket, _] = ctx.client->CreateBucket(kBucketName); + REQUIRE(bucket); + + IBucket::AttachmentMap attachments{ + {"meta-1", R"({"value":1})"}, + {"$system", R"({"value":"test"})"}, + {"meta-2", R"({"value":2})"}, + }; + + REQUIRE(bucket->WriteAttachments("entry-1", attachments) == Error::kOk); + REQUIRE(bucket->RemoveAttachments("entry-1", std::set{"meta-1", "$system"}) == Error::kOk); + + auto [stored, err] = bucket->ReadAttachments("entry-1"); + REQUIRE(err == Error::kOk); + REQUIRE(stored.size() == 1); + REQUIRE(stored.contains("meta-2")); + REQUIRE(nlohmann::json::parse(stored.at("meta-2")) == nlohmann::json::parse(attachments.at("meta-2"))); +} + TEST_CASE("Batch should slice data", "[batch]") { auto batch = IBucket::Batch();